diff --git a/docs/sdks/authorization.mdx b/docs/sdks/authorization.mdx index d18c467e..07fb27f1 100644 --- a/docs/sdks/authorization.mdx +++ b/docs/sdks/authorization.mdx @@ -7,162 +7,227 @@ 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, claims, or registered resource FQN. + +**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'` | +| Claims | — | `.setClaims(claims)` | `'claims'` | +| Registered Resource | — | `.setRegisteredResourceValueFqn(fqn)` | `'registeredResourceValueFqn'` | + +- **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. + +--- + +## 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) +} + +for _, e := range entitlements.GetEntitlements() { + fmt.Printf("Entitled to: %v\n", e.GetActionsPerAttributeValueFqn()) } ``` @@ -170,46 +235,41 @@ func getEntitlementsV2(client *sdk.SDK) { 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 +279,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 +308,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. -You can limit entitlement queries to specific attribute hierarchies: +--- + +## GetDecision + +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 +534,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 +582,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](/components/policy/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 +737,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 +777,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 +841,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,271 +893,174 @@ 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 +## Type Reference -### Entity Identifier Helpers (Go) +### Decision -The Go SDK provides convenience constructors in the `authorization/v2` package that eliminate the deeply nested proto construction required to build an `EntityIdentifier`. +The authorization result. -| 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 | +| Value | Description | +|-------|-------------| +| `DECISION_PERMIT` | Access is granted. Check `requiredObligations` for any controls the PEP must enforce. | +| `DECISION_DENY` | Access is denied. | -```go -import authorization "github.com/opentdf/platform/protocol/go/authorization/v2" +### ResourceDecision -req := &authorization.GetDecisionRequest{ - EntityIdentifier: authorization.ForClientID("opentdf"), - // ... -} -``` +The result for a single resource in a decision response. -All five constructors work with every v2 authorization method: `GetDecision`, `GetDecisionMultiResource`, `GetDecisionBulk`, and `GetEntitlements`. +| 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. | -### Token-Based Authentication Example +### Resource - - +Identifies the data being accessed. A resource can be specified in two ways: -#### V2 API (Recommended) +| 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). 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 -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"}, - }, - }, +// Go +&authorizationv2.Resource{ + EphemeralId: "resource-1", + Resource: &authorizationv2.Resource_AttributeValues_{ + AttributeValues: &authorizationv2.Resource_AttributeValues{ + Fqns: []string{"https://example.com/attr/classification/value/secret"}, }, - } - - 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()) +```typescript +// JavaScript +{ + ephemeralId: 'resource-1', + resource: { + case: 'attributeValues', + value: { fqns: ['https://example.com/attr/classification/value/secret'] }, + }, } ``` -
-V1 API (Legacy) +### EntityEntitlements -```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"}, - }}, - }} +Returned by [GetEntitlements](#getentitlements). One per entity, mapping attribute value FQNs to the actions that entity can perform. - decisionRequest := &authorization.GetDecisionsRequest{ - DecisionRequests: decisionRequests, - } +| 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. | - decisionResponse, err := client.Authorization.GetDecisions( - context.Background(), - decisionRequest, - ) - if err != nil { - log.Fatal(err) +```go +for _, e := range resp.GetEntitlements() { + for fqn, actions := range e.GetActionsPerAttributeValueFqn() { + fmt.Printf("FQN: %s → actions: %v\n", fqn, actions.GetActions()) } +} +``` - 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()) - } - } +```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. -```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()); -} -``` +| 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. | - - +--- -```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'], - }, - }, - }, - }); +## Typical Workflow - console.log('Token-based decision:', response.decision?.decision); -} -``` +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 Optimization +### Performance -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 +- **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 Considerations +### Security -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 +- **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 Patterns +### Integration Pattern ```go +import authorizationv2 "github.com/opentdf/platform/protocol/go/authorization/v2" + // Example: Authorization middleware -func authorizationMiddleware(next http.Handler, sdk *sdk.SDK) http.Handler { +func authorizationMiddleware(next http.Handler, client *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 { + 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 -Always implement comprehensive error handling for authorization calls: - ```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 - */ - } - - // Process successful response - handleDecisionResponse(decision) +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) ```