From b6f138428395f633ae9b89dc2a748128d2a70a5b Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Tue, 7 Apr 2026 09:32:43 -0700 Subject: [PATCH 1/8] chore(docs): restructure Authorization page (#274) - Rename sections to method names: GetEntitlements, GetDecision, GetDecisionBulk - Add Setup section, EntityIdentifier type reference with cross-language table - Add Signature/Parameters/Returns to each method - Add Type Reference: Decision, ResourceDecision, Resource, EntityEntitlements, GetDecisionMultiResourceResponse - Update Entitlements with Scope to use v2 API - Add Go imports to each code example - V1 legacy examples in collapsible blocks Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/sdks/authorization.mdx | 1590 +++++++++++++++++------------------ 1 file changed, 765 insertions(+), 825 deletions(-) diff --git a/docs/sdks/authorization.mdx b/docs/sdks/authorization.mdx index ab0ecffc..971e2003 100644 --- a/docs/sdks/authorization.mdx +++ b/docs/sdks/authorization.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 6 +sidebar_position: 7 title: Authorization --- @@ -7,209 +7,263 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import JsAuthNote from '../../code_samples/js_auth_note.mdx' -# Making Authorization Decisions +# Authorization -OpenTDF's authorization system provides two primary methods for access control: **Entitlements** and **Authorization Decisions**. Understanding when and how to use each is crucial for implementing effective data security. +OpenTDF's authorization system answers two questions: *"What can this entity access?"* ([GetEntitlements](#getentitlements)) and *"Can this entity access this specific resource?"* ([GetDecision](#getdecision)). For batch checks, use [GetDecisionBulk](#getdecisionbulk). -## Overview +## Setup -### Entitlements vs Decisions - -- **Entitlements**: Answer "*What can this entity access?*" - Returns all attribute values an entity is entitled to access -- **Decisions**: Answer "*Can this entity access this specific resource?*" - Returns a permit/deny decision for specific resource access - -### Typical Workflow - -1. **During Resource Discovery**: Use `GetEntitlements` to show users what data they can access -2. **During Resource Access**: Use `GetDecision` to enforce access controls when accessing specific resources -3. **For Bulk Operations**: Use `GetDecisionBulk` for efficient batch authorization - -## Authentication Setup - -All authorization calls require proper authentication. Here's how to set up the SDK client: - - +All examples on this page assume you have created a platform client. See [Authentication](/sdks/authentication) for full details including DPoP key binding. ```go -package main - -import ( - "context" - "fmt" - "log" - - "github.com/opentdf/platform/protocol/go/authorization" - authorizationv2 "github.com/opentdf/platform/protocol/go/authorization/v2" - "github.com/opentdf/platform/protocol/go/entity" - "github.com/opentdf/platform/protocol/go/policy" - "github.com/opentdf/platform/sdk" - "google.golang.org/protobuf/proto" +client, err := sdk.New("http://localhost:8080", + sdk.WithClientCredentials("opentdf", "secret", nil), ) - -func main() { - platformEndpoint := "http://localhost:8080" - - // Create authenticated client - client, err := sdk.New( - platformEndpoint, - sdk.WithClientCredentials("opentdf", "secret", nil), - ) - if err != nil { - log.Fatal(err) - } - - // Client is ready for authorization calls +if err != nil { + log.Fatal(err) } + +// All Go snippets below use `client` and `context.Background()`. ``` ```java -import io.opentdf.platform.sdk.*; -import io.opentdf.platform.authorization.*; -import io.opentdf.platform.entity.*; -import io.opentdf.platform.policy.*; -import java.util.List; -import java.util.concurrent.ExecutionException; - -public class AuthorizationSetup { - public static void main(String[] args) { - String clientId = "opentdf"; - String clientSecret = "secret"; - String platformEndpoint = "http://localhost:8080"; - - SDKBuilder builder = new SDKBuilder(); - SDK sdk = builder.platformEndpoint(platformEndpoint) - .clientSecret(clientId, clientSecret) - .useInsecurePlaintextConnection(true) - .build(); - - // SDK is ready for authorization calls - } -} +SDK sdk = SDKBuilder.newBuilder() + .platformEndpoint("http://localhost:8080") + .clientSecret("opentdf", "secret") + .useInsecurePlaintextConnection(true) + .build(); + +// All Java snippets below use `sdk`. ``` ```typescript -// Option 1: Using interceptors (recommended for new code) import { authTokenInterceptor, clientCredentialsTokenProvider } from '@opentdf/sdk'; import { PlatformClient } from '@opentdf/sdk/platform'; const platformClient = new PlatformClient({ interceptors: [authTokenInterceptor(clientCredentialsTokenProvider({ - clientId: 'opentdf', - clientSecret: 'secret', + clientId: 'opentdf', clientSecret: 'secret', oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', }))], platformUrl: 'http://localhost:8080', }); -// Option 2: Legacy AuthProvider (deprecated — use interceptors for new code) -import { AuthProviders, OpenTDF } from '@opentdf/sdk'; +// All JavaScript snippets below use `platformClient`. +``` -const authProvider = await AuthProviders.clientSecretAuthProvider({ - clientId: 'opentdf', - clientSecret: 'secret', - oidcOrigin: 'http://localhost:8080/auth/realms/opentdf', -}); -const client = new OpenTDF({ authProvider, platformUrl: 'http://localhost:8080' }); -await client.ready; + + -const platformClient2 = new PlatformClient({ - authProvider, - platformUrl: 'http://localhost:8080', -}); + + +### EntityIdentifier + +Every authorization call requires an `EntityIdentifier` — the entity (user, service, etc.) you're asking about. The entity can be identified by email, username, client ID, JWT token, or UUID. + +**Go** — use the v2 helper functions: + +| Helper | Description | +|--------|-------------| +| `authorizationv2.ForEmail(email)` | Identify by email address | +| `authorizationv2.ForClientID(clientID)` | Identify by client ID (service account / NPE) | +| `authorizationv2.ForUserName(username)` | Identify by username | +| `authorizationv2.ForToken(jwt)` | Resolve entity from a JWT token | +| `authorizationv2.WithRequestToken()` | Derive entity from the request's Authorization header | + +```go +import authorizationv2 "github.com/opentdf/platform/protocol/go/authorization/v2" + +req := &authorizationv2.GetDecisionRequest{ + EntityIdentifier: authorizationv2.ForEmail("alice@example.com"), + // ... +} +``` + +**Java** — build the nested proto structure: + +```java +EntityIdentifier.newBuilder() + .setEntityChain( + EntityChain.newBuilder() + .addEntities( + Entity.newBuilder() + .setId("user-1") + .setEmailAddress("alice@example.com") // or .setClientId(), .setUserName(), etc. + ) + ) + .build() +``` + +**JavaScript** — use the `identifier` oneof: + +```typescript +{ + entityIdentifier: { + identifier: { + case: 'entityChain', + value: { + entities: [ + { + ephemeralId: 'user-1', + entityType: { + case: 'emailAddress', // or 'clientId', 'userName', 'token' + value: 'alice@example.com', + }, + }, + ], + }, + }, + }, +} +``` + +**Supported entity types:** + +| Type | Go helper | Java setter | JS `case` | +|------|-----------|-------------|-----------| +| Email | `ForEmail(email)` | `.setEmailAddress(email)` | `'emailAddress'` | +| Client ID | `ForClientID(id)` | `.setClientId(id)` | `'clientId'` | +| Username | `ForUserName(name)` | `.setUserName(name)` | `'userName'` | +| JWT Token | `ForToken(jwt)` | `.setToken(Token.newBuilder().setJwt(jwt))` | `'token'` | +| UUID | — | `.setUuid(uuid)` | `'uuid'` | + +--- + +## GetEntitlements + +Returns all attribute values an entity is entitled to access. Use this for building UIs that show available data, pre-filtering content, or understanding an entity's overall access scope. + +**Signature** + + + + +```go +client.AuthorizationV2.GetEntitlements(ctx, &authorizationv2.GetEntitlementsRequest{...}) +``` + + + + +```java +sdk.getServices().authorization().getEntitlements(req).get() +``` + + + + +```typescript +await platformClient.v2.authorization.getEntitlements({ ... }) ``` -## Getting Entitlements +**Parameters** -Use `GetEntitlements` to discover what attribute values an entity can access. This is useful for: -- Building user interfaces that show available data -- Pre-filtering content based on user permissions -- Understanding an entity's overall access scope +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `entityIdentifier` | `EntityIdentifier` | Yes | The entity to query. In Go, use helpers like `authorizationv2.ForEmail("user@example.com")`. | +| `withComprehensiveHierarchy` | `bool` | No | When true, returns all entitled values for attributes with hierarchy rules, propagating down from the entitled value. | -### Basic Entitlements Query +**Example** -#### V2 API (Recommended) +```go +import authorizationv2 "github.com/opentdf/platform/protocol/go/authorization/v2" + +entitlementReq := &authorizationv2.GetEntitlementsRequest{ + EntityIdentifier: authorizationv2.ForEmail("bob@OrgA.com"), +} + +entitlements, err := client.AuthorizationV2.GetEntitlements( + context.Background(), + entitlementReq, +) +if err != nil { + log.Fatal(err) +} + +for _, entitlement := range entitlements.GetEntitlements() { + fmt.Printf("Entity has access to: %v\n", + entitlement.GetActionsPerAttributeValueFqn()) +} +``` + +To expand hierarchy rules, set `WithComprehensiveHierarchy`: ```go -func getEntitlementsV2(client *sdk.SDK) { - entitlementReq := &authorizationv2.GetEntitlementsRequest{ - EntityIdentifier: authorizationv2.ForEmail("bob@OrgA.com"), - } +import ( + authorizationv2 "github.com/opentdf/platform/protocol/go/authorization/v2" + "google.golang.org/protobuf/proto" +) - entitlements, err := client.AuthorizationV2.GetEntitlements( - context.Background(), - entitlementReq, - ) - if err != nil { - log.Fatal(err) - } +entitlementReq := &authorizationv2.GetEntitlementsRequest{ + EntityIdentifier: authorizationv2.ForEmail("user@company.com"), + WithComprehensiveHierarchy: proto.Bool(true), +} - // Process entitlements - for _, entitlement := range entitlements.GetEntitlements() { - fmt.Printf("Entity has access to: %v\n", - entitlement.GetActionsPerAttributeValueFqn()) - } +entitlements, err := client.AuthorizationV2.GetEntitlements( + context.Background(), + entitlementReq, +) +if err != nil { + log.Fatal(err) } + +log.Printf("Scoped entitlements: %v", entitlements.GetEntitlements()) ```
V1 API (Legacy) ```go -func getEntitlementsV1(client *sdk.SDK) { - // V1 API also supports GetEntitlements, but uses a different request shape. - // Here we use GetDecisions to show bulk decision-based entitlement discovery. - decisionRequests := []*authorization.DecisionRequest{{ - Actions: []*policy.Action{{Name: "read"}}, - EntityChains: []*authorization.EntityChain{{ - Id: "ec1", - Entities: []*authorization.Entity{{ - EntityType: &authorization.Entity_EmailAddress{ - EmailAddress: "bob@OrgA.com", - }, - Category: authorization.Entity_CATEGORY_SUBJECT, - }}, - }}, - // Query with multiple resource attributes to understand scope - ResourceAttributes: []*authorization.ResourceAttribute{{ - AttributeValueFqns: []string{ - "https://company.com/attr/clearance/value/public", - "https://company.com/attr/clearance/value/confidential", +import ( + "github.com/opentdf/platform/protocol/go/authorization" + "github.com/opentdf/platform/protocol/go/policy" +) + +decisionRequests := []*authorization.DecisionRequest{{ + Actions: []*policy.Action{{Name: "read"}}, + EntityChains: []*authorization.EntityChain{{ + Id: "ec1", + Entities: []*authorization.Entity{{ + EntityType: &authorization.Entity_EmailAddress{ + EmailAddress: "bob@OrgA.com", }, + Category: authorization.Entity_CATEGORY_SUBJECT, }}, - }} - - decisionRequest := &authorization.GetDecisionsRequest{ - DecisionRequests: decisionRequests, - } + }}, + ResourceAttributes: []*authorization.ResourceAttribute{{ + AttributeValueFqns: []string{ + "https://company.com/attr/clearance/value/public", + "https://company.com/attr/clearance/value/confidential", + }, + }}, +}} - decisionResponse, err := client.Authorization.GetDecisions( - context.Background(), - decisionRequest, - ) - if err != nil { - log.Fatal(err) - } +decisionResponse, err := client.Authorization.GetDecisions( + context.Background(), + &authorization.GetDecisionsRequest{DecisionRequests: decisionRequests}, +) +if err != nil { + log.Fatal(err) +} - // Process decisions to understand entitlements - for _, dr := range decisionResponse.GetDecisionResponses() { - fmt.Printf("Entity chain %s has decision: %v\n", - dr.GetEntityChainId(), dr.GetDecision()) - } +for _, dr := range decisionResponse.GetDecisionResponses() { + fmt.Printf("Entity chain %s has decision: %v\n", + dr.GetEntityChainId(), dr.GetDecision()) } ``` @@ -219,32 +273,28 @@ func getEntitlementsV1(client *sdk.SDK) { ```java -public void getEntitlements(SDK sdk) throws ExecutionException, InterruptedException { - GetEntitlementsRequest request = GetEntitlementsRequest.newBuilder() - .setEntityIdentifier( - EntityIdentifier.newBuilder() - .setEntityChain( - EntityChain.newBuilder() - .addEntities( - Entity.newBuilder() - .setId("user-bob") - .setEmailAddress("bob@OrgA.com") - ) - ) - ) - .build(); - - GetEntitlementsResponse resp = sdk.getServices() - .authorization() - .getEntitlements(request) - .get(); - - List entitlements = resp.getEntitlementsList(); - - for (EntityEntitlements entitlement : entitlements) { - System.out.println("Entitled to: " + - entitlement.getActionsPerAttributeValueFqnMap()); - } +GetEntitlementsRequest request = GetEntitlementsRequest.newBuilder() + .setEntityIdentifier( + EntityIdentifier.newBuilder() + .setEntityChain( + EntityChain.newBuilder() + .addEntities( + Entity.newBuilder() + .setId("user-bob") + .setEmailAddress("bob@OrgA.com") + ) + ) + ) + .build(); + +GetEntitlementsResponse resp = sdk.getServices() + .authorization() + .getEntitlements(request) + .get(); + +for (EntityEntitlements entitlement : resp.getEntitlementsList()) { + System.out.println("Entitled to: " + + entitlement.getActionsPerAttributeValueFqnMap()); } ``` @@ -252,236 +302,222 @@ public void getEntitlements(SDK sdk) throws ExecutionException, InterruptedExcep ```typescript -async function getEntitlements(platformClient: PlatformClient) { - const response = await platformClient.v2.authorization.getEntitlements({ - entityIdentifier: { - identifier: { - case: 'entityChain', - value: { - entities: [ - { - ephemeralId: 'user-bob', - entityType: { - case: 'emailAddress', - value: 'bob@OrgA.com', - }, +const response = await platformClient.v2.authorization.getEntitlements({ + entityIdentifier: { + identifier: { + case: 'entityChain', + value: { + entities: [ + { + ephemeralId: 'user-bob', + entityType: { + case: 'emailAddress', + value: 'bob@OrgA.com', }, - ], - }, + }, + ], }, }, - }); + }, +}); - for (const entitlement of response.entitlements) { - console.log('Entitled to:', entitlement.actionsPerAttributeValueFqn); - } +for (const entitlement of response.entitlements) { + console.log('Entitled to:', entitlement.actionsPerAttributeValueFqn); } ``` +To expand hierarchy rules: + +```typescript +const response = await platformClient.v2.authorization.getEntitlements({ + entityIdentifier: { + identifier: { + case: 'entityChain', + value: { + entities: [ + { + ephemeralId: 'user-123', + entityType: { case: 'emailAddress', value: 'user@company.com' }, + }, + ], + }, + }, + }, + withComprehensiveHierarchy: true, +}); + +console.log('Scoped entitlements:', response.entitlements); +``` + -### Entitlements with Scope +**Returns** + +A list of [EntityEntitlements](#entityentitlements) objects, each containing a map of attribute value FQNs to the actions the entity can perform on them. + +--- + +## GetDecision -You can limit entitlement queries to specific attribute hierarchies: +Returns a permit/deny decision for a specific entity + action + resource combination. This is the enforcement point in your application. + +**Signature** ```go -// Note: This example uses the v1 API as WithComprehensiveHierarchy is a v1-only feature -func getEntitlementsWithScope(client *sdk.SDK) { - entitlementReq := &authorization.GetEntitlementsRequest{ - EntityIdentifier: &authorization.EntityIdentifier{ - EntityChain: &entity.EntityChain{ - Entities: []*entity.Entity{ - { - Id: "user-123", - EntityType: &entity.Entity_EmailAddress{ - EmailAddress: "user@company.com", - }, - }, - }, - }, - }, - // When true, returns all entitled values for attributes with hierarchy rules, propagating down from the entitled value - WithComprehensiveHierarchy: proto.Bool(true), - } - - entitlements, err := client.Authorization.GetEntitlements( - context.Background(), - entitlementReq, - ) - if err != nil { - log.Fatal(err) - } - - log.Printf("Scoped entitlements: %v", entitlements.GetEntitlements()) -} +client.AuthorizationV2.GetDecision(ctx, &authorizationv2.GetDecisionRequest{...}) ``` ```java -public void getEntitlementsWithScope(SDK sdk) throws ExecutionException, InterruptedException { - GetEntitlementsRequest request = GetEntitlementsRequest.newBuilder() - .setEntityIdentifier( - EntityIdentifier.newBuilder() - .setEntityChain( - EntityChain.newBuilder() - .addEntities( - Entity.newBuilder() - .setId("user-123") - .setEmailAddress("user@company.com") - ) - ) - ) - // When true, returns all entitled values for attributes with hierarchy rules - .setWithComprehensiveHierarchy(true) - .build(); - - GetEntitlementsResponse resp = sdk.getServices() - .authorization() - .getEntitlements(request) - .get(); - - System.out.println("Scoped entitlements: " + resp.getEntitlementsList()); -} +sdk.getServices().authorization().getDecision(req).get() ``` ```typescript -async function getEntitlementsWithScope(platformClient: PlatformClient) { - const response = await platformClient.v2.authorization.getEntitlements({ - entityIdentifier: { - identifier: { - case: 'entityChain', - value: { - entities: [ - { - ephemeralId: 'user-123', - entityType: { - case: 'emailAddress', - value: 'user@company.com', - }, - }, - ], - }, - }, - }, - // When true, returns all entitled values for attributes with hierarchy rules - withComprehensiveHierarchy: true, - }); - - console.log('Scoped entitlements:', response.entitlements); -} +await platformClient.v2.authorization.getDecision({ ... }) ``` -## Making Authorization Decisions +**Parameters** -Use `GetDecision` when you need to authorize access to specific resources. This is the enforcement point in your application. +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `entityIdentifier` | `EntityIdentifier` | Yes | The entity requesting access. In Go, use helpers like `authorizationv2.ForEmail(...)` or `authorizationv2.ForToken(jwt)`. | +| `action` | `Action` | Yes | The action being performed (e.g., `decrypt`, `read`). | +| `resource` | `Resource` | Yes | The resource being accessed, identified by attribute value FQNs. | -### Single Resource Decision +**Example** -#### V2 API (Recommended) - ```go -func getDecisionV2(client *sdk.SDK) { - decisionReq := &authorizationv2.GetDecisionRequest{ - EntityIdentifier: authorizationv2.ForEmail("user@company.com"), - Action: &policy.Action{ - Name: "decrypt", - }, - Resource: &authorizationv2.Resource{ - Resource: &authorizationv2.Resource_AttributeValues_{ - AttributeValues: &authorizationv2.Resource_AttributeValues{ - Fqns: []string{ - "https://company.com/attr/clearance/value/confidential", - "https://company.com/attr/department/value/finance", - }, +import ( + authorizationv2 "github.com/opentdf/platform/protocol/go/authorization/v2" + "github.com/opentdf/platform/protocol/go/policy" +) + +decisionReq := &authorizationv2.GetDecisionRequest{ + EntityIdentifier: authorizationv2.ForEmail("user@company.com"), + Action: &policy.Action{ + Name: "decrypt", + }, + Resource: &authorizationv2.Resource{ + Resource: &authorizationv2.Resource_AttributeValues_{ + AttributeValues: &authorizationv2.Resource_AttributeValues{ + Fqns: []string{ + "https://company.com/attr/clearance/value/confidential", + "https://company.com/attr/department/value/finance", }, }, }, - } + }, +} - decision, err := client.AuthorizationV2.GetDecision( - context.Background(), - decisionReq, - ) - if err != nil { - log.Fatal(err) - } +decision, err := client.AuthorizationV2.GetDecision( + context.Background(), + decisionReq, +) +if err != nil { + log.Fatal(err) +} - resDecision := decision.GetDecision() - if resDecision.GetDecision() == authorizationv2.Decision_DECISION_PERMIT { - fmt.Println("Access granted") - if len(resDecision.GetRequiredObligations()) > 0 { - fmt.Printf("Required obligations: %v\n", resDecision.GetRequiredObligations()) - } - } else { - fmt.Println("Access denied") +resDecision := decision.GetDecision() +if resDecision.GetDecision() == authorizationv2.Decision_DECISION_PERMIT { + fmt.Println("Access granted") + if len(resDecision.GetRequiredObligations()) > 0 { + fmt.Printf("Required obligations: %v\n", resDecision.GetRequiredObligations()) } +} else { + fmt.Println("Access denied") } ``` +Using a JWT token instead of email: + +```go +import ( + authorizationv2 "github.com/opentdf/platform/protocol/go/authorization/v2" + "github.com/opentdf/platform/protocol/go/policy" +) + +decisionReq := &authorizationv2.GetDecisionRequest{ + EntityIdentifier: authorizationv2.ForToken(jwtToken), + Action: &policy.Action{Name: "decrypt"}, + Resource: &authorizationv2.Resource{ + Resource: &authorizationv2.Resource_AttributeValues_{ + AttributeValues: &authorizationv2.Resource_AttributeValues{ + Fqns: []string{"https://company.com/attr/clearance/value/public"}, + }, + }, + }, +} + +decision, err := client.AuthorizationV2.GetDecision( + context.Background(), + decisionReq, +) +if err != nil { + log.Fatal(err) +} + +fmt.Printf("Token-based decision: %v\n", decision.GetDecision().GetDecision()) +``` +
V1 API (Legacy) ```go -func getDecisionV1(client *sdk.SDK) { - // V1 API uses bulk decisions - decisionRequests := []*authorization.DecisionRequest{{ - Actions: []*policy.Action{{ - Name: "decrypt", - }}, - EntityChains: []*authorization.EntityChain{{ - Id: "ec1", - Entities: []*authorization.Entity{{ - EntityType: &authorization.Entity_EmailAddress{ - EmailAddress: "user@company.com", - }, - Category: authorization.Entity_CATEGORY_SUBJECT, - }}, - }}, - ResourceAttributes: []*authorization.ResourceAttribute{{ - AttributeValueFqns: []string{ - "https://company.com/attr/clearance/value/confidential", - "https://company.com/attr/department/value/finance", +import ( + "github.com/opentdf/platform/protocol/go/authorization" + "github.com/opentdf/platform/protocol/go/policy" +) + +decisionRequests := []*authorization.DecisionRequest{{ + Actions: []*policy.Action{{Name: "decrypt"}}, + EntityChains: []*authorization.EntityChain{{ + Id: "ec1", + Entities: []*authorization.Entity{{ + EntityType: &authorization.Entity_EmailAddress{ + EmailAddress: "user@company.com", }, + Category: authorization.Entity_CATEGORY_SUBJECT, }}, - }} - - decisionRequest := &authorization.GetDecisionsRequest{ - DecisionRequests: decisionRequests, - } + }}, + ResourceAttributes: []*authorization.ResourceAttribute{{ + AttributeValueFqns: []string{ + "https://company.com/attr/clearance/value/confidential", + "https://company.com/attr/department/value/finance", + }, + }}, +}} - decisionResponse, err := client.Authorization.GetDecisions( - context.Background(), - decisionRequest, - ) - if err != nil { - log.Fatal(err) - } +decisionResponse, err := client.Authorization.GetDecisions( + context.Background(), + &authorization.GetDecisionsRequest{DecisionRequests: decisionRequests}, +) +if err != nil { + log.Fatal(err) +} - for _, dr := range decisionResponse.GetDecisionResponses() { - if dr.GetDecision() == authorization.DecisionResponse_DECISION_PERMIT { - fmt.Println("Access granted") - // Process any obligations - if len(dr.GetObligations()) > 0 { - fmt.Printf("Obligations to fulfill: %v\n", dr.GetObligations()) - } - } else { - fmt.Println("Access denied") +for _, dr := range decisionResponse.GetDecisionResponses() { + if dr.GetDecision() == authorization.DecisionResponse_DECISION_PERMIT { + fmt.Println("Access granted") + if len(dr.GetObligations()) > 0 { + fmt.Printf("Obligations to fulfill: %v\n", dr.GetObligations()) } + } else { + fmt.Println("Access denied") } } ``` @@ -492,48 +528,45 @@ func getDecisionV1(client *sdk.SDK) { ```java -public void getDecision(SDK sdk) throws ExecutionException, InterruptedException { - GetDecisionRequest request = GetDecisionRequest.newBuilder() - .setEntityIdentifier( - EntityIdentifier.newBuilder() - .setEntityChain( - EntityChain.newBuilder() - .addEntities( - Entity.newBuilder() - .setId("user-123") - .setEmailAddress("user@company.com") - ) - ) - ) - .setAction( - Action.newBuilder() - .setName("decrypt") - ) - .setResource( - Resource.newBuilder() - .setAttributeValues( - Resource.AttributeValues.newBuilder() - .addFqns("https://company.com/attr/clearance/value/confidential") - .addFqns("https://company.com/attr/department/value/finance") - ) - ) - .build(); - - GetDecisionResponse resp = sdk.getServices() - .authorization() - .getDecision(request) - .get(); - - Decision decision = resp.getDecision(); - if (decision.getDecision() == Decision.DECISION_PERMIT) { - System.out.println("Access granted"); - // Process any obligations - if (decision.getObligationsCount() > 0) { - System.out.println("Obligations to fulfill: " + decision.getObligationsList()); - } - } else { - System.out.println("Access denied"); +GetDecisionRequest request = GetDecisionRequest.newBuilder() + .setEntityIdentifier( + EntityIdentifier.newBuilder() + .setEntityChain( + EntityChain.newBuilder() + .addEntities( + Entity.newBuilder() + .setId("user-123") + .setEmailAddress("user@company.com") + ) + ) + ) + .setAction( + Action.newBuilder() + .setName("decrypt") + ) + .setResource( + Resource.newBuilder() + .setAttributeValues( + Resource.AttributeValues.newBuilder() + .addFqns("https://company.com/attr/clearance/value/confidential") + .addFqns("https://company.com/attr/department/value/finance") + ) + ) + .build(); + +GetDecisionResponse resp = sdk.getServices() + .authorization() + .getDecision(request) + .get(); + +Decision decision = resp.getDecision(); +if (decision.getDecision() == Decision.DECISION_PERMIT) { + System.out.println("Access granted"); + if (decision.getObligationsCount() > 0) { + System.out.println("Obligations to fulfill: " + decision.getObligationsList()); } +} else { + System.out.println("Access denied"); } ``` @@ -543,111 +576,153 @@ public void getDecision(SDK sdk) throws ExecutionException, InterruptedException ```typescript import { Decision } from '@opentdf/sdk/platform/authorization/v2/authorization_pb.js'; -async function getDecision(platformClient: PlatformClient) { - const response = await platformClient.v2.authorization.getDecision({ - entityIdentifier: { - identifier: { - case: 'entityChain', - value: { - entities: [ - { - ephemeralId: 'user-123', - entityType: { - case: 'emailAddress', - value: 'user@company.com', - }, - }, - ], - }, +const response = await platformClient.v2.authorization.getDecision({ + entityIdentifier: { + identifier: { + case: 'entityChain', + value: { + entities: [ + { + ephemeralId: 'user-123', + entityType: { case: 'emailAddress', value: 'user@company.com' }, + }, + ], }, }, - action: { - name: 'decrypt', - }, + }, + action: { name: 'decrypt' }, + resource: { resource: { - resource: { - case: 'attributeValues', - value: { - fqns: [ - 'https://company.com/attr/clearance/value/confidential', - 'https://company.com/attr/department/value/finance', - ], - }, + case: 'attributeValues', + value: { + fqns: [ + 'https://company.com/attr/clearance/value/confidential', + 'https://company.com/attr/department/value/finance', + ], }, }, - }); + }, +}); - const decision = response.decision; - if (decision?.decision === Decision.PERMIT) { - console.log('Access granted'); - if (decision.requiredObligations.length > 0) { - console.log('Required obligations:', decision.requiredObligations); - } - } else { - console.log('Access denied'); +const decision = response.decision; +if (decision?.decision === Decision.PERMIT) { + console.log('Access granted'); + if (decision.requiredObligations.length > 0) { + console.log('Required obligations:', decision.requiredObligations); } +} else { + console.log('Access denied'); } ``` -### Bulk Authorization Decisions +**Returns** -For efficient batch processing, use bulk decision endpoints: +A [ResourceDecision](#resourcedecision) with a [Decision](#decision) (permit/deny) and any required [obligations](/sdks/obligations) the consuming application must enforce. + +--- + +## GetDecisionBulk + +Evaluates multiple entity + action + resource combinations in a single call. Each request can include multiple resources, and the response indicates whether all resources were permitted plus per-resource decisions. + +**Signature** -#### V2 API (Recommended) +```go +client.AuthorizationV2.GetDecisionBulk(ctx, &authorizationv2.GetDecisionBulkRequest{...}) +``` + + + + +```java +sdk.getServices().authorization().getDecisionBulk(req).get() +``` + + + + +```typescript +await platformClient.v2.authorization.getDecisionBulk({ ... }) +``` + + + + +**Parameters** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `decisionRequests` | `[]GetDecisionMultiResourceRequest` | Yes | Each entry contains an entity, action, and one or more resources to evaluate. | + +Each `GetDecisionMultiResourceRequest` contains: + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `entityIdentifier` | `EntityIdentifier` | Yes | The entity requesting access. | +| `action` | `Action` | Yes | The action being performed. | +| `resources` | `[]Resource` | Yes | Resources to evaluate, each with an `ephemeralId` for correlation. | + +**Example** + + + ```go -func getBulkDecisionsV2(client *sdk.SDK) { - bulkReq := &authorizationv2.GetDecisionBulkRequest{ - DecisionRequests: []*authorizationv2.GetDecisionMultiResourceRequest{ - { - EntityIdentifier: authorizationv2.ForEmail("user@company.com"), - Action: &policy.Action{Name: "decrypt"}, - Resources: []*authorizationv2.Resource{ - { - EphemeralId: "resource-1", - Resource: &authorizationv2.Resource_AttributeValues_{ - AttributeValues: &authorizationv2.Resource_AttributeValues{ - Fqns: []string{"https://company.com/attr/class/value/public"}, - }, +import ( + authorizationv2 "github.com/opentdf/platform/protocol/go/authorization/v2" + "github.com/opentdf/platform/protocol/go/policy" +) + +bulkReq := &authorizationv2.GetDecisionBulkRequest{ + DecisionRequests: []*authorizationv2.GetDecisionMultiResourceRequest{ + { + EntityIdentifier: authorizationv2.ForEmail("user@company.com"), + Action: &policy.Action{Name: "decrypt"}, + Resources: []*authorizationv2.Resource{ + { + EphemeralId: "resource-1", + Resource: &authorizationv2.Resource_AttributeValues_{ + AttributeValues: &authorizationv2.Resource_AttributeValues{ + Fqns: []string{"https://company.com/attr/class/value/public"}, }, }, - { - EphemeralId: "resource-2", - Resource: &authorizationv2.Resource_AttributeValues_{ - AttributeValues: &authorizationv2.Resource_AttributeValues{ - Fqns: []string{"https://company.com/attr/class/value/confidential"}, - }, + }, + { + EphemeralId: "resource-2", + Resource: &authorizationv2.Resource_AttributeValues_{ + AttributeValues: &authorizationv2.Resource_AttributeValues{ + Fqns: []string{"https://company.com/attr/class/value/confidential"}, }, }, }, }, }, + }, +} + +decisions, err := client.AuthorizationV2.GetDecisionBulk( + context.Background(), + bulkReq, +) +if err != nil { + log.Fatal(err) +} + +for _, resp := range decisions.GetDecisionResponses() { + allPermitted := resp.GetAllPermitted() + if allPermitted != nil { + fmt.Printf("All resources permitted: %v\n", allPermitted.GetValue()) } - - decisions, err := client.AuthorizationV2.GetDecisionBulk( - context.Background(), - bulkReq, - ) - if err != nil { - log.Fatal(err) - } - - for _, resp := range decisions.GetDecisionResponses() { - allPermitted := resp.GetAllPermitted() - if allPermitted != nil { - fmt.Printf("All resources permitted: %v\n", allPermitted.GetValue()) - } - for _, resourceDecision := range resp.GetResourceDecisions() { - fmt.Printf("Resource %s: %v\n", - resourceDecision.GetEphemeralResourceId(), - resourceDecision.GetDecision()) - } + for _, resourceDecision := range resp.GetResourceDecisions() { + fmt.Printf("Resource %s: %v\n", + resourceDecision.GetEphemeralResourceId(), + resourceDecision.GetDecision()) } } ``` @@ -656,49 +731,38 @@ func getBulkDecisionsV2(client *sdk.SDK) { V1 API (Legacy) ```go -func getBulkDecisionsV1(client *sdk.SDK) { - // V1 API uses GetDecisions for bulk processing - decisionRequests := []*authorization.DecisionRequest{{ - Actions: []*policy.Action{{Name: "decrypt"}}, - EntityChains: []*authorization.EntityChain{{ - Id: "ec1", - Entities: []*authorization.Entity{{ - EntityType: &authorization.Entity_EmailAddress{ - EmailAddress: "user@company.com", - }, - Category: authorization.Entity_CATEGORY_SUBJECT, - }}, - }}, - ResourceAttributes: []*authorization.ResourceAttribute{ - { - AttributeValueFqns: []string{"https://company.com/attr/class/value/public"}, - }, - { - AttributeValueFqns: []string{"https://company.com/attr/class/value/confidential"}, - }, - }, - }} +import ( + "github.com/opentdf/platform/protocol/go/authorization" + "github.com/opentdf/platform/protocol/go/policy" +) - decisionRequest := &authorization.GetDecisionsRequest{ - DecisionRequests: decisionRequests, - } +decisionRequests := []*authorization.DecisionRequest{{ + Actions: []*policy.Action{{Name: "decrypt"}}, + EntityChains: []*authorization.EntityChain{{ + Id: "ec1", + Entities: []*authorization.Entity{{ + EntityType: &authorization.Entity_EmailAddress{ + EmailAddress: "user@company.com", + }, + Category: authorization.Entity_CATEGORY_SUBJECT, + }}, + }}, + ResourceAttributes: []*authorization.ResourceAttribute{ + {AttributeValueFqns: []string{"https://company.com/attr/class/value/public"}}, + {AttributeValueFqns: []string{"https://company.com/attr/class/value/confidential"}}, + }, +}} - decisionResponse, err := client.Authorization.GetDecisions( - context.Background(), - decisionRequest, - ) - if err != nil { - log.Fatal(err) - } +decisionResponse, err := client.Authorization.GetDecisions( + context.Background(), + &authorization.GetDecisionsRequest{DecisionRequests: decisionRequests}, +) +if err != nil { + log.Fatal(err) +} - for _, dr := range decisionResponse.GetDecisionResponses() { - fmt.Printf("Entity chain %s: %v\n", - dr.GetEntityChainId(), - dr.GetDecision()) - if len(dr.GetObligations()) > 0 { - fmt.Printf("Obligations: %v\n", dr.GetObligations()) - } - } +for _, dr := range decisionResponse.GetDecisionResponses() { + fmt.Printf("Entity chain %s: %v\n", dr.GetEntityChainId(), dr.GetDecision()) } ``` @@ -707,60 +771,53 @@ func getBulkDecisionsV1(client *sdk.SDK) { -#### V2 API (Recommended) - ```java -public void getBulkDecisions(SDK sdk) throws ExecutionException, InterruptedException { - GetDecisionBulkRequest request = GetDecisionBulkRequest.newBuilder() - .addDecisionRequests( - GetDecisionMultiResourceRequest.newBuilder() - .setEntityIdentifier( - EntityIdentifier.newBuilder() - .setEntityChain( - EntityChain.newBuilder() - .addEntities( - Entity.newBuilder() - .setId("user-123") - .setEmailAddress("user@company.com") - ) - ) - ) - .setAction( - Action.newBuilder() - .setName("decrypt") - ) - .addResources( - Resource.newBuilder() - .setEphemeralId("resource-1") - .setAttributeValues( - Resource.AttributeValues.newBuilder() - .addFqns("https://company.com/attr/class/value/public") - ) - ) - .addResources( - Resource.newBuilder() - .setEphemeralId("resource-2") - .setAttributeValues( - Resource.AttributeValues.newBuilder() - .addFqns("https://company.com/attr/class/value/confidential") - ) - ) - ) - .build(); - - GetDecisionBulkResponse resp = sdk.getServices() - .authorization() - .getDecisionBulk(request) - .get(); - - for (GetDecisionMultiResourceResponse response : resp.getDecisionResponsesList()) { - if (response.hasAllPermitted()) { - System.out.println("All resources permitted: " + response.getAllPermitted().getValue()); - } - for (ResourceDecision resourceDecision : response.getResourceDecisionsList()) { - System.out.println("Resource " + resourceDecision.getEphemeralResourceId() + - ": " + resourceDecision.getDecision()); - } +GetDecisionBulkRequest request = GetDecisionBulkRequest.newBuilder() + .addDecisionRequests( + GetDecisionMultiResourceRequest.newBuilder() + .setEntityIdentifier( + EntityIdentifier.newBuilder() + .setEntityChain( + EntityChain.newBuilder() + .addEntities( + Entity.newBuilder() + .setId("user-123") + .setEmailAddress("user@company.com") + ) + ) + ) + .setAction(Action.newBuilder().setName("decrypt")) + .addResources( + Resource.newBuilder() + .setEphemeralId("resource-1") + .setAttributeValues( + Resource.AttributeValues.newBuilder() + .addFqns("https://company.com/attr/class/value/public") + ) + ) + .addResources( + Resource.newBuilder() + .setEphemeralId("resource-2") + .setAttributeValues( + Resource.AttributeValues.newBuilder() + .addFqns("https://company.com/attr/class/value/confidential") + ) + ) + ) + .build(); + +GetDecisionBulkResponse resp = sdk.getServices() + .authorization() + .getDecisionBulk(request) + .get(); + +for (GetDecisionMultiResourceResponse response : resp.getDecisionResponsesList()) { + if (response.hasAllPermitted()) { + System.out.println("All resources permitted: " + response.getAllPermitted().getValue()); + } + for (ResourceDecision resourceDecision : response.getResourceDecisionsList()) { + System.out.println("Resource " + resourceDecision.getEphemeralResourceId() + + ": " + resourceDecision.getDecision()); } } ``` @@ -778,62 +835,51 @@ import GetDecisionsExample from '@site/code_samples/java/get-decisions.mdx'; ```typescript -async function getBulkDecisions(platformClient: PlatformClient) { - const response = await platformClient.v2.authorization.getDecisionBulk({ - decisionRequests: [ - { - entityIdentifier: { - identifier: { - case: 'entityChain', - value: { - entities: [ - { - ephemeralId: 'user-123', - entityType: { - case: 'emailAddress', - value: 'user@company.com', - }, - }, - ], - }, +const response = await platformClient.v2.authorization.getDecisionBulk({ + decisionRequests: [ + { + entityIdentifier: { + identifier: { + case: 'entityChain', + value: { + entities: [ + { + ephemeralId: 'user-123', + entityType: { case: 'emailAddress', value: 'user@company.com' }, + }, + ], }, }, - action: { - name: 'decrypt', - }, - resources: [ - { - ephemeralId: 'resource-1', - resource: { - case: 'attributeValues', - value: { - fqns: ['https://company.com/attr/class/value/public'], - }, - }, + }, + action: { name: 'decrypt' }, + resources: [ + { + ephemeralId: 'resource-1', + resource: { + case: 'attributeValues', + value: { fqns: ['https://company.com/attr/class/value/public'] }, }, - { - ephemeralId: 'resource-2', - resource: { - case: 'attributeValues', - value: { - fqns: ['https://company.com/attr/class/value/confidential'], - }, - }, + }, + { + ephemeralId: 'resource-2', + resource: { + case: 'attributeValues', + value: { fqns: ['https://company.com/attr/class/value/confidential'] }, }, - ], - }, - ], - }); + }, + ], + }, + ], +}); - for (const resp of response.decisionResponses) { - if (resp.allPermitted !== undefined) { - console.log('All resources permitted:', resp.allPermitted.value); - } - for (const resourceDecision of resp.resourceDecisions) { - console.log( - `Resource ${resourceDecision.ephemeralResourceId}: ${resourceDecision.decision}` - ); - } +for (const resp of response.decisionResponses) { + if (resp.allPermitted !== undefined) { + console.log('All resources permitted:', resp.allPermitted.value); + } + for (const resourceDecision of resp.resourceDecisions) { + console.log( + `Resource ${resourceDecision.ephemeralResourceId}: ${resourceDecision.decision}` + ); } } ``` @@ -841,272 +887,166 @@ async function getBulkDecisions(platformClient: PlatformClient) { -## Entity Types and Authentication +**Returns** -OpenTDF supports various entity types for flexible authentication: +A list of [GetDecisionMultiResourceResponse](#getdecisionmultiresourceresponse) objects, each containing an `allPermitted` flag and per-resource [ResourceDecision](#resourcedecision) entries. -### Supported Entity Types +--- -- **ClientId**: Service-to-service authentication -- **EmailAddress**: User identification via email -- **UserName**: User identification via username -- **UUID**: Direct entity UUID reference -- **Token**: JWT-based authentication -- **Claims**: Custom claims-based entities +## Best Practices -### Entity Identifier Helpers (Go) +### Performance -The Go SDK provides convenience constructors in the `authorization/v2` package that eliminate the deeply nested proto construction required to build an `EntityIdentifier`. +- **Batch operations**: Use `GetDecisionBulk` for multiple authorization checks instead of calling `GetDecision` in a loop +- **Cache entitlements**: Cache `GetEntitlements` results when appropriate (consider TTL) +- **Scope queries**: Use `withComprehensiveHierarchy` only when you need expanded hierarchy values -| Helper | Description | -|--------|-------------| -| `authorizationv2.ForClientID(clientID string)` | Identifies a subject entity by client ID | -| `authorizationv2.ForEmail(email string)` | Identifies a subject entity by email address | -| `authorizationv2.ForUserName(username string)` | Identifies a subject entity by username | -| `authorizationv2.ForToken(jwt string)` | Resolves the entity from the given JWT | -| `authorizationv2.WithRequestToken()` | Derives the entity from the request's Authorization header | +### Security + +- **Least privilege**: Request only the minimum necessary permissions +- **Token validation**: Ensure JWT tokens are properly validated before passing to `ForToken` +- **Obligation handling**: Always process and fulfill returned [obligations](/sdks/obligations) +- **Deny by default**: If an authorization call fails, deny access rather than allowing it + +### Integration Pattern ```go -import authorization "github.com/opentdf/platform/protocol/go/authorization/v2" +import authorizationv2 "github.com/opentdf/platform/protocol/go/authorization/v2" -req := &authorization.GetDecisionRequest{ - EntityIdentifier: authorization.ForClientID("opentdf"), - // ... +// Example: Authorization middleware +func authorizationMiddleware(next http.Handler, client *sdk.SDK) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + decision, err := client.AuthorizationV2.GetDecision( + r.Context(), + &authorizationv2.GetDecisionRequest{ + EntityIdentifier: authorizationv2.WithRequestToken(), + Action: &policy.Action{Name: "access"}, + Resource: resourceFromPath(r.URL.Path), + }, + ) + if err != nil || decision.GetDecision().GetDecision() != authorizationv2.Decision_DECISION_PERMIT { + http.Error(w, "Access denied", http.StatusForbidden) + return + } + next.ServeHTTP(w, r) + }) } ``` -All five constructors work with every v2 authorization method: `GetDecision`, `GetDecisionMultiResource`, `GetDecisionBulk`, and `GetEntitlements`. - -### Token-Based Authentication Example +## Error Handling -#### V2 API (Recommended) - ```go -func getDecisionWithTokenV2(client *sdk.SDK, jwtToken string) { - decisionReq := &authorizationv2.GetDecisionRequest{ - EntityIdentifier: authorizationv2.ForToken(jwtToken), - Action: &policy.Action{Name: "decrypt"}, - Resource: &authorizationv2.Resource{ - Resource: &authorizationv2.Resource_AttributeValues_{ - AttributeValues: &authorizationv2.Resource_AttributeValues{ - Fqns: []string{"https://company.com/attr/clearance/value/public"}, - }, - }, - }, - } - - decision, err := client.AuthorizationV2.GetDecision( - context.Background(), - decisionReq, - ) - if err != nil { - log.Fatal(err) - } - - resDecision := decision.GetDecision() - fmt.Printf("Token-based decision: %v\n", resDecision.GetDecision()) -} -``` - -
-V1 API (Legacy) - -```go -func getDecisionWithTokenV1(client *sdk.SDK, jwtToken string) { - // V1 API uses bulk decisions with token entity - decisionRequests := []*authorization.DecisionRequest{{ - Actions: []*policy.Action{{Name: "decrypt"}}, - EntityChains: []*authorization.EntityChain{{ - Id: "token-chain", - Entities: []*authorization.Entity{{ - EntityType: &authorization.Entity_Token{ - Token: &entity.Token{ - EphemeralId: "token-1", - Jwt: jwtToken, - }, - }, - Category: authorization.Entity_CATEGORY_SUBJECT, - }}, - }}, - ResourceAttributes: []*authorization.ResourceAttribute{{ - AttributeValueFqns: []string{"https://company.com/attr/clearance/value/public"}, - }}, - }} +import ( + authorizationv2 "github.com/opentdf/platform/protocol/go/authorization/v2" + "github.com/opentdf/platform/protocol/go/policy" +) - decisionRequest := &authorization.GetDecisionsRequest{ - DecisionRequests: decisionRequests, - } +decision, err := client.AuthorizationV2.GetDecision(context.Background(), req) - decisionResponse, err := client.Authorization.GetDecisions( - context.Background(), - decisionRequest, - ) - if err != nil { - log.Fatal(err) - } +if err != nil { + // Log the error for debugging + log.Printf("Authorization error: %v", err) - for _, dr := range decisionResponse.GetDecisionResponses() { - fmt.Printf("Token-based decision: %v\n", dr.GetDecision()) - if len(dr.GetObligations()) > 0 { - fmt.Printf("Obligations: %v\n", dr.GetObligations()) - } - } + // Deny by default (more secure) + handleAccessDenied() + return } -``` -
+// Process successful response +handleDecisionResponse(decision) +```
- +
-```java -public void getDecisionWithToken(SDK sdk, String jwtToken) throws ExecutionException, InterruptedException { - GetDecisionRequest request = GetDecisionRequest.newBuilder() - .setEntityIdentifier( - EntityIdentifier.newBuilder() - .setToken( - Token.newBuilder() - .setId("token-1") - .setJwt(jwtToken) - ) - ) - .setAction( - Action.newBuilder() - .setName("decrypt") - ) - .setResource( - Resource.newBuilder() - .setAttributeValues( - Resource.AttributeValues.newBuilder() - .addFqns("https://company.com/attr/clearance/value/public") - ) - ) - .build(); - - GetDecisionResponse resp = sdk.getServices() - .authorization() - .getDecision(request) - .get(); - - System.out.println("Token-based decision: " + resp.getDecision().getDecision()); -} -``` +--- - - +## Type Reference -```typescript -async function getDecisionWithToken(platformClient: PlatformClient, jwtToken: string) { - const response = await platformClient.v2.authorization.getDecision({ - entityIdentifier: { - identifier: { - case: 'token', - value: { - ephemeralId: 'token-1', - jwt: jwtToken, - }, - }, - }, - action: { - name: 'decrypt', - }, - resource: { - resource: { - case: 'attributeValues', - value: { - fqns: ['https://company.com/attr/clearance/value/public'], - }, - }, - }, - }); +### Decision - console.log('Token-based decision:', response.decision?.decision); -} -``` +The authorization result. - - +| Value | Description | +|-------|-------------| +| `DECISION_PERMIT` | Access is granted. Check `requiredObligations` for any controls the PEP must enforce. | +| `DECISION_DENY` | Access is denied. | -## Best Practices +### ResourceDecision -### Performance Optimization +The result for a single resource in a decision response. -1. **Batch Operations**: Use bulk endpoints for multiple authorization checks -2. **Caching**: Cache entitlement results when appropriate (consider TTL) -3. **Scope Limiting**: Use scoped entitlement queries to reduce response size +| Field | Type | Description | +|-------|------|-------------| +| `ephemeralResourceId` | `string` | Correlates with the `ephemeralId` set on the request's [Resource](#resource). | +| `decision` | [`Decision`](#decision) | Permit or deny. | +| `requiredObligations` | `[]string` | Obligation value FQNs the PEP must fulfill (e.g., `https://example.com/obl/drm/value/watermarking`). Empty if none required. | -### Security Considerations +### Resource -1. **Least Privilege**: Request only the minimum necessary permissions -2. **Token Validation**: Ensure JWT tokens are properly validated before use -3. **Obligation Handling**: Always process and fulfill returned [obligations](/sdks/policy#obligations) -4. **Error Handling**: Implement proper error handling and fallback policies +Identifies the data being accessed, by its attribute value FQNs (the same FQNs embedded in a TDF policy). -### Integration Patterns +| Field | Type | Description | +|-------|------|-------------| +| `ephemeralId` | `string` | An ID you assign for correlating with the response. Used in bulk requests. | +| `attributeValues.fqns` | `[]string` | Attribute value FQNs on the resource (1–20). | ```go -// Example: Authorization middleware -func authorizationMiddleware(next http.Handler, sdk *sdk.SDK) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Extract entity from request (JWT, session, etc.) - entity := extractEntityFromRequest(r) - - // Extract resource attributes from the requested resource - resourceAttrs := extractResourceAttributes(r.URL.Path) - - // Make authorization decision - decision := makeAuthorizationDecision(sdk, entity, "access", resourceAttrs) - - if decision == authorization.DecisionResponse_DECISION_PERMIT { - next.ServeHTTP(w, r) - } else { - http.Error(w, "Access denied", http.StatusForbidden) - } - }) +// Go +&authorizationv2.Resource{ + EphemeralId: "resource-1", + Resource: &authorizationv2.Resource_AttributeValues_{ + AttributeValues: &authorizationv2.Resource_AttributeValues{ + Fqns: []string{"https://example.com/attr/classification/value/secret"}, + }, + }, } ``` -## Error Handling +```typescript +// JavaScript +{ + ephemeralId: 'resource-1', + resource: { + case: 'attributeValues', + value: { fqns: ['https://example.com/attr/classification/value/secret'] }, + }, +} +``` -Always implement comprehensive error handling for authorization calls: +### EntityEntitlements - - +Returned by [GetEntitlements](#getentitlements). One per entity, mapping attribute value FQNs to the actions that entity can perform. + +| Field | Type | Description | +|-------|------|-------------| +| `ephemeralId` | `string` | Correlates with the entity in the request. | +| `actionsPerAttributeValueFqn` | `map` | Keys are attribute value FQNs. Values are lists of [actions](/sdks/policy#action) the entity can perform on data carrying that attribute value. | ```go -func safeAuthorizationCall(client *sdk.SDK, req *authorizationv2.GetDecisionRequest) { - decision, err := client.AuthorizationV2.GetDecision(context.Background(), req) - - if err != nil { - // Log the error for debugging - log.Printf("Authorization error: %v", err) - - // Implement your fallback policy. Choose one of the options below. - - // Option 1: Deny by default (more secure) - handleAccessDenied() - return - - /* - // Option 2: Allow by default (less secure, only for non-critical resources) - handleAccessAllowed() - return - */ - - /* - // Option 3: Retry with exponential backoff - retryWithBackoff(client, req) - return - */ +for _, e := range resp.GetEntitlements() { + for fqn, actions := range e.GetActionsPerAttributeValueFqn() { + fmt.Printf("FQN: %s → actions: %v\n", fqn, actions.GetActions()) } - - // Process successful response - handleDecisionResponse(decision) } ``` - - +```typescript +for (const e of resp.entitlements) { + for (const [fqn, actions] of Object.entries(e.actionsPerAttributeValueFqn)) { + console.log(`FQN: ${fqn} → actions:`, actions.actions); + } +} +``` + +### GetDecisionMultiResourceResponse + +Returned by [GetDecisionBulk](#getdecisionbulk). One per entity+action combination in the request. + +| Field | Type | Description | +|-------|------|-------------| +| `allPermitted` | `bool` | Convenience flag — `true` if every resource in this group was permitted. | +| `resourceDecisions` | [`[]ResourceDecision`](#resourcedecision) | Per-resource results with individual decisions and obligations. | From 0c901056b5b309a45c0d9c60f4e0f40f7564083e Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Tue, 7 Apr 2026 10:19:20 -0700 Subject: [PATCH 2/8] fix: remove UUID entity type (v2 only), fix %v on pointer slice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove UUID from entity types table — not available in v2 API - Replace log.Printf with iteration in scoped entitlements example Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/sdks/authorization.mdx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/sdks/authorization.mdx b/docs/sdks/authorization.mdx index 971e2003..b791b53a 100644 --- a/docs/sdks/authorization.mdx +++ b/docs/sdks/authorization.mdx @@ -134,7 +134,6 @@ EntityIdentifier.newBuilder() | Client ID | `ForClientID(id)` | `.setClientId(id)` | `'clientId'` | | Username | `ForUserName(name)` | `.setUserName(name)` | `'userName'` | | JWT Token | `ForToken(jwt)` | `.setToken(Token.newBuilder().setJwt(jwt))` | `'token'` | -| UUID | — | `.setUuid(uuid)` | `'uuid'` | --- @@ -222,7 +221,9 @@ if err != nil { log.Fatal(err) } -log.Printf("Scoped entitlements: %v", entitlements.GetEntitlements()) +for _, e := range entitlements.GetEntitlements() { + fmt.Printf("Entitled to: %v\n", e.GetActionsPerAttributeValueFqn()) +} ```
From 5aa834c93daf6ddaf0038cdae3f525182a091165 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Tue, 7 Apr 2026 10:23:59 -0700 Subject: [PATCH 3/8] fix: add Claims entity type back to EntityIdentifier table Claims was listed in the original docs but dropped during restructure. It's a valid v2 entity type used by the Entity Resolution Service. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/sdks/authorization.mdx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/sdks/authorization.mdx b/docs/sdks/authorization.mdx index b791b53a..11bebc47 100644 --- a/docs/sdks/authorization.mdx +++ b/docs/sdks/authorization.mdx @@ -134,6 +134,9 @@ EntityIdentifier.newBuilder() | Client ID | `ForClientID(id)` | `.setClientId(id)` | `'clientId'` | | Username | `ForUserName(name)` | `.setUserName(name)` | `'userName'` | | JWT Token | `ForToken(jwt)` | `.setToken(Token.newBuilder().setJwt(jwt))` | `'token'` | +| Claims | — | `.setClaims(claims)` | `'claims'` | + +Claims are used by the Entity Resolution Service (ERS) for custom claim-based entity resolution. --- From 7057828c85503aaf668f500ab8e4c2087f7c0524 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Tue, 7 Apr 2026 10:30:27 -0700 Subject: [PATCH 4/8] chore(docs): add Typical Workflow section to Authorization page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restore the workflow guidance from main (resource discovery → resource access → bulk operations) as a section near the bottom, matching the pattern used on the Discovery page. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/sdks/authorization.mdx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/sdks/authorization.mdx b/docs/sdks/authorization.mdx index 11bebc47..8d74d5bb 100644 --- a/docs/sdks/authorization.mdx +++ b/docs/sdks/authorization.mdx @@ -968,6 +968,14 @@ handleDecisionResponse(decision) --- +## Typical Workflow + +1. **During resource discovery**: Use [GetEntitlements](#getentitlements) to show users what data they can access — build UIs, pre-filter content, or understand an entity's overall access scope +2. **During resource access**: Use [GetDecision](#getdecision) to enforce access controls when an entity attempts to access a specific resource +3. **For bulk operations**: Use [GetDecisionBulk](#getdecisionbulk) for efficient batch authorization when checking multiple resources at once + +--- + ## Type Reference ### Decision From 45ad5b567c7173ba486187823e7c04fa2c8578c5 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Tue, 7 Apr 2026 10:33:29 -0700 Subject: [PATCH 5/8] fix: add registeredResourceValueFqn to EntityIdentifier and Resource types Available as both an entity identifier (resource acts as entity for authorization) and a resource variant (alternative to attribute value FQNs). Links to registered resources concept page. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/sdks/authorization.mdx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/sdks/authorization.mdx b/docs/sdks/authorization.mdx index 8d74d5bb..395eddf7 100644 --- a/docs/sdks/authorization.mdx +++ b/docs/sdks/authorization.mdx @@ -135,8 +135,10 @@ EntityIdentifier.newBuilder() | Username | `ForUserName(name)` | `.setUserName(name)` | `'userName'` | | JWT Token | `ForToken(jwt)` | `.setToken(Token.newBuilder().setJwt(jwt))` | `'token'` | | Claims | — | `.setClaims(claims)` | `'claims'` | +| Registered Resource | — | `.setRegisteredResourceValueFqn(fqn)` | `'registeredResourceValueFqn'` | -Claims are used by the Entity Resolution Service (ERS) for custom claim-based entity resolution. +- **Claims** are used by the Entity Resolution Service (ERS) for custom claim-based entity resolution. +- **Registered Resource** identifies an entity by a [registered resource](/components/policy/registered_resources) value FQN stored in platform policy, where the resource acts as a single entity for authorization decisions. --- @@ -999,12 +1001,13 @@ The result for a single resource in a decision response. ### Resource -Identifies the data being accessed, by its attribute value FQNs (the same FQNs embedded in a TDF policy). +Identifies the data being accessed. A resource can be specified in two ways: | Field | Type | Description | |-------|------|-------------| | `ephemeralId` | `string` | An ID you assign for correlating with the response. Used in bulk requests. | -| `attributeValues.fqns` | `[]string` | Attribute value FQNs on the resource (1–20). | +| `attributeValues.fqns` | `[]string` | Attribute value FQNs on the resource (1–20). Use this for TDF payloads or any resource identified by attribute values. | +| `registeredResourceValueFqn` | `string` (URI) | A [registered resource](/components/policy/registered_resources) value FQN stored in platform policy. Alternative to `attributeValues`. | ```go // Go From 2173137d55c046c27e8d3979dff5f7b0aa8524b0 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Tue, 7 Apr 2026 10:39:34 -0700 Subject: [PATCH 6/8] fix: link obligations to concept page instead of SDK page The /sdks/obligations page is created in a separate PR. Link to the existing concept page to avoid broken link in CI. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/sdks/authorization.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sdks/authorization.mdx b/docs/sdks/authorization.mdx index 395eddf7..8aaeaa9c 100644 --- a/docs/sdks/authorization.mdx +++ b/docs/sdks/authorization.mdx @@ -626,7 +626,7 @@ if (decision?.decision === Decision.PERMIT) { **Returns** -A [ResourceDecision](#resourcedecision) with a [Decision](#decision) (permit/deny) and any required [obligations](/sdks/obligations) the consuming application must enforce. +A [ResourceDecision](#resourcedecision) with a [Decision](#decision) (permit/deny) and any required [obligations](/components/policy/obligations) the consuming application must enforce. --- @@ -911,7 +911,7 @@ A list of [GetDecisionMultiResourceResponse](#getdecisionmultiresourceresponse) - **Least privilege**: Request only the minimum necessary permissions - **Token validation**: Ensure JWT tokens are properly validated before passing to `ForToken` -- **Obligation handling**: Always process and fulfill returned [obligations](/sdks/obligations) +- **Obligation handling**: Always process and fulfill returned [obligations](/components/policy/obligations) - **Deny by default**: If an authorization call fails, deny access rather than allowing it ### Integration Pattern From 46c8a459f5d7b5c73e3780997ea31fd8253d9b7e Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Tue, 7 Apr 2026 10:48:11 -0700 Subject: [PATCH 7/8] chore(docs): reorder Authorization sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Type Reference → Typical Workflow → Best Practices → Error Handling Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/sdks/authorization.mdx | 158 ++++++++++++++++++------------------ 1 file changed, 79 insertions(+), 79 deletions(-) diff --git a/docs/sdks/authorization.mdx b/docs/sdks/authorization.mdx index 8aaeaa9c..e95d15c1 100644 --- a/docs/sdks/authorization.mdx +++ b/docs/sdks/authorization.mdx @@ -899,85 +899,6 @@ A list of [GetDecisionMultiResourceResponse](#getdecisionmultiresourceresponse) --- -## Best Practices - -### Performance - -- **Batch operations**: Use `GetDecisionBulk` for multiple authorization checks instead of calling `GetDecision` in a loop -- **Cache entitlements**: Cache `GetEntitlements` results when appropriate (consider TTL) -- **Scope queries**: Use `withComprehensiveHierarchy` only when you need expanded hierarchy values - -### Security - -- **Least privilege**: Request only the minimum necessary permissions -- **Token validation**: Ensure JWT tokens are properly validated before passing to `ForToken` -- **Obligation handling**: Always process and fulfill returned [obligations](/components/policy/obligations) -- **Deny by default**: If an authorization call fails, deny access rather than allowing it - -### Integration Pattern - -```go -import authorizationv2 "github.com/opentdf/platform/protocol/go/authorization/v2" - -// Example: Authorization middleware -func authorizationMiddleware(next http.Handler, client *sdk.SDK) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - decision, err := client.AuthorizationV2.GetDecision( - r.Context(), - &authorizationv2.GetDecisionRequest{ - EntityIdentifier: authorizationv2.WithRequestToken(), - Action: &policy.Action{Name: "access"}, - Resource: resourceFromPath(r.URL.Path), - }, - ) - if err != nil || decision.GetDecision().GetDecision() != authorizationv2.Decision_DECISION_PERMIT { - http.Error(w, "Access denied", http.StatusForbidden) - return - } - next.ServeHTTP(w, r) - }) -} -``` - -## Error Handling - - - - -```go -import ( - authorizationv2 "github.com/opentdf/platform/protocol/go/authorization/v2" - "github.com/opentdf/platform/protocol/go/policy" -) - -decision, err := client.AuthorizationV2.GetDecision(context.Background(), req) - -if err != nil { - // Log the error for debugging - log.Printf("Authorization error: %v", err) - - // Deny by default (more secure) - handleAccessDenied() - return -} - -// Process successful response -handleDecisionResponse(decision) -``` - - - - ---- - -## Typical Workflow - -1. **During resource discovery**: Use [GetEntitlements](#getentitlements) to show users what data they can access — build UIs, pre-filter content, or understand an entity's overall access scope -2. **During resource access**: Use [GetDecision](#getdecision) to enforce access controls when an entity attempts to access a specific resource -3. **For bulk operations**: Use [GetDecisionBulk](#getdecisionbulk) for efficient batch authorization when checking multiple resources at once - ---- - ## Type Reference ### Decision @@ -1065,3 +986,82 @@ Returned by [GetDecisionBulk](#getdecisionbulk). One per entity+action combinati |-------|------|-------------| | `allPermitted` | `bool` | Convenience flag — `true` if every resource in this group was permitted. | | `resourceDecisions` | [`[]ResourceDecision`](#resourcedecision) | Per-resource results with individual decisions and obligations. | + +--- + +## Typical Workflow + +1. **During resource discovery**: Use [GetEntitlements](#getentitlements) to show users what data they can access — build UIs, pre-filter content, or understand an entity's overall access scope +2. **During resource access**: Use [GetDecision](#getdecision) to enforce access controls when an entity attempts to access a specific resource +3. **For bulk operations**: Use [GetDecisionBulk](#getdecisionbulk) for efficient batch authorization when checking multiple resources at once + +--- + +## Best Practices + +### Performance + +- **Batch operations**: Use `GetDecisionBulk` for multiple authorization checks instead of calling `GetDecision` in a loop +- **Cache entitlements**: Cache `GetEntitlements` results when appropriate (consider TTL) +- **Scope queries**: Use `withComprehensiveHierarchy` only when you need expanded hierarchy values + +### Security + +- **Least privilege**: Request only the minimum necessary permissions +- **Token validation**: Ensure JWT tokens are properly validated before passing to `ForToken` +- **Obligation handling**: Always process and fulfill returned [obligations](/components/policy/obligations) +- **Deny by default**: If an authorization call fails, deny access rather than allowing it + +### Integration Pattern + +```go +import authorizationv2 "github.com/opentdf/platform/protocol/go/authorization/v2" + +// Example: Authorization middleware +func authorizationMiddleware(next http.Handler, client *sdk.SDK) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + decision, err := client.AuthorizationV2.GetDecision( + r.Context(), + &authorizationv2.GetDecisionRequest{ + EntityIdentifier: authorizationv2.WithRequestToken(), + Action: &policy.Action{Name: "access"}, + Resource: resourceFromPath(r.URL.Path), + }, + ) + if err != nil || decision.GetDecision().GetDecision() != authorizationv2.Decision_DECISION_PERMIT { + http.Error(w, "Access denied", http.StatusForbidden) + return + } + next.ServeHTTP(w, r) + }) +} +``` + +## Error Handling + + + + +```go +import ( + authorizationv2 "github.com/opentdf/platform/protocol/go/authorization/v2" + "github.com/opentdf/platform/protocol/go/policy" +) + +decision, err := client.AuthorizationV2.GetDecision(context.Background(), req) + +if err != nil { + // Log the error for debugging + log.Printf("Authorization error: %v", err) + + // Deny by default (more secure) + handleAccessDenied() + return +} + +// Process successful response +handleDecisionResponse(decision) +``` + + + From 6dc7ecc6414d23766e71668f10be44c9b5127b12 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Wed, 8 Apr 2026 07:30:17 -0700 Subject: [PATCH 8/8] fix: remove UUID from EntityIdentifier description text Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/sdks/authorization.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sdks/authorization.mdx b/docs/sdks/authorization.mdx index e95d15c1..07fb27f1 100644 --- a/docs/sdks/authorization.mdx +++ b/docs/sdks/authorization.mdx @@ -67,7 +67,7 @@ const platformClient = new PlatformClient({ ### EntityIdentifier -Every authorization call requires an `EntityIdentifier` — the entity (user, service, etc.) you're asking about. The entity can be identified by email, username, client ID, JWT token, or UUID. +Every authorization call requires an `EntityIdentifier` — the entity (user, service, etc.) you're asking about. The entity can be identified by email, username, client ID, JWT token, claims, or registered resource FQN. **Go** — use the v2 helper functions: