Skip to content

Feature Request: Add GetDecisionMultiEntity endpoint to Authorization Service V2 #2953

@jp-ayyappan

Description

@jp-ayyappan

Problem Statement

The current Authorization V2 API has a significant gap for a common real-world use case: checking if many entities (e.g., 500 group members) can access a small number of resources (e.g., 1-5 messages or documents).

Current Options (both inadequate):

  1. Use GetDecisionBulk with 500 requests

    • ❌ Massive payload duplication (e.g., 5 resources × 500 members = 2,500 resource objects)
    • ❌ Sequential processing with no batching optimizations
    • ❌ Entity re-resolution for each request
    • ❌ Same resources evaluated 500 times
  2. Make 500 separate API calls

    • ❌ Unacceptable network latency
    • ❌ Connection pool exhaustion
    • ❌ Unusable for real-time scenarios

Use Cases

This endpoint is critical for several real-world scenarios:

1. Group Messaging Applications

When sending a message to a 500-member group:

  • Need to determine which members can decrypt the message
  • Must complete in sub-second time for real-time messaging
  • Only checking access to 1 message resource

2. Email Clients with Sensitive Attachments

When sending an email with TDF-protected attachments to 200 recipients:

  • Need to verify which recipients can access 3 attached documents
  • Should display access warnings before sending
  • Real-time feedback required

3. Collaborative Platforms

When sharing documents in a virtual meeting with many attendees:

  • Need to check access to a small number of shared documents
  • Many participant entities
  • Real-time authorization required for UX

4. Healthcare/Compliance

Auditing which members of a care team can access patient records:

  • 1-2 patient record resources
  • Many healthcare provider entities
  • Compliance reporting requirements

5. Shared Resource Access Control

Any scenario where multiple users need access to the same resource(s):

  • File sharing platforms
  • Collaborative editing tools
  • Content management systems

Proposed Solution

Add a new endpoint GetDecisionMultiEntity that is the symmetric inverse of GetDecisionMultiResource:

  • GetDecisionMultiResource: 1 entity × 1 action × MANY resources
  • GetDecisionMultiEntity: MANY entities × 1 action × few resources (proposed)

API Design

```protobuf
// Can multiple entities access the same resource(s)?
// 1. multiple entity references (actors)
// 2. one action
// 3. few resources (optimized for small resource count)
//
// This is the inverse of GetDecisionMultiResource, optimized for
// scenarios like group messaging where many users need access to
// the same few resources
message GetDecisionMultiEntityRequest {
// Multiple entities to check authorization for (1-500)
repeated EntityIdentifier entity_identifiers = 1 [(buf.validate.field).repeated = {
min_items: 1
max_items: 500
}];

// Single action to check
policy.Action action = 2 [(buf.validate.field).required = true];

// Small set of resources (optimized for 1-20)
repeated Resource resources = 3 [(buf.validate.field).repeated = {
min_items: 1
max_items: 20
}];

// obligations the requester is capable of fulfilling
repeated string fulfillable_obligation_fqns = 4 [(buf.validate.field).cel = {
id: "obligation_value_fqns_valid"
message: "if provided, fulfillable_obligation_fqns must be between 1 and 50 in count with all valid FQNs"
expression: "this.size() == 0 || (this.size() <= 50 && this.all(item, item.isUri()))"
}];

option (buf.validate.message).cel = {
id: "get_decision_multi_entity.action_name_required"
message: "action.name must be provided"
expression: "has(this.action.name)"
};
}

message EntityDecisionResult {
// Ephemeral ID from the entity identifier
string ephemeral_entity_id = 1;

// Convenience flag: are all resources permitted for this entity?
google.protobuf.BoolValue all_permitted = 2;

// Individual resource decisions for this entity
repeated ResourceDecision resource_decisions = 3;
}

message GetDecisionMultiEntityResponse {
// One result per entity
repeated EntityDecisionResult entity_results = 1;
}
```

Service Definition

```protobuf
service AuthorizationService {
rpc GetDecision(GetDecisionRequest) returns (GetDecisionResponse) {}
rpc GetDecisionMultiResource(GetDecisionMultiResourceRequest) returns (GetDecisionMultiResourceResponse) {}
rpc GetDecisionMultiEntity(GetDecisionMultiEntityRequest) returns (GetDecisionMultiEntityResponse) {} // NEW
rpc GetDecisionBulk(GetDecisionBulkRequest) returns (GetDecisionBulkResponse) {}

rpc GetEntitlements(GetEntitlementsRequest) returns (GetEntitlementsResponse) {}
}
```

Benefits

1. Dramatic Payload Reduction

Scenario: 500 entities, 5 resources

Endpoint Entities Resources Total Objects Reduction
GetDecisionBulk 500 2,500 (5×500) 3,000 baseline
GetDecisionMultiEntity 500 5 505 83%

2. Performance Optimizations

The implementation can leverage:

  • Batch entity resolution: Resolve all entities in a single ERS call
  • Policy caching: Load resource policies once, evaluate against all entities
  • Attribute deduplication: Common attributes resolved once
  • Parallel evaluation: Entities can be evaluated in parallel since resources are fixed

3. Real-Time Capability

Sub-second authorization for group messaging scenarios becomes feasible.

4. API Completeness

Creates symmetric coverage of authorization patterns:

```
┌─────────────────────────────────────────────────────────────┐
│ Authorization API │
├─────────────────────────────────────────────────────────────┤
│ │
│ GetDecision │
│ └─ 1 entity × 1 action × 1 resource │
│ │
│ GetDecisionMultiResource │
│ └─ 1 entity × 1 action × MANY resources (1-1000) │
│ │
│ GetDecisionMultiEntity (PROPOSED) │
│ └─ MANY entities (1-500) × 1 action × few resources │
│ │
│ GetDecisionBulk │
│ └─ MANY (entity × action × resource) combinations │
│ │
└─────────────────────────────────────────────────────────────┘
```

Implementation Notes

Core Service Method

Location: `service/authorization/v2/authorization.go`

```go
func (as *Service) GetDecisionMultiEntity(
ctx context.Context,
req *connect.Request[authzV2.GetDecisionMultiEntityRequest],
) (*connect.Response[authzV2.GetDecisionMultiEntityResponse], error) {

ctx, span := as.Tracer.Start(ctx, "GetDecisionMultiEntity")
defer span.End()

pdp, err := access.NewJustInTimePDP(ctx, as.logger, as.sdk, as.cache)
if err != nil {
    return nil, statusifyError(ctx, as.logger, err)
}

entityIdentifiers := req.Msg.GetEntityIdentifiers()
action := req.Msg.GetAction()
resources := req.Msg.GetResources()
fulfillableObligations := req.Msg.GetFulfillableObligationFqns()

reqContext, err := as.getDecisionRequestContext(ctx)
if err != nil {
    return nil, statusifyError(ctx, as.logger, err)
}

// Optimized path: evaluate multiple entities against same resources
entityResults, err := pdp.GetDecisionMultiEntity(
    ctx,
    entityIdentifiers,
    action,
    resources,
    reqContext,
    fulfillableObligations,
)
if err != nil {
    return nil, statusifyError(ctx, as.logger, err)
}

return connect.NewResponse(&authzV2.GetDecisionMultiEntityResponse{
    EntityResults: entityResults,
}), nil

}
```

PDP Optimization Strategy

Key optimizations in the PDP implementation:

  1. Batch Entity Resolution: Single ERS call for all entities
  2. Policy Pre-loading: Load resource policies once
  3. Parallel Evaluation: Use goroutine pool to evaluate entities concurrently
  4. Cache Leverage: Maximize use of entitlement policy cache

Testing Requirements

  • Unit tests for validation (min/max entity counts, resource counts)
  • Integration tests with ERS for batch entity resolution
  • Performance benchmarks comparing to `GetDecisionBulk` workaround
  • Load tests with 500 entities × 5 resources

Example Usage

```go
// Messaging client: Check which group members can decrypt a message
req := &authzV2.GetDecisionMultiEntityRequest{
EntityIdentifiers: groupMemberTokens, // 500 members
Action: &policy.Action{Name: "decrypt"},
Resources: []*authzV2.Resource{
{
EphemeralId: "msg-12345",
Resource: &authzV2.Resource_AttributeValues_{
AttributeValues: &authzV2.Resource_AttributeValues{
Fqns: []string{
"https://example.com/attr/clearance/value/secret",
"https://example.com/attr/group/value/engineering",
},
},
},
},
},
}

resp, err := client.AuthorizationV2.GetDecisionMultiEntity(ctx, req)

// Filter members who can't access before sending encrypted keys
authorizedMembers := []string{}
for _, result := range resp.EntityResults {
if result.AllPermitted.Value {
authorizedMembers = append(authorizedMembers, result.EphemeralEntityId)
}
}
```

Files to Modify

  1. `service/authorization/v2/authorization.proto` - Add message definitions and RPC
  2. `service/authorization/v2/authorization.go` - Implement service method
  3. `service/internal/access/v2/pdp.go` - Add optimized PDP method
  4. `service/authorization/v2/authorization_test.go` - Add validation tests
  5. `protocol/go/` - Regenerate via `make proto-generate`
  6. `sdk/sdkconnect/authorizationv2.go` - Will be auto-generated
  7. `docs/openapi/` and `docs/grpc/` - Will be auto-generated

Related Issues/PRs

Priority Justification

High Priority because:

  1. Clear, validated use case from messaging and email client scenarios
  2. No efficient workaround with current API
  3. Blocking adoption for real-time messaging and collaboration platforms
  4. Fills architectural gap in API design
  5. Performance-critical for user experience

Questions for Discussion

  1. Should the entity limit be 500 or higher/lower?
  2. Should the resource limit be 20 or adjusted based on expected use cases?
  3. Should we support different actions per entity, or keep it strictly single-action?
  4. What's the priority relative to other authorization features?

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions