-
Notifications
You must be signed in to change notification settings - Fork 24
Description
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):
-
Use
GetDecisionBulkwith 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
-
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 resourcesGetDecisionMultiEntity: 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:
- Batch Entity Resolution: Single ERS call for all entities
- Policy Pre-loading: Load resource policies once
- Parallel Evaluation: Use goroutine pool to evaluate entities concurrently
- 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
- `service/authorization/v2/authorization.proto` - Add message definitions and RPC
- `service/authorization/v2/authorization.go` - Implement service method
- `service/internal/access/v2/pdp.go` - Add optimized PDP method
- `service/authorization/v2/authorization_test.go` - Add validation tests
- `protocol/go/` - Regenerate via `make proto-generate`
- `sdk/sdkconnect/authorizationv2.go` - Will be auto-generated
- `docs/openapi/` and `docs/grpc/` - Will be auto-generated
Related Issues/PRs
- Related to KAS rewrap optimization efforts
- Complements existing entitlement policy caching (fix(core): CORS #2787)
- Supports obligation enforcement feature (feat(authz): wire up obligations enforcement in auth service #2756)
Priority Justification
High Priority because:
- Clear, validated use case from messaging and email client scenarios
- No efficient workaround with current API
- Blocking adoption for real-time messaging and collaboration platforms
- Fills architectural gap in API design
- Performance-critical for user experience
Questions for Discussion
- Should the entity limit be 500 or higher/lower?
- Should the resource limit be 20 or adjusted based on expected use cases?
- Should we support different actions per entity, or keep it strictly single-action?
- What's the priority relative to other authorization features?