From d53b54bdb06c47d3def87ccdf0080cf11afa32e7 Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Mon, 11 Aug 2025 12:13:26 -0400 Subject: [PATCH 01/16] Improve authorization SDK documentation and fix API version inconsistencies - Fix API version inconsistencies in authorization examples - Update Get Entitlements to use v2 API with EntityIdentifier - Update Get Decision to use v2 API with proper resource structure - Replace authorization.mdx with comprehensive documentation - Add complete JavaScript/TypeScript examples - Include best practices, error handling, and integration patterns - Add performance optimization and security guidance Addresses community requests for better authz documentation and fills gaps identified in issues #15 and #25. --- code_samples/authorization/get_decision.mdx | 48 +- .../authorization/get_entitlements.mdx | 18 +- docs/sdks/authorization.mdx | 574 +++++++++++++++++- 3 files changed, 599 insertions(+), 41 deletions(-) diff --git a/code_samples/authorization/get_decision.mdx b/code_samples/authorization/get_decision.mdx index ee1cb4f9..43802dac 100644 --- a/code_samples/authorization/get_decision.mdx +++ b/code_samples/authorization/get_decision.mdx @@ -14,6 +14,7 @@ import ( "log" "github.com/opentdf/platform/protocol/go/authorization" + "github.com/opentdf/platform/protocol/go/entity" "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/sdk" ) @@ -32,47 +33,36 @@ func main() { log.Fatal(err) } - // Get Entitlements - - decision := &authorization.GetDecisionsRequest{ - DecisionRequests: []*authorization.DecisionRequest{ - { - Actions: []*policy.Action{ - { - Value: &policy.Action_Standard{ - Standard: policy.Action_STANDARD_ACTION_DECRYPT, - }, - }, - }, - EntityChains: []*authorization.EntityChain{ + // Get Decision using v2 API + decisionReq := &authorization.GetDecisionRequest{ + EntityIdentifier: &authorization.EntityIdentifier{ + EntityChain: &entity.EntityChain{ + Entities: []*entity.Entity{ { - Id: "entity-chain-1", - Entities: []*authorization.Entity{ - { - Id: "entity-1", - EntityType: &authorization.Entity_ClientId{ - ClientId: "opentdf", - }, - }, + Id: "entity-1", + EntityType: &entity.Entity_ClientId{ + ClientId: "opentdf", }, }, }, - ResourceAttributes: []*authorization.ResourceAttribute{ - { - ResourceAttributesId: "resource-attribute-1", - AttributeValueFqns: []string{"https://opentdf.io/attr/role/value/developer"}, - }, - }, + }, + }, + Action: &policy.Action{ + Name: "decrypt", + }, + Resource: &authorization.Resource{ + AttributeValues: &authorization.Resource_AttributeValues{ + Fqns: []string{"https://opentdf.io/attr/role/value/developer"}, }, }, } - decisions, err := client.Authorization.GetDecisions(context.Background(), decision) + decision, err := client.Authorization.GetDecision(context.Background(), decisionReq) if err != nil { log.Fatal(err) } - log.Printf("Decisions: %v", decisions.GetDecisionResponses()) + log.Printf("Decision: %v", decision.GetDecision().GetDecision()) } ``` diff --git a/code_samples/authorization/get_entitlements.mdx b/code_samples/authorization/get_entitlements.mdx index 3b676946..a16e3ca6 100644 --- a/code_samples/authorization/get_entitlements.mdx +++ b/code_samples/authorization/get_entitlements.mdx @@ -14,6 +14,7 @@ import ( "log" "github.com/opentdf/platform/protocol/go/authorization" + "github.com/opentdf/platform/protocol/go/entity" "github.com/opentdf/platform/sdk" ) @@ -31,14 +32,17 @@ func main() { log.Fatal(err) } - // Get Entitlements - + // Get Entitlements using v2 API entitlementReq := &authorization.GetEntitlementsRequest{ - Entities: []*authorization.Entity{ - { - Id: "entity-1", - EntityType: &authorization.Entity_ClientId{ - ClientId: "opentdf", + EntityIdentifier: &authorization.EntityIdentifier{ + EntityChain: &entity.EntityChain{ + Entities: []*entity.Entity{ + { + Id: "entity-1", + EntityType: &entity.Entity_ClientId{ + ClientId: "opentdf", + }, + }, }, }, }, diff --git a/docs/sdks/authorization.mdx b/docs/sdks/authorization.mdx index b350b49e..478b345c 100644 --- a/docs/sdks/authorization.mdx +++ b/docs/sdks/authorization.mdx @@ -2,12 +2,576 @@ sidebar_position: 5 --- -import Decision from '../../code_samples/authorization/get_decision.mdx' -import Entitlements from '../../code_samples/authorization/get_entitlements.mdx' - +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; # Making Authorization Decisions - +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. + +## Overview + +### 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: + + + + +```go +package main + +import ( + "context" + "log" + + "github.com/opentdf/platform/protocol/go/authorization" + "github.com/opentdf/platform/sdk" +) + +func main() { + platformEndpoint := "http://localhost:9002" + + // 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 +} +``` + + + + +```java +import io.opentdf.platform.sdk.*; + +public class AuthorizationSetup { + public static void main(String[] args) { + String clientId = "opentdf"; + String clientSecret = "secret"; + String platformEndpoint = "localhost:8080"; + + SDKBuilder builder = new SDKBuilder(); + SDK sdk = builder.platformEndpoint(platformEndpoint) + .clientSecret(clientId, clientSecret) + .useInsecurePlaintextConnection(true) + .build(); + + // SDK is ready for authorization calls + } +} +``` + + + + +```javascript +import { SDK } from '@opentdf/client'; + +const sdk = new SDK({ + platformEndpoint: 'http://localhost:9002', + clientId: 'opentdf', + clientSecret: 'secret' +}); + +// SDK is ready for authorization calls +``` + + + + +## Getting Entitlements + +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 + +### Basic Entitlements Query + + + + +```go +func getEntitlements(client *sdk.SDK) { + // Using v2 API with EntityIdentifier + entitlementReq := &authorization.GetEntitlementsRequest{ + EntityIdentifier: &authorization.EntityIdentifier{ + EntityChain: &entity.EntityChain{ + Entities: []*entity.Entity{ + { + Id: "entity-1", + EntityType: &entity.Entity_ClientId{ + ClientId: "opentdf", + }, + }, + }, + }, + }, + } + + entitlements, err := client.Authorization.GetEntitlements( + context.Background(), + entitlementReq, + ) + if err != nil { + log.Fatal(err) + } + + // Process entitlements + for _, entitlement := range entitlements.GetEntitlements() { + fmt.Printf("Entity has access to: %v\n", + entitlement.ActionsPerAttributeValueFqn) + } +} +``` + + + + +```java +public void getEntitlements(SDK sdk) throws ExecutionException, InterruptedException { + GetEntitlementsRequest request = GetEntitlementsRequest.newBuilder() + .setEntityIdentifier( + EntityIdentifier.newBuilder() + .setEntityChain( + EntityChain.newBuilder() + .addEntities( + Entity.newBuilder() + .setId("entity-1") + .setClientId("opentdf") + ) + ) + ) + .build(); + + GetEntitlementsResponse resp = sdk.getServices() + .authorization() + .getEntitlements(request) + .get(); + + List entitlements = resp.getEntitlementsList(); + + for (EntityEntitlements entitlement : entitlements) { + System.out.println("Entitled to: " + + entitlement.getActionsPerAttributeValueFqnMap()); + } +} +``` + + + + +```javascript +async function getEntitlements(sdk) { + const request = { + entityIdentifier: { + entityChain: { + entities: [{ + id: 'entity-1', + clientId: 'opentdf' + }] + } + } + }; + + const response = await sdk.authorization.getEntitlements(request); + + response.entitlements.forEach(entitlement => { + console.log('Entitled to:', entitlement.actionsPerAttributeValueFqn); + }); +} +``` + + + + +### Entitlements with Scope + +You can limit entitlement queries to specific attribute hierarchies: + + + + +```go +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", + }, + }, + }, + }, + }, + // Only return entitlements within this attribute scope + 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()) +} +``` + + + + +## Making Authorization Decisions + +Use `GetDecision` when you need to authorize access to specific resources. This is the enforcement point in your application. + +### Single Resource Decision + + + + +```go +func getDecision(client *sdk.SDK) { + decisionReq := &authorization.GetDecisionRequest{ + EntityIdentifier: &authorization.EntityIdentifier{ + EntityChain: &entity.EntityChain{ + Entities: []*entity.Entity{ + { + Id: "user-123", + EntityType: &entity.Entity_EmailAddress{ + EmailAddress: "user@company.com", + }, + }, + }, + }, + }, + Action: &policy.Action{ + Name: "decrypt", + }, + Resource: &authorization.Resource{ + AttributeValues: &authorization.Resource_AttributeValues{ + Fqns: []string{ + "https://company.com/attr/classification/value/confidential", + "https://company.com/attr/department/value/finance", + }, + }, + }, + } + + decision, err := client.Authorization.GetDecision( + context.Background(), + decisionReq, + ) + if err != nil { + log.Fatal(err) + } + + if decision.Decision.Decision == authorization.Decision_DECISION_PERMIT { + fmt.Println("Access granted") + // Process any obligations + if len(decision.Decision.Obligations) > 0 { + fmt.Printf("Obligations to fulfill: %v\n", decision.Decision.Obligations) + } + } else { + fmt.Println("Access denied") + } +} +``` + + + + +```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/classification/value/confidential") + .addFqns("https://company.com/attr/department/value/finance") + ) + ) + .build(); + + GetDecisionResponse resp = sdk.getServices() + .authorization() + .getDecision(request) + .get(); + + if (resp.getDecision().getDecision() == Decision.DECISION_PERMIT) { + System.out.println("Access granted"); + } else { + System.out.println("Access denied"); + } +} +``` + + + + +```javascript +async function getDecision(sdk) { + const request = { + entityIdentifier: { + entityChain: { + entities: [{ + id: 'user-123', + emailAddress: 'user@company.com' + }] + } + }, + action: { + name: 'decrypt' + }, + resource: { + attributeValues: { + fqns: [ + 'https://company.com/attr/classification/value/confidential', + 'https://company.com/attr/department/value/finance' + ] + } + } + }; + + const response = await sdk.authorization.getDecision(request); + + if (response.decision.decision === 'DECISION_PERMIT') { + console.log('Access granted'); + if (response.decision.obligations?.length > 0) { + console.log('Obligations:', response.decision.obligations); + } + } else { + console.log('Access denied'); + } +} +``` + + + + +### Bulk Authorization Decisions + +For efficient batch processing, use bulk decision endpoints: + + + + +```go +func getBulkDecisions(client *sdk.SDK) { + bulkReq := &authorization.GetDecisionBulkRequest{ + DecisionRequests: []*authorization.GetDecisionMultiResourceRequest{ + { + EntityIdentifier: &authorization.EntityIdentifier{ + EntityChain: &entity.EntityChain{ + Entities: []*entity.Entity{ + { + Id: "user-123", + EntityType: &entity.Entity_EmailAddress{ + EmailAddress: "user@company.com", + }, + }, + }, + }, + }, + Action: &policy.Action{Name: "decrypt"}, + Resources: []*authorization.Resource{ + { + EphemeralId: "resource-1", + AttributeValues: &authorization.Resource_AttributeValues{ + Fqns: []string{"https://company.com/attr/class/value/public"}, + }, + }, + { + EphemeralId: "resource-2", + AttributeValues: &authorization.Resource_AttributeValues{ + Fqns: []string{"https://company.com/attr/class/value/confidential"}, + }, + }, + }, + }, + }, + } + + decisions, err := client.Authorization.GetDecisionBulk( + context.Background(), + bulkReq, + ) + if err != nil { + log.Fatal(err) + } + + for _, resp := range decisions.DecisionResponses { + fmt.Printf("All resources permitted: %v\n", resp.AllPermitted) + for _, resourceDecision := range resp.ResourceDecisions { + fmt.Printf("Resource %s: %v\n", + resourceDecision.EphemeralResourceId, + resourceDecision.Decision) + } + } +} +``` + + + + +## Entity Types and Authentication + +OpenTDF supports various entity types for flexible authentication: + +### 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 + +### Token-Based Authentication Example + + + + +```go +func getDecisionWithToken(client *sdk.SDK, jwtToken string) { + decisionReq := &authorization.GetDecisionRequest{ + EntityIdentifier: &authorization.EntityIdentifier{ + Token: &entity.Token{ + Id: "token-1", + Jwt: jwtToken, + }, + }, + Action: &policy.Action{Name: "decrypt"}, + Resource: &authorization.Resource{ + AttributeValues: &authorization.Resource_AttributeValues{ + Fqns: []string{"https://company.com/attr/classification/value/public"}, + }, + }, + } + + decision, err := client.Authorization.GetDecision( + context.Background(), + decisionReq, + ) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Token-based decision: %v\n", decision.Decision.Decision) +} +``` + + + + +## Best Practices + +### Performance Optimization + +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 + +### Security Considerations + +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 +4. **Error Handling**: Implement proper error handling and fallback policies + +### Integration Patterns + +```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.Decision_DECISION_PERMIT { + next.ServeHTTP(w, r) + } else { + http.Error(w, "Access denied", http.StatusForbidden) + } + }) +} +``` + +## Error Handling + +Always implement comprehensive error handling for authorization calls: + + + + +```go +func safeAuthorizationCall(client *sdk.SDK) { + decision, err := client.Authorization.GetDecision(context.Background(), req) + + if err != nil { + // Log the error for debugging + log.Printf("Authorization error: %v", err) + + // Implement your fallback policy + // Option 1: Deny by default (more secure) + return handleAccessDenied() + + // Option 2: Allow by default (less secure, only for non-critical resources) + // return handleAccessAllowed() + + // Option 3: Retry with exponential backoff + // return retryWithBackoff(client, req) + } + + // Process successful response + return handleDecisionResponse(decision) +} +``` - \ No newline at end of file + + From 180d3e5ef828976b32f28f0631af211db216318d Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Mon, 11 Aug 2025 12:33:54 -0400 Subject: [PATCH 02/16] Address review comments: fix missing imports and undefined variable - Add missing imports to Go setup example (fmt, entity, policy, proto) - Add missing imports to Java setup example (authorization, entity, policy packages) - Fix undefined 'req' variable in safeAuthorizationCall function by adding it as parameter Resolves compilation issues identified in Gemini Code Assist review. --- docs/sdks/authorization.mdx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/sdks/authorization.mdx b/docs/sdks/authorization.mdx index 478b345c..097c9f37 100644 --- a/docs/sdks/authorization.mdx +++ b/docs/sdks/authorization.mdx @@ -34,10 +34,14 @@ package main import ( "context" + "fmt" "log" "github.com/opentdf/platform/protocol/go/authorization" + "github.com/opentdf/platform/protocol/go/entity" + "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/sdk" + "google.golang.org/protobuf/proto" ) func main() { @@ -61,6 +65,11 @@ func main() { ```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) { @@ -550,7 +559,7 @@ Always implement comprehensive error handling for authorization calls: ```go -func safeAuthorizationCall(client *sdk.SDK) { +func safeAuthorizationCall(client *sdk.SDK, req *authorization.GetDecisionRequest) { decision, err := client.Authorization.GetDecision(context.Background(), req) if err != nil { From b6ce8fe4adac0b25b0130d4efcc995fd9958483d Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Mon, 11 Aug 2025 13:30:15 -0400 Subject: [PATCH 03/16] Update getEntitlements examples to use realistic email-based user - Changed entity from generic clientId 'opentdf' to user 'bob@OrgA.com' - Updated across all languages (Go, Java, JavaScript) - Uses EmailAddress entity type instead of ClientId - Provides more realistic example for developers --- docs/sdks/authorization.mdx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/sdks/authorization.mdx b/docs/sdks/authorization.mdx index 097c9f37..0abf0e4f 100644 --- a/docs/sdks/authorization.mdx +++ b/docs/sdks/authorization.mdx @@ -126,9 +126,9 @@ func getEntitlements(client *sdk.SDK) { EntityChain: &entity.EntityChain{ Entities: []*entity.Entity{ { - Id: "entity-1", - EntityType: &entity.Entity_ClientId{ - ClientId: "opentdf", + Id: "user-bob", + EntityType: &entity.Entity_EmailAddress{ + EmailAddress: "bob@OrgA.com", }, }, }, @@ -164,8 +164,8 @@ public void getEntitlements(SDK sdk) throws ExecutionException, InterruptedExcep EntityChain.newBuilder() .addEntities( Entity.newBuilder() - .setId("entity-1") - .setClientId("opentdf") + .setId("user-bob") + .setEmailAddress("bob@OrgA.com") ) ) ) @@ -194,8 +194,8 @@ async function getEntitlements(sdk) { entityIdentifier: { entityChain: { entities: [{ - id: 'entity-1', - clientId: 'opentdf' + id: 'user-bob', + emailAddress: 'bob@OrgA.com' }] } } From 88ac7232bd575d4e8852d528205e7a58a3ea9053 Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Mon, 11 Aug 2025 13:53:59 -0400 Subject: [PATCH 04/16] Address all remaining review comments: improve code quality and consistency - Fix trailing space in Java clientSecret string literal - Correct misleading comment for WithComprehensiveHierarchy parameter - Update protobuf field access to use getter methods instead of direct access - Fix bulk decision examples to use proper BoolValue handling - Standardize Java endpoint to http://localhost:9002 for consistency - Fix error handling function syntax errors and unreachable code All examples now follow protobuf best practices and have consistent formatting. --- docs/sdks/authorization.mdx | 44 +++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/docs/sdks/authorization.mdx b/docs/sdks/authorization.mdx index 0abf0e4f..ec6eff4d 100644 --- a/docs/sdks/authorization.mdx +++ b/docs/sdks/authorization.mdx @@ -74,8 +74,8 @@ import java.util.concurrent.ExecutionException; public class AuthorizationSetup { public static void main(String[] args) { String clientId = "opentdf"; - String clientSecret = "secret"; - String platformEndpoint = "localhost:8080"; + String clientSecret = "secret"; + String platformEndpoint = "http://localhost:9002"; SDKBuilder builder = new SDKBuilder(); SDK sdk = builder.platformEndpoint(platformEndpoint) @@ -234,7 +234,7 @@ func getEntitlementsWithScope(client *sdk.SDK) { }, }, }, - // Only return entitlements within this attribute scope + // When true, returns all entitled values for attributes with hierarchy rules, propagating down from the entitled value WithComprehensiveHierarchy: proto.Bool(true), } @@ -298,11 +298,12 @@ func getDecision(client *sdk.SDK) { log.Fatal(err) } - if decision.Decision.Decision == authorization.Decision_DECISION_PERMIT { + resDecision := decision.GetDecision() + if resDecision.GetDecision() == authorization.Decision_DECISION_PERMIT { fmt.Println("Access granted") // Process any obligations - if len(decision.Decision.Obligations) > 0 { - fmt.Printf("Obligations to fulfill: %v\n", decision.Decision.Obligations) + if len(resDecision.GetObligations()) > 0 { + fmt.Printf("Obligations to fulfill: %v\n", resDecision.GetObligations()) } } else { fmt.Println("Access denied") @@ -448,12 +449,15 @@ func getBulkDecisions(client *sdk.SDK) { log.Fatal(err) } - for _, resp := range decisions.DecisionResponses { - fmt.Printf("All resources permitted: %v\n", resp.AllPermitted) - for _, resourceDecision := range resp.ResourceDecisions { + 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.EphemeralResourceId, - resourceDecision.Decision) + resourceDecision.GetEphemeralResourceId(), + resourceDecision.GetDecision()) } } } @@ -566,19 +570,27 @@ func safeAuthorizationCall(client *sdk.SDK, req *authorization.GetDecisionReques // Log the error for debugging log.Printf("Authorization error: %v", err) - // Implement your fallback policy + // Implement your fallback policy. Choose one of the options below. + // Option 1: Deny by default (more secure) - return handleAccessDenied() + handleAccessDenied() + return + /* // Option 2: Allow by default (less secure, only for non-critical resources) - // return handleAccessAllowed() + handleAccessAllowed() + return + */ + /* // Option 3: Retry with exponential backoff - // return retryWithBackoff(client, req) + retryWithBackoff(client, req) + return + */ } // Process successful response - return handleDecisionResponse(decision) + handleDecisionResponse(decision) } ``` From 9315721122a39839adeaac2880f2c2f68b06825b Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Mon, 11 Aug 2025 14:57:46 -0400 Subject: [PATCH 05/16] Standardize platform endpoints across all code samples to http://localhost:8080 - Updated authorization documentation (docs/sdks/authorization.mdx) from port 9002 to 8080 - Updated all code sample files to use consistent http://localhost:8080 endpoint: - Go examples: Changed from http://localhost:9002 to http://localhost:8080 - Java examples: Changed from localhost:8080 to http://localhost:8080 (added http://) - This ensures consistency across all documentation and addresses review feedback - Affects authorization, policy, TDF encryption, and other code samples --- code_samples/authorization/get_decision.mdx | 4 ++-- code_samples/authorization/get_entitlements.mdx | 4 ++-- code_samples/policy_code/create_attribute.mdx | 4 ++-- code_samples/policy_code/create_namespace.mdx | 4 ++-- code_samples/policy_code/create_subject_condition_set.mdx | 4 ++-- code_samples/policy_code/create_subject_mapping.mdx | 4 ++-- code_samples/policy_code/list_attributes.mdx | 4 ++-- code_samples/policy_code/list_namespaces.mdx | 4 ++-- code_samples/policy_code/list_subject_mapping.mdx | 4 ++-- code_samples/tdf/encryption_ztdf.mdx | 4 ++-- docs/sdks/authorization.mdx | 6 +++--- 11 files changed, 23 insertions(+), 23 deletions(-) diff --git a/code_samples/authorization/get_decision.mdx b/code_samples/authorization/get_decision.mdx index 43802dac..0e755446 100644 --- a/code_samples/authorization/get_decision.mdx +++ b/code_samples/authorization/get_decision.mdx @@ -21,7 +21,7 @@ import ( func main() { - platformEndpoint := "http://localhost:9002" + platformEndpoint := "http://localhost:8080" // Create a new client client, err := sdk.New( @@ -85,7 +85,7 @@ public class GetDecisions { String clientId = "opentdf"; String clientSecret = "secret"; - String platformEndpoint = "localhost:8080"; + String platformEndpoint = "http://localhost:8080"; SDKBuilder builder = new SDKBuilder(); SDK sdk = builder.platformEndpoint(platformEndpoint) diff --git a/code_samples/authorization/get_entitlements.mdx b/code_samples/authorization/get_entitlements.mdx index a16e3ca6..a4533271 100644 --- a/code_samples/authorization/get_entitlements.mdx +++ b/code_samples/authorization/get_entitlements.mdx @@ -20,7 +20,7 @@ import ( func main() { - platformEndpoint := "http://localhost:9002" + platformEndpoint := "http://localhost:8080" // Create a new client client, err := sdk.New( @@ -75,7 +75,7 @@ public class GetEntitlements { String clientId = "opentdf"; String clientSecret = "secret"; - String platformEndpoint = "localhost:8080"; + String platformEndpoint = "http://localhost:8080"; SDKBuilder builder = new SDKBuilder(); SDK sdk = builder.platformEndpoint(platformEndpoint) diff --git a/code_samples/policy_code/create_attribute.mdx b/code_samples/policy_code/create_attribute.mdx index 8a5710c1..5afc737f 100644 --- a/code_samples/policy_code/create_attribute.mdx +++ b/code_samples/policy_code/create_attribute.mdx @@ -22,7 +22,7 @@ import ( func main() { - platformEndpoint := "http://localhost:9002" + platformEndpoint := "http://localhost:8080" // Create a new client client, err := sdk.New( @@ -83,7 +83,7 @@ public class CreateAttribute { String clientId = "opentdf"; String clientSecret = "secret"; - String platformEndpoint = "localhost:8080"; + String platformEndpoint = "http://localhost:8080"; SDKBuilder builder = new SDKBuilder(); SDK sdk = builder.platformEndpoint(platformEndpoint) diff --git a/code_samples/policy_code/create_namespace.mdx b/code_samples/policy_code/create_namespace.mdx index 403293da..037b6b66 100644 --- a/code_samples/policy_code/create_namespace.mdx +++ b/code_samples/policy_code/create_namespace.mdx @@ -19,7 +19,7 @@ import ( func main() { - platformEndpoint := "http://localhost:9002" + platformEndpoint := "http://localhost:8080" // Create a new client client, err := sdk.New( @@ -59,7 +59,7 @@ public class CreateNamespace { String clientId = "opentdf"; String clientSecret = "secret"; - String platformEndpoint = "localhost:8080"; + String platformEndpoint = "http://localhost:8080"; SDKBuilder builder = new SDKBuilder(); SDK sdk = builder.platformEndpoint(platformEndpoint) diff --git a/code_samples/policy_code/create_subject_condition_set.mdx b/code_samples/policy_code/create_subject_condition_set.mdx index b84851a0..04fb78c3 100644 --- a/code_samples/policy_code/create_subject_condition_set.mdx +++ b/code_samples/policy_code/create_subject_condition_set.mdx @@ -20,7 +20,7 @@ import ( func main() { - platformEndpoint := "http://localhost:9002" + platformEndpoint := "http://localhost:8080" // Create a new client client, err := sdk.New( @@ -88,7 +88,7 @@ public class CreateSubjectConditionSet { String clientId = "opentdf"; String clientSecret = "secret"; - String platformEndpoint = "localhost:8080"; + String platformEndpoint = "http://localhost:8080"; SDKBuilder builder = new SDKBuilder(); SDK sdk = builder.platformEndpoint(platformEndpoint) diff --git a/code_samples/policy_code/create_subject_mapping.mdx b/code_samples/policy_code/create_subject_mapping.mdx index ee3826bb..7ca9f255 100644 --- a/code_samples/policy_code/create_subject_mapping.mdx +++ b/code_samples/policy_code/create_subject_mapping.mdx @@ -20,7 +20,7 @@ import ( func main() { - platformEndpoint := "http://localhost:9002" + platformEndpoint := "http://localhost:8080" // Create a new client client, err := sdk.New( @@ -70,7 +70,7 @@ public class CreateSubjectMapping { String clientId = "opentdf"; String clientSecret = "secret"; - String platformEndpoint = "localhost:8080"; + String platformEndpoint = "http://localhost:8080"; SDKBuilder builder = new SDKBuilder(); SDK sdk = builder.platformEndpoint(platformEndpoint) diff --git a/code_samples/policy_code/list_attributes.mdx b/code_samples/policy_code/list_attributes.mdx index b56f8524..66591c60 100644 --- a/code_samples/policy_code/list_attributes.mdx +++ b/code_samples/policy_code/list_attributes.mdx @@ -19,7 +19,7 @@ import ( func main() { - platformEndpoint := "http://localhost:9002" + platformEndpoint := "http://localhost:8080" // Create a new client client, err := sdk.New( @@ -68,7 +68,7 @@ public class ListAttributes { String clientId = "opentdf"; String clientSecret = "secret"; - String platformEndpoint = "localhost:8080"; + String platformEndpoint = "http://localhost:8080"; SDKBuilder builder = new SDKBuilder(); SDK sdk = builder.platformEndpoint(platformEndpoint) diff --git a/code_samples/policy_code/list_namespaces.mdx b/code_samples/policy_code/list_namespaces.mdx index 638a33c8..fcb3ccb8 100644 --- a/code_samples/policy_code/list_namespaces.mdx +++ b/code_samples/policy_code/list_namespaces.mdx @@ -19,7 +19,7 @@ import ( func main() { - platformEndpoint := "http://localhost:9002" + platformEndpoint := "http://localhost:8080" // Create a new client client, err := sdk.New( @@ -59,7 +59,7 @@ public class ListNamespaces { String clientId = "opentdf"; String clientSecret = "secret"; - String platformEndpoint = "localhost:8080"; + String platformEndpoint = "http://localhost:8080"; SDKBuilder builder = new SDKBuilder(); SDK sdk = builder.platformEndpoint(platformEndpoint) diff --git a/code_samples/policy_code/list_subject_mapping.mdx b/code_samples/policy_code/list_subject_mapping.mdx index 418841fd..a2d7fea3 100644 --- a/code_samples/policy_code/list_subject_mapping.mdx +++ b/code_samples/policy_code/list_subject_mapping.mdx @@ -19,7 +19,7 @@ import ( func main() { - platformEndpoint := "http://localhost:9002" + platformEndpoint := "http://localhost:8080" // Create a new client client, err := sdk.New( @@ -64,7 +64,7 @@ public class ListSubjectMappings { String clientId = "opentdf"; String clientSecret = "secret"; - String platformEndpoint = "localhost:8080"; + String platformEndpoint = "http://localhost:8080"; SDKBuilder builder = new SDKBuilder(); SDK sdk = builder.platformEndpoint(platformEndpoint) diff --git a/code_samples/tdf/encryption_ztdf.mdx b/code_samples/tdf/encryption_ztdf.mdx index 34373670..17a22aa1 100644 --- a/code_samples/tdf/encryption_ztdf.mdx +++ b/code_samples/tdf/encryption_ztdf.mdx @@ -20,7 +20,7 @@ import ( func main() { log.Println("🚀 Starting OpenTDF example...") - platformEndpoint := "http://localhost:9002" + platformEndpoint := "http://localhost:8080" log.Printf("📡 Connecting to platform: %s", platformEndpoint) // Create a new client @@ -107,7 +107,7 @@ public class EncryptExample { public static void main(String[] args) throws IOException, JOSEException, AutoConfigureException, InterruptedException, ExecutionException { String clientId = "opentdf"; String clientSecret = "secret"; - String platformEndpoint = "localhost:8080"; + String platformEndpoint = "http://localhost:8080"; SDKBuilder builder = new SDKBuilder(); SDK sdk = builder.platformEndpoint(platformEndpoint) diff --git a/docs/sdks/authorization.mdx b/docs/sdks/authorization.mdx index ec6eff4d..7ed949ba 100644 --- a/docs/sdks/authorization.mdx +++ b/docs/sdks/authorization.mdx @@ -45,7 +45,7 @@ import ( ) func main() { - platformEndpoint := "http://localhost:9002" + platformEndpoint := "http://localhost:8080" // Create authenticated client client, err := sdk.New( @@ -75,7 +75,7 @@ public class AuthorizationSetup { public static void main(String[] args) { String clientId = "opentdf"; String clientSecret = "secret"; - String platformEndpoint = "http://localhost:9002"; + String platformEndpoint = "http://localhost:8080"; SDKBuilder builder = new SDKBuilder(); SDK sdk = builder.platformEndpoint(platformEndpoint) @@ -95,7 +95,7 @@ public class AuthorizationSetup { import { SDK } from '@opentdf/client'; const sdk = new SDK({ - platformEndpoint: 'http://localhost:9002', + platformEndpoint: 'http://localhost:8080', clientId: 'opentdf', clientSecret: 'secret' }); From 24e969e208865290b030a60411d1047082f44b0a Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Mon, 11 Aug 2025 15:17:59 -0400 Subject: [PATCH 06/16] Address PR review feedback from Gemini Code Assist **High Priority Issues Fixed:** - Updated Java GetDecision example to use v2 API instead of v1 API - Updated Java GetEntitlements example to use v2 API with EntityIdentifier - Both Java examples now use consistent v2 API patterns matching Go examples **Medium Priority Issues Fixed:** - Fixed Java code indentation consistency (standardized 4-space continuation indent) - Added missing Java and JavaScript examples to complete language coverage: - Entitlements with Scope section - Bulk Authorization Decisions section - Token-Based Authentication Example section **Documentation Improvements:** - All sections now have complete Go, Java, and JavaScript examples - Java examples use proper v2 API patterns with EntityIdentifier - Consistent code formatting across all language examples - Enhanced developer experience with comprehensive language coverage Addresses all issues raised in the latest Gemini Code Assist review. --- code_samples/authorization/get_decision.mdx | 49 ++-- .../authorization/get_entitlements.mdx | 20 +- docs/sdks/authorization.mdx | 231 +++++++++++++++++- 3 files changed, 277 insertions(+), 23 deletions(-) diff --git a/code_samples/authorization/get_decision.mdx b/code_samples/authorization/get_decision.mdx index 0e755446..474be909 100644 --- a/code_samples/authorization/get_decision.mdx +++ b/code_samples/authorization/get_decision.mdx @@ -76,11 +76,10 @@ import io.opentdf.platform.sdk.*; import java.util.concurrent.ExecutionException; import io.opentdf.platform.authorization.*; -import io.opentdf.platform.policy.Action; +import io.opentdf.platform.entity.*; +import io.opentdf.platform.policy.*; -import java.util.List; - -public class GetDecisions { +public class GetDecision { public static void main(String[] args) throws ExecutionException, InterruptedException{ String clientId = "opentdf"; @@ -92,19 +91,35 @@ public class GetDecisions { .clientSecret(clientId, clientSecret).useInsecurePlaintextConnection(true) .build(); - GetDecisionsRequest request = GetDecisionsRequest.newBuilder() - .addDecisionRequests(DecisionRequest.newBuilder() - .addEntityChains(EntityChain.newBuilder().setId("ec1").addEntities(Entity.newBuilder().setId("entity-1").setClientId("opentdf"))) - .addActions(Action.newBuilder().setStandard(Action.StandardAction.STANDARD_ACTION_DECRYPT)) - .addResourceAttributes(ResourceAttribute.newBuilder().setResourceAttributesId("resource-attribute-1") - .addAttributeValueFqns("https://mynamespace.com/attr/test/value/test1")) - ).build(); - - GetDecisionsResponse resp = sdk.getServices().authorization().getDecisions(request).get(); - - List decisions = resp.getDecisionResponsesList(); - - System.out.println(DecisionResponse.Decision.forNumber(decisions.get(0).getDecisionValue())); + // Get Decision using v2 API + GetDecisionRequest request = GetDecisionRequest.newBuilder() + .setEntityIdentifier( + EntityIdentifier.newBuilder() + .setEntityChain( + EntityChain.newBuilder() + .addEntities( + Entity.newBuilder() + .setId("entity-1") + .setClientId("opentdf") + ) + ) + ) + .setAction( + Action.newBuilder() + .setName("decrypt") + ) + .setResource( + Resource.newBuilder() + .setAttributeValues( + Resource.AttributeValues.newBuilder() + .addFqns("https://opentdf.io/attr/role/value/developer") + ) + ) + .build(); + + GetDecisionResponse resp = sdk.getServices().authorization().getDecision(request).get(); + + System.out.println("Decision: " + resp.getDecision().getDecision()); } } ``` diff --git a/code_samples/authorization/get_entitlements.mdx b/code_samples/authorization/get_entitlements.mdx index a4533271..e536918d 100644 --- a/code_samples/authorization/get_entitlements.mdx +++ b/code_samples/authorization/get_entitlements.mdx @@ -67,6 +67,7 @@ import io.opentdf.platform.sdk.*; import java.util.concurrent.ExecutionException; import io.opentdf.platform.authorization.*; +import io.opentdf.platform.entity.*; import java.util.List; @@ -82,15 +83,28 @@ public class GetEntitlements { .clientSecret(clientId, clientSecret).useInsecurePlaintextConnection(true) .build(); + // Get Entitlements using v2 API GetEntitlementsRequest request = GetEntitlementsRequest.newBuilder() - .addEntities(Entity.newBuilder().setId("entity-1").setClientId("opentdf")) - .build(); + .setEntityIdentifier( + EntityIdentifier.newBuilder() + .setEntityChain( + EntityChain.newBuilder() + .addEntities( + Entity.newBuilder() + .setId("entity-1") + .setClientId("opentdf") + ) + ) + ) + .build(); GetEntitlementsResponse resp = sdk.getServices().authorization().getEntitlements(request).get(); List entitlements = resp.getEntitlementsList(); - System.out.println(entitlements.get(0).getAttributeValueFqnsList()); + for (EntityEntitlements entitlement : entitlements) { + System.out.println("Entitled to: " + entitlement.getActionsPerAttributeValueFqnMap()); + } } } ``` diff --git a/docs/sdks/authorization.mdx b/docs/sdks/authorization.mdx index 7ed949ba..23b16fc3 100644 --- a/docs/sdks/authorization.mdx +++ b/docs/sdks/authorization.mdx @@ -79,9 +79,9 @@ public class AuthorizationSetup { SDKBuilder builder = new SDKBuilder(); SDK sdk = builder.platformEndpoint(platformEndpoint) - .clientSecret(clientId, clientSecret) - .useInsecurePlaintextConnection(true) - .build(); + .clientSecret(clientId, clientSecret) + .useInsecurePlaintextConnection(true) + .build(); // SDK is ready for authorization calls } @@ -250,6 +250,60 @@ func getEntitlementsWithScope(client *sdk.SDK) { } ``` + + + +```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()); +} +``` + + + + +```javascript +async function getEntitlementsWithScope(sdk) { + const request = { + entityIdentifier: { + entityChain: { + entities: [{ + id: 'user-123', + emailAddress: 'user@company.com' + }] + } + }, + // When true, returns all entitled values for attributes with hierarchy rules + withComprehensiveHierarchy: true + }; + + const response = await sdk.authorization.getEntitlements(request); + + console.log('Scoped entitlements:', response.entitlements); +} +``` + @@ -463,6 +517,113 @@ func getBulkDecisions(client *sdk.SDK) { } ``` + + + +```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()); + } + } +} +``` + + + + +```javascript +async function getBulkDecisions(sdk) { + const request = { + decisionRequests: [{ + entityIdentifier: { + entityChain: { + entities: [{ + id: 'user-123', + emailAddress: 'user@company.com' + }] + } + }, + action: { + name: 'decrypt' + }, + resources: [ + { + ephemeralId: 'resource-1', + attributeValues: { + fqns: ['https://company.com/attr/class/value/public'] + } + }, + { + ephemeralId: 'resource-2', + attributeValues: { + fqns: ['https://company.com/attr/class/value/confidential'] + } + } + ] + }] + }; + + const response = await sdk.authorization.getDecisionBulk(request); + + response.decisionResponses.forEach(resp => { + if (resp.allPermitted !== undefined) { + console.log('All resources permitted:', resp.allPermitted.value); + } + resp.resourceDecisions.forEach(resourceDecision => { + console.log(`Resource ${resourceDecision.ephemeralResourceId}: ${resourceDecision.decision}`); + }); + }); +} +``` + @@ -513,6 +674,70 @@ func getDecisionWithToken(client *sdk.SDK, jwtToken string) { } ``` + + + +```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/classification/value/public") + ) + ) + .build(); + + GetDecisionResponse resp = sdk.getServices() + .authorization() + .getDecision(request) + .get(); + + System.out.println("Token-based decision: " + resp.getDecision().getDecision()); +} +``` + + + + +```javascript +async function getDecisionWithToken(sdk, jwtToken) { + const request = { + entityIdentifier: { + token: { + id: 'token-1', + jwt: jwtToken + } + }, + action: { + name: 'decrypt' + }, + resource: { + attributeValues: { + fqns: ['https://company.com/attr/classification/value/public'] + } + } + }; + + const response = await sdk.authorization.getDecision(request); + + console.log('Token-based decision:', response.decision.decision); +} +``` + From 50b74e13c27e7a270d2cd19dd1eb11f88f85b53b Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Tue, 12 Aug 2025 00:05:03 -0400 Subject: [PATCH 07/16] Fix protobuf getter usage in Token-based authentication example - Use decision.GetDecision().GetDecision() instead of direct field access - Ensures consistency with protobuf best practices throughout the file - Addresses final direct field access issue in Go token authentication example --- 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 23b16fc3..50df4741 100644 --- a/docs/sdks/authorization.mdx +++ b/docs/sdks/authorization.mdx @@ -670,7 +670,7 @@ func getDecisionWithToken(client *sdk.SDK, jwtToken string) { log.Fatal(err) } - fmt.Printf("Token-based decision: %v\n", decision.Decision.Decision) + fmt.Printf("Token-based decision: %v\n", decision.GetDecision().GetDecision()) } ``` From 8257d8b7e8599ac7d197de0e4b09a840db0f2834 Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Tue, 12 Aug 2025 10:36:52 -0400 Subject: [PATCH 08/16] Add obligation handling to authorization examples - Add obligation handling to Go example in get_decision.mdx - Add obligation handling to Java examples in both get_decision.mdx and authorization.mdx - Add complete JavaScript example to get_decision.mdx with obligation handling - Examples now properly check for PERMIT decisions and display any obligations - Addresses GitHub review comments about incomplete examples and missing obligation handling --- code_samples/authorization/get_decision.mdx | 61 ++++++++++++++++++++- docs/sdks/authorization.mdx | 4 ++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/code_samples/authorization/get_decision.mdx b/code_samples/authorization/get_decision.mdx index 474be909..3263dfb0 100644 --- a/code_samples/authorization/get_decision.mdx +++ b/code_samples/authorization/get_decision.mdx @@ -62,7 +62,11 @@ func main() { log.Fatal(err) } - log.Printf("Decision: %v", decision.GetDecision().GetDecision()) + decisionResult := decision.GetDecision() + log.Printf("Decision: %v", decisionResult.GetDecision()) + if decisionResult.GetDecision() == authorization.Decision_DECISION_PERMIT && len(decisionResult.GetObligations()) > 0 { + log.Printf("Obligations: %v", decisionResult.GetObligations()) + } } ``` @@ -119,7 +123,11 @@ public class GetDecision { GetDecisionResponse resp = sdk.getServices().authorization().getDecision(request).get(); - System.out.println("Decision: " + resp.getDecision().getDecision()); + Decision decision = resp.getDecision(); + System.out.println("Decision: " + decision.getDecision()); + if (decision.getDecision() == Decision.DECISION_PERMIT && decision.getObligationsCount() > 0) { + System.out.println("Obligations: " + decision.getObligationsList()); + } } } ``` @@ -128,6 +136,55 @@ public class GetDecision { ```javascript +const { AuthzClient } = require('@opentdf/client'); + +async function main() { + const platformEndpoint = 'http://localhost:8080'; + const clientId = 'opentdf'; + const clientSecret = 'secret'; + + // Create a new client + const client = new AuthzClient({ + endpoint: platformEndpoint, + auth: { + clientId, + clientSecret + } + }); + + // Get Decision using v2 API + const request = { + entityIdentifier: { + entityChain: { + entities: [{ + id: 'entity-1', + clientId: 'opentdf' + }] + } + }, + action: { + name: 'decrypt' + }, + resource: { + attributeValues: { + fqns: ['https://opentdf.io/attr/role/value/developer'] + } + } + }; + + try { + const response = await client.getDecision(request); + + console.log('Decision:', response.decision.decision); + if (response.decision.decision === 'DECISION_PERMIT' && response.decision.obligations?.length > 0) { + console.log('Obligations:', response.decision.obligations); + } + } catch (error) { + console.error('Error:', error); + } +} + +main(); ``` diff --git a/docs/sdks/authorization.mdx b/docs/sdks/authorization.mdx index 50df4741..e86865d0 100644 --- a/docs/sdks/authorization.mdx +++ b/docs/sdks/authorization.mdx @@ -403,6 +403,10 @@ public void getDecision(SDK sdk) throws ExecutionException, InterruptedException if (resp.getDecision().getDecision() == Decision.DECISION_PERMIT) { System.out.println("Access granted"); + // Process any obligations + if (resp.getDecision().getObligationsCount() > 0) { + System.out.println("Obligations to fulfill: " + resp.getDecision().getObligationsList()); + } } else { System.out.println("Access denied"); } From 33ea9933183f8b91001f187a6577b55e23a79a46 Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Thu, 14 Aug 2025 21:27:58 -0400 Subject: [PATCH 09/16] Fix OpenTDF Authorization SDK documentation with accurate v1/v2 API examples - Add both v1 and v2 API examples for Go SDK throughout documentation - Fix v2 API usage patterns: Resource nesting, EntityIdentifier wrappers, EphemeralId fields - Update JavaScript examples to use proper PlatformClient and token-based authentication - Standardize all platform endpoints to http://localhost:8080 - Add comprehensive v1 vs v2 API distinction and guidance - Ensure all Go code examples compile and work correctly - Add manual_tests/ to .gitignore --- .gitignore | 3 + code_samples/authorization/get_decision.mdx | 189 +++++++--- docs/sdks/authorization.mdx | 390 ++++++++++++++++---- 3 files changed, 451 insertions(+), 131 deletions(-) diff --git a/.gitignore b/.gitignore index 71e2dea5..71d35fc7 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ node_modules .github/vale-styles/* # Except for the config directory where we keep the vocab !.github/vale-styles/config/ + +# Ignore manual test scripts +manual_tests/ diff --git a/code_samples/authorization/get_decision.mdx b/code_samples/authorization/get_decision.mdx index 3263dfb0..b2da40ec 100644 --- a/code_samples/authorization/get_decision.mdx +++ b/code_samples/authorization/get_decision.mdx @@ -6,6 +6,8 @@ import TabItem from '@theme/TabItem'; +#### V2 API (Recommended) + ```go package main @@ -13,14 +15,13 @@ import ( "context" "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" ) func main() { - platformEndpoint := "http://localhost:8080" // Create a new client @@ -28,44 +29,114 @@ func main() { platformEndpoint, sdk.WithClientCredentials("opentdf", "secret", nil), ) - if err != nil { log.Fatal(err) } // Get Decision using v2 API - decisionReq := &authorization.GetDecisionRequest{ - EntityIdentifier: &authorization.EntityIdentifier{ - EntityChain: &entity.EntityChain{ - Entities: []*entity.Entity{ - { - Id: "entity-1", - EntityType: &entity.Entity_ClientId{ - ClientId: "opentdf", + decisionReq := &authorizationv2.GetDecisionRequest{ + EntityIdentifier: &authorizationv2.EntityIdentifier{ + Identifier: &authorizationv2.EntityIdentifier_EntityChain{ + EntityChain: &entity.EntityChain{ + Entities: []*entity.Entity{ + { + EphemeralId: "entity-1", + EntityType: &entity.Entity_ClientId{ + ClientId: "opentdf", + }, + }, }, }, - }, }, }, Action: &policy.Action{ Name: "decrypt", }, - Resource: &authorization.Resource{ - AttributeValues: &authorization.Resource_AttributeValues{ - Fqns: []string{"https://opentdf.io/attr/role/value/developer"}, + Resource: &authorizationv2.Resource{ + Resource: &authorizationv2.Resource_AttributeValues_{ + AttributeValues: &authorizationv2.Resource_AttributeValues{ + Fqns: []string{"https://opentdf.io/attr/role/value/developer"}, + }, }, }, } - decision, err := client.Authorization.GetDecision(context.Background(), decisionReq) + decision, err := client.AuthorizationV2.GetDecision(context.Background(), decisionReq) if err != nil { log.Fatal(err) } decisionResult := decision.GetDecision() log.Printf("Decision: %v", decisionResult.GetDecision()) - if decisionResult.GetDecision() == authorization.Decision_DECISION_PERMIT && len(decisionResult.GetObligations()) > 0 { - log.Printf("Obligations: %v", decisionResult.GetObligations()) + if decisionResult.GetDecision() == authorizationv2.Decision_DECISION_PERMIT { + log.Printf("✓ Access GRANTED") + // Note: ResourceDecision doesn't have obligations in v2 API + } +} +``` + +#### V1 API (Legacy) + +```go +package main + +import ( + "context" + "log" + + "github.com/opentdf/platform/protocol/go/authorization" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/sdk" +) + +func main() { + platformEndpoint := "http://localhost:8080" + + // Create a new client + client, err := sdk.New( + platformEndpoint, + sdk.WithClientCredentials("opentdf", "secret", nil), + ) + if err != nil { + log.Fatal(err) + } + + // Get Decision using v1 API (bulk decisions) + decisionRequests := []*authorization.DecisionRequest{{ + Actions: []*policy.Action{{ + Name: "decrypt", + }}, + EntityChains: []*authorization.EntityChain{{ + Id: "ec1", + Entities: []*authorization.Entity{{ + EntityType: &authorization.Entity_ClientId{ + ClientId: "opentdf", + }, + Category: authorization.Entity_CATEGORY_SUBJECT, + }}, + }}, + ResourceAttributes: []*authorization.ResourceAttribute{{ + AttributeValueFqns: []string{"https://opentdf.io/attr/role/value/developer"}, + }}, + }} + + decisionRequest := &authorization.GetDecisionsRequest{ + DecisionRequests: decisionRequests, + } + + decisionResponse, err := client.Authorization.GetDecisions(context.Background(), decisionRequest) + if err != nil { + log.Fatal(err) + } + + for _, dr := range decisionResponse.GetDecisionResponses() { + log.Printf("Decision for entity chain %s: %v", dr.GetEntityChainId(), dr.GetDecision()) + if dr.GetDecision() == authorization.DecisionResponse_DECISION_PERMIT { + log.Printf("✓ Access GRANTED") + if len(dr.GetObligations()) > 0 { + log.Printf("Obligations: %v", dr.GetObligations()) + } + } } } ``` @@ -136,49 +207,63 @@ public class GetDecision { ```javascript -const { AuthzClient } = require('@opentdf/client'); +import { PlatformClient } from '@opentdf/sdk/platform'; +import { AuthProviders } from '@opentdf/sdk'; +import { create } from '@bufbuild/protobuf'; +import { GetDecisionsRequestSchema, DecisionRequestSchema } from '@opentdf/sdk/platform'; async function main() { const platformEndpoint = 'http://localhost:8080'; - const clientId = 'opentdf'; - const clientSecret = 'secret'; - - // Create a new client - const client = new AuthzClient({ - endpoint: platformEndpoint, - auth: { - clientId, - clientSecret - } + + // Assume you have an existing access token + const accessToken = 'your-access-token-here'; + + // Create auth provider with existing token + const authProvider = await AuthProviders.accessTokenAuthProvider({ + accessToken: accessToken + }); + + // Create platform client + const platformClient = new PlatformClient({ + platformUrl: platformEndpoint, + authProvider }); - // Get Decision using v2 API - const request = { - entityIdentifier: { - entityChain: { - entities: [{ - id: 'entity-1', - clientId: 'opentdf' + // Get Decision using v1 API (bulk decisions) + const request = create(GetDecisionsRequestSchema, { + decisionRequests: [ + create(DecisionRequestSchema, { + entityChains: [{ + id: 'ec1', + entities: [{ + id: 'entity-1', + entityType: { + case: 'clientId', + value: 'opentdf' + }, + category: Entity_CategorySchema.SUBJECT + }] + }], + actions: [{ + name: 'decrypt' + }], + resourceAttributes: [{ + resourceAttributesId: 'resource-1', + attributeValueFqns: ['https://opentdf.io/attr/role/value/developer'] }] - } - }, - action: { - name: 'decrypt' - }, - resource: { - attributeValues: { - fqns: ['https://opentdf.io/attr/role/value/developer'] - } - } - }; + }) + ] + }); try { - const response = await client.getDecision(request); + const response = await platformClient.v1.authorization.getDecisions(request); - console.log('Decision:', response.decision.decision); - if (response.decision.decision === 'DECISION_PERMIT' && response.decision.obligations?.length > 0) { - console.log('Obligations:', response.decision.obligations); - } + response.decisionResponses.forEach(decision => { + console.log('Decision:', decision.decision); + if (decision.decision === 'DECISION_PERMIT' && decision.obligations?.length > 0) { + console.log('Obligations:', decision.obligations); + } + }); } catch (error) { console.error('Error:', error); } diff --git a/docs/sdks/authorization.mdx b/docs/sdks/authorization.mdx index e86865d0..cd8e1912 100644 --- a/docs/sdks/authorization.mdx +++ b/docs/sdks/authorization.mdx @@ -38,6 +38,7 @@ import ( "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" @@ -92,15 +93,24 @@ public class AuthorizationSetup { ```javascript -import { SDK } from '@opentdf/client'; +import { PlatformClient } from '@opentdf/sdk/platform'; +import { AuthProviders } from '@opentdf/sdk'; -const sdk = new SDK({ - platformEndpoint: 'http://localhost:8080', - clientId: 'opentdf', - clientSecret: 'secret' +// Assume you have an existing access token +const accessToken = 'your-access-token-here'; + +// Create auth provider with existing token +const authProvider = await AuthProviders.accessTokenAuthProvider({ + accessToken: accessToken +}); + +// Create platform client +const platformClient = new PlatformClient({ + platformUrl: 'http://localhost:8080', + authProvider }); -// SDK is ready for authorization calls +// Client is ready for authorization calls ``` @@ -118,17 +128,21 @@ Use `GetEntitlements` to discover what attribute values an entity can access. Th +#### V2 API (Recommended) + ```go -func getEntitlements(client *sdk.SDK) { +func getEntitlementsV2(client *sdk.SDK) { // Using v2 API with EntityIdentifier - entitlementReq := &authorization.GetEntitlementsRequest{ - EntityIdentifier: &authorization.EntityIdentifier{ - EntityChain: &entity.EntityChain{ - Entities: []*entity.Entity{ - { - Id: "user-bob", - EntityType: &entity.Entity_EmailAddress{ - EmailAddress: "bob@OrgA.com", + entitlementReq := &authorizationv2.GetEntitlementsRequest{ + EntityIdentifier: &authorizationv2.EntityIdentifier{ + Identifier: &authorizationv2.EntityIdentifier_EntityChain{ + EntityChain: &entity.EntityChain{ + Entities: []*entity.Entity{ + { + EphemeralId: "user-bob", + EntityType: &entity.Entity_EmailAddress{ + EmailAddress: "bob@OrgA.com", + }, }, }, }, @@ -136,7 +150,7 @@ func getEntitlements(client *sdk.SDK) { }, } - entitlements, err := client.Authorization.GetEntitlements( + entitlements, err := client.AuthorizationV2.GetEntitlements( context.Background(), entitlementReq, ) @@ -147,7 +161,53 @@ func getEntitlements(client *sdk.SDK) { // Process entitlements for _, entitlement := range entitlements.GetEntitlements() { fmt.Printf("Entity has access to: %v\n", - entitlement.ActionsPerAttributeValueFqn) + entitlement.GetActionsPerAttributeValueFqn()) + } +} +``` + +#### V1 API (Legacy) + +```go +func getEntitlementsV1(client *sdk.SDK) { + // Using v1 API - note: v1 doesn't have GetEntitlements + // Instead, use GetDecisions to understand entity capabilities + 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/classification/value/public", + "https://company.com/attr/classification/value/confidential", + }, + }}, + }} + + decisionRequest := &authorization.GetDecisionsRequest{ + DecisionRequests: decisionRequests, + } + + decisionResponse, err := client.Authorization.GetDecisions( + context.Background(), + decisionRequest, + ) + 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()) } } ``` @@ -189,22 +249,30 @@ public void getEntitlements(SDK sdk) throws ExecutionException, InterruptedExcep ```javascript -async function getEntitlements(sdk) { - const request = { - entityIdentifier: { - entityChain: { - entities: [{ - id: 'user-bob', - emailAddress: 'bob@OrgA.com' - }] - } - } - }; +import { create } from '@bufbuild/protobuf'; +import { GetEntitlementsRequestSchema, EntitySchema, Entity_CategorySchema } from '@opentdf/sdk/platform'; + +async function getEntitlements(platformClient) { + // Assume we have an access token representing the user + const accessToken = 'user-access-token-here'; - const response = await sdk.authorization.getEntitlements(request); + const request = create(GetEntitlementsRequestSchema, { + entities: [ + create(EntitySchema, { + id: 'user-bob', + entityType: { + case: 'emailAddress', + value: 'bob@OrgA.com' + }, + category: Entity_CategorySchema.SUBJECT + }) + ] + }); + + const response = await platformClient.v1.authorization.getEntitlements(request); response.entitlements.forEach(entitlement => { - console.log('Entitled to:', entitlement.actionsPerAttributeValueFqn); + console.log('Entitled to:', entitlement.attributeValueFqns); }); } ``` @@ -316,16 +384,20 @@ Use `GetDecision` when you need to authorize access to specific resources. This +#### V2 API (Recommended) + ```go -func getDecision(client *sdk.SDK) { - decisionReq := &authorization.GetDecisionRequest{ - EntityIdentifier: &authorization.EntityIdentifier{ - EntityChain: &entity.EntityChain{ - Entities: []*entity.Entity{ - { - Id: "user-123", - EntityType: &entity.Entity_EmailAddress{ - EmailAddress: "user@company.com", +func getDecisionV2(client *sdk.SDK) { + decisionReq := &authorizationv2.GetDecisionRequest{ + EntityIdentifier: &authorizationv2.EntityIdentifier{ + Identifier: &authorizationv2.EntityIdentifier_EntityChain{ + EntityChain: &entity.EntityChain{ + Entities: []*entity.Entity{ + { + EphemeralId: "user-123", + EntityType: &entity.Entity_EmailAddress{ + EmailAddress: "user@company.com", + }, }, }, }, @@ -334,17 +406,19 @@ func getDecision(client *sdk.SDK) { Action: &policy.Action{ Name: "decrypt", }, - Resource: &authorization.Resource{ - AttributeValues: &authorization.Resource_AttributeValues{ - Fqns: []string{ - "https://company.com/attr/classification/value/confidential", - "https://company.com/attr/department/value/finance", + Resource: &authorizationv2.Resource{ + Resource: &authorizationv2.Resource_AttributeValues_{ + AttributeValues: &authorizationv2.Resource_AttributeValues{ + Fqns: []string{ + "https://company.com/attr/classification/value/confidential", + "https://company.com/attr/department/value/finance", + }, }, }, }, } - decision, err := client.Authorization.GetDecision( + decision, err := client.AuthorizationV2.GetDecision( context.Background(), decisionReq, ) @@ -353,18 +427,67 @@ func getDecision(client *sdk.SDK) { } resDecision := decision.GetDecision() - if resDecision.GetDecision() == authorization.Decision_DECISION_PERMIT { + if resDecision.GetDecision() == authorizationv2.Decision_DECISION_PERMIT { fmt.Println("Access granted") - // Process any obligations - if len(resDecision.GetObligations()) > 0 { - fmt.Printf("Obligations to fulfill: %v\n", resDecision.GetObligations()) - } + // Note: ResourceDecision doesn't have obligations in v2 API } else { fmt.Println("Access denied") } } ``` +#### 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/classification/value/confidential", + "https://company.com/attr/department/value/finance", + }, + }}, + }} + + decisionRequest := &authorization.GetDecisionsRequest{ + DecisionRequests: decisionRequests, + } + + decisionResponse, err := client.Authorization.GetDecisions( + context.Background(), + decisionRequest, + ) + 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") + } + } +} +``` + @@ -463,35 +586,43 @@ For efficient batch processing, use bulk decision endpoints: +#### V2 API (Recommended) + ```go -func getBulkDecisions(client *sdk.SDK) { - bulkReq := &authorization.GetDecisionBulkRequest{ - DecisionRequests: []*authorization.GetDecisionMultiResourceRequest{ +func getBulkDecisionsV2(client *sdk.SDK) { + bulkReq := &authorizationv2.GetDecisionBulkRequest{ + DecisionRequests: []*authorizationv2.GetDecisionMultiResourceRequest{ { - EntityIdentifier: &authorization.EntityIdentifier{ - EntityChain: &entity.EntityChain{ - Entities: []*entity.Entity{ - { - Id: "user-123", - EntityType: &entity.Entity_EmailAddress{ - EmailAddress: "user@company.com", + EntityIdentifier: &authorizationv2.EntityIdentifier{ + Identifier: &authorizationv2.EntityIdentifier_EntityChain{ + EntityChain: &entity.EntityChain{ + Entities: []*entity.Entity{ + { + EphemeralId: "user-123", + EntityType: &entity.Entity_EmailAddress{ + EmailAddress: "user@company.com", + }, }, }, }, }, }, Action: &policy.Action{Name: "decrypt"}, - Resources: []*authorization.Resource{ + Resources: []*authorizationv2.Resource{ { EphemeralId: "resource-1", - AttributeValues: &authorization.Resource_AttributeValues{ - Fqns: []string{"https://company.com/attr/class/value/public"}, + Resource: &authorizationv2.Resource_AttributeValues_{ + AttributeValues: &authorizationv2.Resource_AttributeValues{ + Fqns: []string{"https://company.com/attr/class/value/public"}, + }, }, }, { - EphemeralId: "resource-2", - AttributeValues: &authorization.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"}, + }, }, }, }, @@ -499,7 +630,7 @@ func getBulkDecisions(client *sdk.SDK) { }, } - decisions, err := client.Authorization.GetDecisionBulk( + decisions, err := client.AuthorizationV2.GetDecisionBulk( context.Background(), bulkReq, ) @@ -513,7 +644,7 @@ func getBulkDecisions(client *sdk.SDK) { fmt.Printf("All resources permitted: %v\n", allPermitted.GetValue()) } for _, resourceDecision := range resp.GetResourceDecisions() { - fmt.Printf("Resource %s: %v\n", + fmt.Printf("Resource %s: %v\n", resourceDecision.GetEphemeralResourceId(), resourceDecision.GetDecision()) } @@ -521,6 +652,55 @@ func getBulkDecisions(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"}, + }, + }, + }} + + decisionRequest := &authorization.GetDecisionsRequest{ + DecisionRequests: decisionRequests, + } + + decisionResponse, err := client.Authorization.GetDecisions( + context.Background(), + decisionRequest, + ) + 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()) + } + } +} +``` + @@ -649,24 +829,30 @@ OpenTDF supports various entity types for flexible authentication: +#### V2 API (Recommended) + ```go -func getDecisionWithToken(client *sdk.SDK, jwtToken string) { - decisionReq := &authorization.GetDecisionRequest{ - EntityIdentifier: &authorization.EntityIdentifier{ - Token: &entity.Token{ - Id: "token-1", - Jwt: jwtToken, +func getDecisionWithTokenV2(client *sdk.SDK, jwtToken string) { + decisionReq := &authorizationv2.GetDecisionRequest{ + EntityIdentifier: &authorizationv2.EntityIdentifier{ + Identifier: &authorizationv2.EntityIdentifier_Token{ + Token: &entity.Token{ + EphemeralId: "token-1", + Jwt: jwtToken, + }, }, }, Action: &policy.Action{Name: "decrypt"}, - Resource: &authorization.Resource{ - AttributeValues: &authorization.Resource_AttributeValues{ - Fqns: []string{"https://company.com/attr/classification/value/public"}, + Resource: &authorizationv2.Resource{ + Resource: &authorizationv2.Resource_AttributeValues_{ + AttributeValues: &authorizationv2.Resource_AttributeValues{ + Fqns: []string{"https://company.com/attr/classification/value/public"}, + }, }, }, } - decision, err := client.Authorization.GetDecision( + decision, err := client.AuthorizationV2.GetDecision( context.Background(), decisionReq, ) @@ -674,7 +860,53 @@ func getDecisionWithToken(client *sdk.SDK, jwtToken string) { log.Fatal(err) } - fmt.Printf("Token-based decision: %v\n", decision.GetDecision().GetDecision()) + 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/classification/value/public"}, + }}, + }} + + decisionRequest := &authorization.GetDecisionsRequest{ + DecisionRequests: decisionRequests, + } + + decisionResponse, err := client.Authorization.GetDecisions( + context.Background(), + decisionRequest, + ) + if err != nil { + log.Fatal(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()) + } + } } ``` From b17b2198187ae65498f813acb8861678522e0030 Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Thu, 14 Aug 2025 21:39:55 -0400 Subject: [PATCH 10/16] fix(docs): Fix authorization SDK examples for API consistency - Fix Go error handling example to use v2 API (authorizationv2.GetDecisionRequest) - Fix Go constant in decision example (authorization.DecisionResponse_DECISION_PERMIT) - Add note explaining getEntitlementsWithScope uses v1 API for WithComprehensiveHierarchy feature - Improve Java example efficiency by storing decision in local variable Addresses critical issues with incorrect Go API types and constants that would cause compilation errors for developers following the documentation. --- docs/sdks/authorization.mdx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/sdks/authorization.mdx b/docs/sdks/authorization.mdx index cd8e1912..ea159be5 100644 --- a/docs/sdks/authorization.mdx +++ b/docs/sdks/authorization.mdx @@ -288,6 +288,7 @@ You can limit entitlement queries to specific attribute hierarchies: ```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{ @@ -524,11 +525,12 @@ public void getDecision(SDK sdk) throws ExecutionException, InterruptedException .getDecision(request) .get(); - if (resp.getDecision().getDecision() == Decision.DECISION_PERMIT) { + Decision decision = resp.getDecision(); + if (decision.getDecision() == Decision.DECISION_PERMIT) { System.out.println("Access granted"); // Process any obligations - if (resp.getDecision().getObligationsCount() > 0) { - System.out.println("Obligations to fulfill: " + resp.getDecision().getObligationsList()); + if (decision.getObligationsCount() > 0) { + System.out.println("Obligations to fulfill: " + decision.getObligationsList()); } } else { System.out.println("Access denied"); @@ -1007,7 +1009,7 @@ func authorizationMiddleware(next http.Handler, sdk *sdk.SDK) http.Handler { // Make authorization decision decision := makeAuthorizationDecision(sdk, entity, "access", resourceAttrs) - if decision == authorization.Decision_DECISION_PERMIT { + if decision == authorization.DecisionResponse_DECISION_PERMIT { next.ServeHTTP(w, r) } else { http.Error(w, "Access denied", http.StatusForbidden) @@ -1024,8 +1026,8 @@ Always implement comprehensive error handling for authorization calls: ```go -func safeAuthorizationCall(client *sdk.SDK, req *authorization.GetDecisionRequest) { - decision, err := client.Authorization.GetDecision(context.Background(), req) +func safeAuthorizationCall(client *sdk.SDK, req *authorizationv2.GetDecisionRequest) { + decision, err := client.AuthorizationV2.GetDecision(context.Background(), req) if err != nil { // Log the error for debugging From 5e28100219c117287167bf6bf4177da403cec0ff Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Thu, 14 Aug 2025 21:49:09 -0400 Subject: [PATCH 11/16] fix(docs): Add missing Entity_CategorySchema import in JavaScript example - Add Entity_CategorySchema to the import statement in get_decision.mdx - Fixes runtime error when Entity_CategorySchema.SUBJECT is used on line 244 - Addresses GitHub PR comment #2278054492 This ensures the JavaScript code sample will run without import errors. --- code_samples/authorization/get_decision.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code_samples/authorization/get_decision.mdx b/code_samples/authorization/get_decision.mdx index b2da40ec..5cc15ac9 100644 --- a/code_samples/authorization/get_decision.mdx +++ b/code_samples/authorization/get_decision.mdx @@ -210,7 +210,7 @@ public class GetDecision { import { PlatformClient } from '@opentdf/sdk/platform'; import { AuthProviders } from '@opentdf/sdk'; import { create } from '@bufbuild/protobuf'; -import { GetDecisionsRequestSchema, DecisionRequestSchema } from '@opentdf/sdk/platform'; +import { GetDecisionsRequestSchema, DecisionRequestSchema, Entity_CategorySchema } from '@opentdf/sdk/platform'; async function main() { const platformEndpoint = 'http://localhost:8080'; From 75b6cc284adb3c318d41b9b94d2de39fb39f3c8e Mon Sep 17 00:00:00 2001 From: Jp Ayyappan <108297634+jp-ayyappan@users.noreply.github.com> Date: Tue, 19 Aug 2025 12:39:49 -0400 Subject: [PATCH 12/16] Update code_samples/authorization/get_decision.mdx resolving comments Co-authored-by: Eugene Yakhnenko --- code_samples/authorization/get_decision.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code_samples/authorization/get_decision.mdx b/code_samples/authorization/get_decision.mdx index 5cc15ac9..139df03f 100644 --- a/code_samples/authorization/get_decision.mdx +++ b/code_samples/authorization/get_decision.mdx @@ -260,7 +260,7 @@ async function main() { response.decisionResponses.forEach(decision => { console.log('Decision:', decision.decision); - if (decision.decision === 'DECISION_PERMIT' && decision.obligations?.length > 0) { + if (decision.decision === DecisionResponse_Decision.PERMIT && decision.obligations?.length > 0) { console.log('Obligations:', decision.obligations); } }); From c518f86fc1f11ee707a08955d02e161566b9ea85 Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Fri, 22 Aug 2025 14:30:24 -0400 Subject: [PATCH 13/16] Updated ts code --- code_samples/authorization/get_decision.mdx | 107 +++++++++++--------- 1 file changed, 60 insertions(+), 47 deletions(-) diff --git a/code_samples/authorization/get_decision.mdx b/code_samples/authorization/get_decision.mdx index 139df03f..8f644b19 100644 --- a/code_samples/authorization/get_decision.mdx +++ b/code_samples/authorization/get_decision.mdx @@ -204,68 +204,81 @@ public class GetDecision { ``` - + -```javascript -import { PlatformClient } from '@opentdf/sdk/platform'; -import { AuthProviders } from '@opentdf/sdk'; -import { create } from '@bufbuild/protobuf'; -import { GetDecisionsRequestSchema, DecisionRequestSchema, Entity_CategorySchema } from '@opentdf/sdk/platform'; +```typescript +import { + DecisionResponse_Decision, + Entity_Category, + type GetDecisionsResponse, +} from "@opentdf/sdk/platform/authorization/authorization_pb.js"; +import { platformConnect, PlatformClient } from "@opentdf/sdk/platform"; async function main() { - const platformEndpoint = 'http://localhost:8080'; - + const platformUrl = "http://localhost:8080"; + // Assume you have an existing access token - const accessToken = 'your-access-token-here'; + const accessToken = "your-refresh-token-here"; - // Create auth provider with existing token - const authProvider = await AuthProviders.accessTokenAuthProvider({ - accessToken: accessToken - }); + const interceptor: platformConnect.Interceptor = (next) => async (req) => { + req.header.set("Authorization", `Bearer ${accessToken}`); + return next(req); + }; - // Create platform client const platformClient = new PlatformClient({ - platformUrl: platformEndpoint, - authProvider + platformUrl: platformUrl, + interceptors: [interceptor], }); // Get Decision using v1 API (bulk decisions) - const request = create(GetDecisionsRequestSchema, { - decisionRequests: [ - create(DecisionRequestSchema, { - entityChains: [{ - id: 'ec1', - entities: [{ - id: 'entity-1', - entityType: { - case: 'clientId', - value: 'opentdf' - }, - category: Entity_CategorySchema.SUBJECT - }] - }], - actions: [{ - name: 'decrypt' - }], - resourceAttributes: [{ - resourceAttributesId: 'resource-1', - attributeValueFqns: ['https://opentdf.io/attr/role/value/developer'] - }] - }) - ] - }); try { - const response = await platformClient.v1.authorization.getDecisions(request); - - response.decisionResponses.forEach(decision => { - console.log('Decision:', decision.decision); - if (decision.decision === DecisionResponse_Decision.PERMIT && decision.obligations?.length > 0) { - console.log('Obligations:', decision.obligations); + const response = (await platformClient.v1.authorization.getDecisions({ + decisionRequests: [ + { + entityChains: [ + { + id: "ec1", + entities: [ + { + id: "entity-1", + entityType: { + case: "clientId", + value: "opentdf", + }, + category: Entity_Category.SUBJECT, + }, + ], + }, + ], + actions: [ + { + name: "decrypt", + }, + ], + resourceAttributes: [ + { + resourceAttributesId: "resource-1", + attributeValueFqns: [ + "https://opentdf.io/attr/role/value/developer", + ], + }, + ], + }, + ], + })) as GetDecisionsResponse; + + response.decisionResponses.forEach((decision) => { + console.log("Decision:", decision.decision); + if ( + decision.decision === DecisionResponse_Decision.PERMIT && + decision.obligations?.length > 0 + ) { + console.log("Obligations:", decision.obligations); } }); } catch (error) { - console.error('Error:', error); + console.error("Error:", error); } } From 9cef6622bd9d7aa6cfc985831400e2178d9c38e7 Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Fri, 22 Aug 2025 23:41:46 -0400 Subject: [PATCH 14/16] Update architecture documentation --- docs/architecture.mdx | 82 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 14 deletions(-) diff --git a/docs/architecture.mdx b/docs/architecture.mdx index 9f893618..180c29c8 100644 --- a/docs/architecture.mdx +++ b/docs/architecture.mdx @@ -4,17 +4,71 @@ sidebar_position: 3 # Architecture -
-
-

