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 ee1cb4f9..8f644b19 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,66 +15,129 @@ 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:9002" + 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 Entitlements - - decision := &authorization.GetDecisionsRequest{ - DecisionRequests: []*authorization.DecisionRequest{ - { - Actions: []*policy.Action{ - { - Value: &policy.Action_Standard{ - Standard: policy.Action_STANDARD_ACTION_DECRYPT, - }, - }, - }, - EntityChains: []*authorization.EntityChain{ - { - Id: "entity-chain-1", - Entities: []*authorization.Entity{ - { - Id: "entity-1", - EntityType: &authorization.Entity_ClientId{ - ClientId: "opentdf", - }, + // Get Decision using v2 API + 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", }, }, + }, }, - }, - ResourceAttributes: []*authorization.ResourceAttribute{ - { - ResourceAttributesId: "resource-attribute-1", - AttributeValueFqns: []string{"https://opentdf.io/attr/role/value/developer"}, - }, + }, + }, + Action: &policy.Action{ + Name: "decrypt", + }, + Resource: &authorizationv2.Resource{ + Resource: &authorizationv2.Resource_AttributeValues_{ + AttributeValues: &authorizationv2.Resource_AttributeValues{ + Fqns: []string{"https://opentdf.io/attr/role/value/developer"}, }, }, }, } - decisions, err := client.Authorization.GetDecisions(context.Background(), decision) + 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() == 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) } - log.Printf("Decisions: %v", decisions.GetDecisionResponses()) + 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()) + } + } + } } ``` @@ -86,43 +151,138 @@ 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"; String clientSecret = "secret"; - String platformEndpoint = "localhost:8080"; + String platformEndpoint = "http://localhost:8080"; SDKBuilder builder = new SDKBuilder(); SDK sdk = builder.platformEndpoint(platformEndpoint) .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(); + // 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(); - List decisions = resp.getDecisionResponsesList(); + GetDecisionResponse resp = sdk.getServices().authorization().getDecision(request).get(); - System.out.println(DecisionResponse.Decision.forNumber(decisions.get(0).getDecisionValue())); + 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()); + } } } ``` - + + +```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 platformUrl = "http://localhost:8080"; + + // Assume you have an existing access token + const accessToken = "your-refresh-token-here"; + + const interceptor: platformConnect.Interceptor = (next) => async (req) => { + req.header.set("Authorization", `Bearer ${accessToken}`); + return next(req); + }; + + const platformClient = new PlatformClient({ + platformUrl: platformUrl, + interceptors: [interceptor], + }); + + // Get Decision using v1 API (bulk decisions) + + try { + 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); + } +} -```javascript +main(); ``` diff --git a/code_samples/authorization/get_entitlements.mdx b/code_samples/authorization/get_entitlements.mdx index 3b676946..e536918d 100644 --- a/code_samples/authorization/get_entitlements.mdx +++ b/code_samples/authorization/get_entitlements.mdx @@ -14,12 +14,13 @@ import ( "log" "github.com/opentdf/platform/protocol/go/authorization" + "github.com/opentdf/platform/protocol/go/entity" "github.com/opentdf/platform/sdk" ) func main() { - platformEndpoint := "http://localhost:9002" + platformEndpoint := "http://localhost:8080" // Create a new client client, err := sdk.New( @@ -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", + }, + }, }, }, }, @@ -63,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; @@ -71,22 +76,35 @@ 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) .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/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 05f9e71c..5b42ce8c 100644 --- a/code_samples/tdf/encryption_ztdf.mdx +++ b/code_samples/tdf/encryption_ztdf.mdx @@ -92,7 +92,6 @@ import java.nio.channels.FileChannel; import java.nio.file.Path; public class EncryptExample { - public static void main(String[] args) { try { System.out.println("🚀 Starting OpenTDF example..."); diff --git a/docs/architecture.mdx b/docs/architecture.mdx index 9f893618..15655104 100644 --- a/docs/architecture.mdx +++ b/docs/architecture.mdx @@ -4,17 +4,79 @@ sidebar_position: 3 # Architecture -
-
-

Overview

-

The OpenTDF platform is made up of 3 main components:

- -
-
- High Level 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)](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 + +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 + CLIENT["đŸ–Ĩī¸ Client Application"] + + subgraph "OpenTDF Platform" + 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" + IDP["🔐 Identity Provider"] + ATTR_SOURCES["📚 Optional Attribute Sources
(LDAP, SQL, etc.)"] + end + + CLIENT -->|1. Authenticates| IDP + CLIENT -->|2. Access Request| KAS + + KAS -->|3. Decision Request| AUTHZ + + 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 + classDef externalSystem fill:#f3e5f5,stroke:#4a148c,stroke-width:2px + + 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](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**. + +In the context of the NIST ABAC model, the Policy Service functions as the **Policy Administration Point (PAP)**. + +### [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, it functions as the **Policy Decision Point (PDP)**. + +### [Entity Resolution Service (ERS)](components/entity_resolution) + +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)](components/key_access) + +The **Key Access Server (KAS)** enforces access control decisions. Its role is more extensive than a typical enforcement point: + +- **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)**. + +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 diff --git a/docs/sdks/authorization.mdx b/docs/sdks/authorization.mdx index b350b49e..ea159be5 100644 --- a/docs/sdks/authorization.mdx +++ b/docs/sdks/authorization.mdx @@ -2,12 +2,1060 @@ 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" + "fmt" + "log" + + "github.com/opentdf/platform/protocol/go/authorization" + authorizationv2 "github.com/opentdf/platform/protocol/go/authorization/v2" + "github.com/opentdf/platform/protocol/go/entity" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/sdk" + "google.golang.org/protobuf/proto" +) + +func main() { + platformEndpoint := "http://localhost:8080" + + // Create authenticated client + client, err := sdk.New( + platformEndpoint, + sdk.WithClientCredentials("opentdf", "secret", nil), + ) + if err != nil { + log.Fatal(err) + } + + // Client is ready for authorization calls +} +``` + + + + +```java +import io.opentdf.platform.sdk.*; +import io.opentdf.platform.authorization.*; +import io.opentdf.platform.entity.*; +import io.opentdf.platform.policy.*; +import java.util.List; +import java.util.concurrent.ExecutionException; + +public class AuthorizationSetup { + public static void main(String[] args) { + String clientId = "opentdf"; + String clientSecret = "secret"; + String platformEndpoint = "http://localhost:8080"; + + SDKBuilder builder = new SDKBuilder(); + SDK sdk = builder.platformEndpoint(platformEndpoint) + .clientSecret(clientId, clientSecret) + .useInsecurePlaintextConnection(true) + .build(); + + // SDK is ready for authorization calls + } +} +``` + + + + +```javascript +import { PlatformClient } from '@opentdf/sdk/platform'; +import { AuthProviders } from '@opentdf/sdk'; + +// 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 +}); + +// Client 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 + + + + +#### V2 API (Recommended) + +```go +func getEntitlementsV2(client *sdk.SDK) { + // Using v2 API with EntityIdentifier + 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", + }, + }, + }, + }, + }, + }, + } + + entitlements, err := client.AuthorizationV2.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.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()) + } +} +``` + + + + +```java +public void getEntitlements(SDK sdk) throws ExecutionException, InterruptedException { + GetEntitlementsRequest request = GetEntitlementsRequest.newBuilder() + .setEntityIdentifier( + EntityIdentifier.newBuilder() + .setEntityChain( + EntityChain.newBuilder() + .addEntities( + Entity.newBuilder() + .setId("user-bob") + .setEmailAddress("bob@OrgA.com") + ) + ) + ) + .build(); + + GetEntitlementsResponse resp = sdk.getServices() + .authorization() + .getEntitlements(request) + .get(); + + List entitlements = resp.getEntitlementsList(); + + for (EntityEntitlements entitlement : entitlements) { + System.out.println("Entitled to: " + + entitlement.getActionsPerAttributeValueFqnMap()); + } +} +``` + + + + +```javascript +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 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.attributeValueFqns); + }); +} +``` + + + + +### Entitlements with Scope + +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{ + EntityChain: &entity.EntityChain{ + Entities: []*entity.Entity{ + { + Id: "user-123", + EntityType: &entity.Entity_EmailAddress{ + EmailAddress: "user@company.com", + }, + }, + }, + }, + }, + // When true, returns all entitled values for attributes with hierarchy rules, propagating down from the entitled value + WithComprehensiveHierarchy: proto.Bool(true), + } + + entitlements, err := client.Authorization.GetEntitlements( + context.Background(), + entitlementReq, + ) + if err != nil { + log.Fatal(err) + } + + log.Printf("Scoped entitlements: %v", entitlements.GetEntitlements()) +} +``` + + + + +```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); +} +``` + + + + +## 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 + + + + +#### V2 API (Recommended) + +```go +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", + }, + }, + }, + }, + }, + }, + Action: &policy.Action{ + Name: "decrypt", + }, + 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.AuthorizationV2.GetDecision( + context.Background(), + decisionReq, + ) + if err != nil { + log.Fatal(err) + } + + resDecision := decision.GetDecision() + if resDecision.GetDecision() == authorizationv2.Decision_DECISION_PERMIT { + fmt.Println("Access granted") + // 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") + } + } +} +``` + + + + +```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(); + + Decision decision = resp.getDecision(); + if (decision.getDecision() == Decision.DECISION_PERMIT) { + System.out.println("Access granted"); + // Process any obligations + if (decision.getObligationsCount() > 0) { + System.out.println("Obligations to fulfill: " + decision.getObligationsList()); + } + } else { + System.out.println("Access denied"); + } +} +``` + + + + +```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: + + + + +#### V2 API (Recommended) + +```go +func getBulkDecisionsV2(client *sdk.SDK) { + bulkReq := &authorizationv2.GetDecisionBulkRequest{ + DecisionRequests: []*authorizationv2.GetDecisionMultiResourceRequest{ + { + 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: []*authorizationv2.Resource{ + { + EphemeralId: "resource-1", + Resource: &authorizationv2.Resource_AttributeValues_{ + AttributeValues: &authorizationv2.Resource_AttributeValues{ + Fqns: []string{"https://company.com/attr/class/value/public"}, + }, + }, + }, + { + EphemeralId: "resource-2", + Resource: &authorizationv2.Resource_AttributeValues_{ + AttributeValues: &authorizationv2.Resource_AttributeValues{ + Fqns: []string{"https://company.com/attr/class/value/confidential"}, + }, + }, + }, + }, + }, + }, + } + + decisions, err := client.AuthorizationV2.GetDecisionBulk( + context.Background(), + bulkReq, + ) + if err != nil { + log.Fatal(err) + } + + for _, resp := range decisions.GetDecisionResponses() { + allPermitted := resp.GetAllPermitted() + if allPermitted != nil { + fmt.Printf("All resources permitted: %v\n", allPermitted.GetValue()) + } + for _, resourceDecision := range resp.GetResourceDecisions() { + fmt.Printf("Resource %s: %v\n", + resourceDecision.GetEphemeralResourceId(), + resourceDecision.GetDecision()) + } + } +} +``` + +#### 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()) + } + } +} +``` + + + + +```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}`); + }); + }); +} +``` + + + + +## 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 + + + + +#### V2 API (Recommended) + +```go +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: &authorizationv2.Resource{ + Resource: &authorizationv2.Resource_AttributeValues_{ + AttributeValues: &authorizationv2.Resource_AttributeValues{ + Fqns: []string{"https://company.com/attr/classification/value/public"}, + }, + }, + }, + } + + decision, err := client.AuthorizationV2.GetDecision( + context.Background(), + decisionReq, + ) + if err != nil { + log.Fatal(err) + } + + resDecision := decision.GetDecision() + fmt.Printf("Token-based decision: %v\n", resDecision.GetDecision()) +} +``` + +#### V1 API (Legacy) + +```go +func getDecisionWithTokenV1(client *sdk.SDK, jwtToken string) { + // V1 API uses bulk decisions with token entity + decisionRequests := []*authorization.DecisionRequest{{ + Actions: []*policy.Action{{Name: "decrypt"}}, + EntityChains: []*authorization.EntityChain{{ + Id: "token-chain", + Entities: []*authorization.Entity{{ + EntityType: &authorization.Entity_Token{ + Token: &entity.Token{ + EphemeralId: "token-1", + Jwt: jwtToken, + }, + }, + Category: authorization.Entity_CATEGORY_SUBJECT, + }}, + }}, + ResourceAttributes: []*authorization.ResourceAttribute{{ + AttributeValueFqns: []string{"https://company.com/attr/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()) + } + } +} +``` + + + + +```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); +} +``` + + + + +## 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.DecisionResponse_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, req *authorizationv2.GetDecisionRequest) { + decision, err := client.AuthorizationV2.GetDecision(context.Background(), req) + + if err != nil { + // Log the error for debugging + log.Printf("Authorization error: %v", err) + + // Implement your fallback policy. Choose one of the options below. + + // Option 1: Deny by default (more secure) + handleAccessDenied() + return + + /* + // Option 2: Allow by default (less secure, only for non-critical resources) + handleAccessAllowed() + return + */ + + /* + // Option 3: Retry with exponential backoff + retryWithBackoff(client, req) + return + */ + } + + // Process successful response + handleDecisionResponse(decision) +} +``` - \ No newline at end of file + +