Overview

-

The OpenTDF platform is made up of 3 main components:

- -
-
- High Level Architecture -
-
+## Overview + +The OpenTDF platform is made up of 4 main components: + +- **[Policy](components/policy/)** - Manages attribute-based access control (ABAC) policies, including namespaces, attributes, values, and their relationships +- **[Authorization](components/authorization)** - Handles entitlement decisions based on policy evaluation and entity context +- **[Key Access Server (KAS)](components/key_access)** - Manages cryptographic keys and provides secure key access for TDF encryption/decryption +- **[Entity Resolution Service](components/entity_resolution)** - Interfaces with Identity Providers (IdPs) to resolve entity information for authorization decisions + +## High-Level Architecture + +```mermaid +graph TD + %% External Systems + CLIENT["đŸ–Ĩī¸ Client Application"] + IDP["🔐 Identity Provider
(Keycloak, Auth0, etc.)"] + + %% OpenTDF Platform Components + subgraph "OpenTDF Platform" + POLICY["📋 Policy Service
â€ĸ Attribute Management
â€ĸ Subject Mappings
â€ĸ Resource Mappings
â€ĸ Key Access Grants"] + + AUTHZ["âš–ī¸ Authorization Service
â€ĸ Entitlement Decisions
â€ĸ Policy Evaluation
â€ĸ ABAC Enforcement"] + + ERS["đŸ‘Ĩ Entity Resolution
â€ĸ JWT Token Parsing
â€ĸ Entity Chain Creation
â€ĸ IdP Integration"] + + KAS["🔑 Key Access Server
â€ĸ Key Management
â€ĸ TDF Encrypt/Decrypt
â€ĸ Access Control"] + end + + %% TDF Operations + TDF_ENC["đŸ“Ļ TDF Creation
(Encrypt)"] + TDF_DEC["📂 TDF Access
(Decrypt)"] + + %% Flow connections + CLIENT -->|"1. Authenticate"| IDP + CLIENT -->|"2. Create TDF"| TDF_ENC + CLIENT -->|"3. Access TDF"| TDF_DEC + + TDF_ENC -->|"Get Policy & Keys"| POLICY + TDF_ENC -->|"Encrypt with Keys"| KAS + + TDF_DEC -->|"Rewrap Request
+ Access Token"| KAS + KAS -->|"Parse Token
Extract Entities"| ERS + ERS -->|"Query Entity Data"| IDP + KAS -->|"Authorization Request
+ Entity Chain"| AUTHZ + AUTHZ -->|"Get Policies
& Mappings"| POLICY + AUTHZ -->|"Decision"| KAS + KAS -->|"Unwrapped Key
(if authorized)"| TDF_DEC + + %% Styling + classDef platformService fill:#e1f5fe,stroke:#01579b,stroke-width:2px + classDef externalSystem fill:#f3e5f5,stroke:#4a148c,stroke-width:2px + classDef tdfOperation fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px + + class POLICY,AUTHZ,ERS,KAS platformService + class CLIENT,IDP externalSystem + class TDF_ENC,TDF_DEC tdfOperation +``` + +## Component Interactions + +The OpenTDF platform components work together to provide secure, policy-based access to encrypted data: + +1. **Policy Service** defines the rules and attributes that govern access +2. **Entity Resolution Service** translates authentication tokens into entity representations +3. **Authorization Service** evaluates policies against entity context to make access decisions +4. **Key Access Server** enforces those decisions by providing or denying access to decryption keys + +This architecture enables fine-grained, attribute-based access control while maintaining the security and integrity of encrypted data throughout its lifecycle. From b8a2227e252ce5da42927891a3abd219377755b7 Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Mon, 25 Aug 2025 10:58:54 -0400 Subject: [PATCH 15/16] docs(architecture): revise diagram to focus on OpenTDF components The architecture documentation is updated to make the OpenTDF services the primary focus. The diagram and narrative now lead with OpenTDF components, referencing the NIST ABAC model as the underlying framework. This change improves clarity by centering the explanation on the platform's concrete services while still highlighting its standards-based design. --- docs/architecture.mdx | 110 +++++++++++++++++++++++------------------- 1 file changed, 60 insertions(+), 50 deletions(-) diff --git a/docs/architecture.mdx b/docs/architecture.mdx index 180c29c8..90559068 100644 --- a/docs/architecture.mdx +++ b/docs/architecture.mdx @@ -4,71 +4,81 @@ sidebar_position: 3 # Architecture -## Overview +OpenTDF is built on a flexible, service-oriented architecture designed for robust and fine-grained access control. The platform consists of four core components that work together to protect data throughout its lifecycle. This architecture aligns with the well-established National Institute of Standards and Technology (NIST) model for Attribute-Based Access Control (ABAC), ensuring a standards-based and interoperable approach. -The OpenTDF platform is made up of 4 main components: +## Core Platform Components -- **[Policy](components/policy/)** - Manages attribute-based access control (ABAC) policies, including namespaces, attributes, values, and their relationships -- **[Authorization](components/authorization)** - Handles entitlement decisions based on policy evaluation and entity context -- **[Key Access Server (KAS)](components/key_access)** - Manages cryptographic keys and provides secure key access for TDF encryption/decryption -- **[Entity Resolution Service](components/entity_resolution)** - Interfaces with Identity Providers (IdPs) to resolve entity information for authorization decisions - -## High-Level Architecture +The four main services of the OpenTDF platform are the Policy Service, Authorization Service, Entity Resolution Service, and the Key Access Server. ```mermaid graph TD - %% External Systems - CLIENT["đŸ–Ĩī¸ Client Application"] - IDP["🔐 Identity Provider
(Keycloak, Auth0, etc.)"] - - %% OpenTDF Platform Components subgraph "OpenTDF Platform" - POLICY["📋 Policy Service
â€ĸ Attribute Management
â€ĸ Subject Mappings
â€ĸ Resource Mappings
â€ĸ Key Access Grants"] - - AUTHZ["âš–ī¸ Authorization Service
â€ĸ Entitlement Decisions
â€ĸ Policy Evaluation
â€ĸ ABAC Enforcement"] + direction LR - ERS["đŸ‘Ĩ Entity Resolution
â€ĸ JWT Token Parsing
â€ĸ Entity Chain Creation
â€ĸ IdP Integration"] - - KAS["🔑 Key Access Server
â€ĸ Key Management
â€ĸ TDF Encrypt/Decrypt
â€ĸ Access Control"] + subgraph "Policy & Decision" + direction TB + POLICY["đŸĸ Policy Service
(Implements NIST PAP)"] + AUTHZ["🧠 Authorization Service
(Implements NIST PDP)"] + end + + subgraph "Attribute & Enforcement" + direction TB + ERS["â„šī¸ Entity Resolution Service
(Implements NIST PIP)"] + KAS["đŸ›Ąī¸ Key Access Server
(Implements NIST PEP)"] + end end - %% TDF Operations - TDF_ENC["đŸ“Ļ TDF Creation
(Encrypt)"] - TDF_DEC["📂 TDF Access
(Decrypt)"] - - %% Flow connections - CLIENT -->|"1. Authenticate"| IDP - CLIENT -->|"2. Create TDF"| TDF_ENC - CLIENT -->|"3. Access TDF"| TDF_DEC - - TDF_ENC -->|"Get Policy & Keys"| POLICY - TDF_ENC -->|"Encrypt with Keys"| KAS + subgraph "External Systems" + direction TB + ATTR_SOURCES["📚 Optional Attribute Sources
(LDAP, SQL, etc.)"] + IDP["🔐 Identity Provider"] + end + + CLIENT["đŸ–Ĩī¸ Client Application"] + + CLIENT -->|1. Authenticates| IDP + CLIENT -->|2. Access Request| KAS - TDF_DEC -->|"Rewrap Request
+ Access Token"| KAS - KAS -->|"Parse Token
Extract Entities"| ERS - ERS -->|"Query Entity Data"| IDP - KAS -->|"Authorization Request
+ Entity Chain"| AUTHZ - AUTHZ -->|"Get Policies
& Mappings"| POLICY - AUTHZ -->|"Decision"| KAS - KAS -->|"Unwrapped Key
(if authorized)"| TDF_DEC + KAS -->|3. Decision Request| AUTHZ + AUTHZ -->|4. Get Attributes| ERS + AUTHZ -->|5. Get Policies| POLICY + ERS -->|6. Optionally Query Attributes| ATTR_SOURCES - %% Styling - classDef platformService fill:#e1f5fe,stroke:#01579b,stroke-width:2px + AUTHZ -->|7. Decision| KAS + KAS -->|8. Grant/Deny Access| CLIENT + + classDef opentdfService fill:#e1f5fe,stroke:#01579b,stroke-width:2px classDef externalSystem fill:#f3e5f5,stroke:#4a148c,stroke-width:2px - classDef tdfOperation fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px - class POLICY,AUTHZ,ERS,KAS platformService - class CLIENT,IDP externalSystem - class TDF_ENC,TDF_DEC tdfOperation + class POLICY,AUTHZ,ERS,KAS opentdfService + class ATTR_SOURCES,IDP,CLIENT externalSystem ``` -## Component Interactions +### Policy Service + +The **Policy Service** is where all access control policies are defined and managed. It provides the tools and APIs to create a rich set of policies that govern data access. This includes not only attributes and their values, but also the definitions of **actions, obligations, and key access mappings**. This rich policy information allows for fine-grained access control that goes beyond a simple PERMIT/DENY decision. + +In the context of the NIST ABAC model, the Policy Service functions as the **Policy Administration Point (PAP)**. + +### Authorization Service + +The **Authorization Service** is the core decision-making engine of the platform. It is responsible for evaluating the rich policies from the Policy Service against a set of attributes to render an authorization decision. + +In the context of the NIST ABAC model, the Authorization Service functions as the **Policy Decision Point (PDP)**. + +### Entity Resolution Service (ERS) + +The **Entity Resolution Service** is responsible for gathering the attributes about a subject that are needed to make an access control decision. By default, the ERS can derive these attributes directly from the claims present in an authentication token (e.g., a JWT) after the subject has been authenticated by an Identity Provider. For more advanced use cases, the ERS can be **optionally configured** to connect to external attribute sources, like LDAP directories or SQL databases, to "hydrate" the entity with additional attributes. + +In the context of the NIST ABAC model, the ERS functions as the **Policy Information Point (PIP)**. + +### Key Access Server (KAS) + +The **Key Access Server (KAS)** is responsible for enforcing access control decisions. Its role, however, is more extensive than that of a typical enforcement point: -The OpenTDF platform components work together to provide secure, policy-based access to encrypted data: +- **Cryptographic Enforcement:** Instead of just allowing or denying an action, the KAS enforces decisions by granting or withholding the cryptographic keys required to decrypt a TDF object. This provides a powerful, cryptographically-secure method of enforcement. +- **Encryption Enablement:** The KAS is a crucial part of the data protection lifecycle. It manages key exchanges and enables the various modes of TDF encryption (e.g., NanoTDF). -1. **Policy Service** defines the rules and attributes that govern access -2. **Entity Resolution Service** translates authentication tokens into entity representations -3. **Authorization Service** evaluates policies against entity context to make access decisions -4. **Key Access Server** enforces those decisions by providing or denying access to decryption keys +In the context of the NIST ABAC model, the KAS functions as the **Policy Enforcement Point (PEP)**. -This architecture enables fine-grained, attribute-based access control while maintaining the security and integrity of encrypted data throughout its lifecycle. +Furthermore, the OpenTDF platform is designed for flexibility. Developers can **build and integrate their own custom PEPs**. These custom enforcement points can leverage the platform's robust Authorization (PDP) and Policy (PAP) services while implementing enforcement logic tailored to specific applications. These custom PEPs can also optionally interface with the KAS to take advantage of its powerful cryptographic capabilities. \ No newline at end of file From 057617d789f908995d410754416c6eb3c1cc856d Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Mon, 25 Aug 2025 12:51:52 -0400 Subject: [PATCH 16/16] feat(docs): overhaul architecture page for clarity and usability This commit completely revises the architecture documentation with a new structure and improved content based on extensive feedback. The key changes include: - **Narrative Focus:** The document is now centered on the OpenTDF components, with the NIST ABAC model used as supporting context rather than the primary structure. - **New Diagram:** A new, portrait-oriented Mermaid diagram is used to improve readability on standard screens. - **Clickable Elements:** The nodes in the Mermaid diagram and the section headings for each component are now linked to their respective documentation pages for easier navigation. - **External Links:** The text now includes links to the NIST homepage and their official guide on ABAC for further reading. - **Layout:** The page now uses a single-column layout with the diagram presented above the explanatory text. --- docs/architecture.mdx | 62 +++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/docs/architecture.mdx b/docs/architecture.mdx index 90559068..15655104 100644 --- a/docs/architecture.mdx +++ b/docs/architecture.mdx @@ -4,7 +4,7 @@ sidebar_position: 3 # Architecture -OpenTDF is built on a flexible, service-oriented architecture designed for robust and fine-grained access control. The platform consists of four core components that work together to protect data throughout its lifecycle. This architecture aligns with the well-established National Institute of Standards and Technology (NIST) model for Attribute-Based Access Control (ABAC), ensuring a standards-based and interoperable approach. +OpenTDF is built on a flexible, service-oriented architecture designed for robust and fine-grained access control. The platform consists of four core components that work together to protect data throughout its lifecycle. This architecture aligns with the well-established [National Institute of Standards and Technology (NIST)](https://www.nist.gov) model for [Attribute-Based Access Control (ABAC)](https://csrc.nist.gov/projects/attribute-based-access-control), ensuring a standards-based and interoperable approach. ## Core Platform Components @@ -12,39 +12,32 @@ The four main services of the OpenTDF platform are the Policy Service, Authoriza ```mermaid graph TD + CLIENT["đŸ–Ĩī¸ Client Application"] + subgraph "OpenTDF Platform" - direction LR - - subgraph "Policy & Decision" - direction TB - POLICY["đŸĸ Policy Service
(Implements NIST PAP)"] - AUTHZ["🧠 Authorization Service
(Implements NIST PDP)"] - end - - subgraph "Attribute & Enforcement" - direction TB - ERS["â„šī¸ Entity Resolution Service
(Implements NIST PIP)"] - KAS["đŸ›Ąī¸ Key Access Server
(Implements NIST PEP)"] - end + KAS["đŸ›Ąī¸ Key Access Server
(Implements NIST PEP)"] + AUTHZ["🧠 Authorization Service
(Implements NIST PDP)"] + ERS["â„šī¸ Entity Resolution Service
(Implements NIST PIP)"] + POLICY["đŸĸ Policy Service
(Implements NIST PAP)"] end - + subgraph "External Systems" - direction TB - ATTR_SOURCES["📚 Optional Attribute Sources
(LDAP, SQL, etc.)"] IDP["🔐 Identity Provider"] + ATTR_SOURCES["📚 Optional Attribute Sources
(LDAP, SQL, etc.)"] end - CLIENT["đŸ–Ĩī¸ Client Application"] - CLIENT -->|1. Authenticates| IDP CLIENT -->|2. Access Request| KAS KAS -->|3. Decision Request| AUTHZ - AUTHZ -->|4. Get Attributes| ERS - AUTHZ -->|5. Get Policies| POLICY - ERS -->|6. Optionally Query Attributes| ATTR_SOURCES + + AUTHZ -->|4. Get Policies| POLICY + AUTHZ -->|5. Get Attributes| ERS + + ERS -->|6. Optionally Query| ATTR_SOURCES AUTHZ -->|7. Decision| KAS + KAS -->|8. Grant/Deny Access| CLIENT classDef opentdfService fill:#e1f5fe,stroke:#01579b,stroke-width:2px @@ -52,32 +45,37 @@ graph TD class POLICY,AUTHZ,ERS,KAS opentdfService class ATTR_SOURCES,IDP,CLIENT externalSystem + + click POLICY "components/policy/" "Go to Policy Service docs" + click AUTHZ "components/authorization" "Go to Authorization Service docs" + click ERS "components/entity_resolution" "Go to Entity Resolution Service docs" + click KAS "components/key_access" "Go to Key Access Server docs" ``` -### Policy Service +### [Policy Service](components/policy/) -The **Policy Service** is where all access control policies are defined and managed. It provides the tools and APIs to create a rich set of policies that govern data access. This includes not only attributes and their values, but also the definitions of **actions, obligations, and key access mappings**. This rich policy information allows for fine-grained access control that goes beyond a simple PERMIT/DENY decision. +The **Policy Service** is where all access control policies are defined and managed. It provides the tools and APIs to create a rich set of policies that govern data access. This includes not only attributes and their values, but also the definitions of **actions, obligations, and key access mappings**. In the context of the NIST ABAC model, the Policy Service functions as the **Policy Administration Point (PAP)**. -### Authorization Service +### [Authorization Service](components/authorization) The **Authorization Service** is the core decision-making engine of the platform. It is responsible for evaluating the rich policies from the Policy Service against a set of attributes to render an authorization decision. -In the context of the NIST ABAC model, the Authorization Service functions as the **Policy Decision Point (PDP)**. +In the context of the NIST ABAC model, it functions as the **Policy Decision Point (PDP)**. -### Entity Resolution Service (ERS) +### [Entity Resolution Service (ERS)](components/entity_resolution) -The **Entity Resolution Service** is responsible for gathering the attributes about a subject that are needed to make an access control decision. By default, the ERS can derive these attributes directly from the claims present in an authentication token (e.g., a JWT) after the subject has been authenticated by an Identity Provider. For more advanced use cases, the ERS can be **optionally configured** to connect to external attribute sources, like LDAP directories or SQL databases, to "hydrate" the entity with additional attributes. +The **Entity Resolution Service** is responsible for gathering the attributes about a subject needed for a decision. By default, it can derive attributes from claims in an authentication token. Optionally, it can be configured to connect to external attribute sources (LDAP, SQL) to "hydrate" the entity with more attributes. In the context of the NIST ABAC model, the ERS functions as the **Policy Information Point (PIP)**. -### Key Access Server (KAS) +### [Key Access Server (KAS)](components/key_access) -The **Key Access Server (KAS)** is responsible for enforcing access control decisions. Its role, however, is more extensive than that of a typical enforcement point: +The **Key Access Server (KAS)** enforces access control decisions. Its role is more extensive than a typical enforcement point: -- **Cryptographic Enforcement:** Instead of just allowing or denying an action, the KAS enforces decisions by granting or withholding the cryptographic keys required to decrypt a TDF object. This provides a powerful, cryptographically-secure method of enforcement. -- **Encryption Enablement:** The KAS is a crucial part of the data protection lifecycle. It manages key exchanges and enables the various modes of TDF encryption (e.g., NanoTDF). +- **Cryptographic Enforcement:** It enforces decisions by granting or withholding cryptographic keys for TDF decryption. +- **Encryption Enablement:** It manages key exchanges and enables various TDF encryption modes. In the context of the NIST ABAC model, the KAS functions as the **Policy Enforcement Point (PEP)**.