From f2bd649f3b8290790be4fa13ceb6991485624b3e Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Mon, 9 Feb 2026 13:40:24 -0800 Subject: [PATCH 01/13] feat: DSPX-2416 add subject mapping deep dive to new Guides section Signed-off-by: Mary Dickson --- docs/components/policy/subject_mappings.md | 4 + docs/guides/_category_.json | 8 + docs/guides/subject-mapping-guide.md | 1305 ++++++++++++++++++++ 3 files changed, 1317 insertions(+) create mode 100644 docs/guides/_category_.json create mode 100644 docs/guides/subject-mapping-guide.md diff --git a/docs/components/policy/subject_mappings.md b/docs/components/policy/subject_mappings.md index 5174b0b1..92dfc50a 100644 --- a/docs/components/policy/subject_mappings.md +++ b/docs/components/policy/subject_mappings.md @@ -1,5 +1,9 @@ # Subject Mappings +:::tip New to Subject Mappings? +For a comprehensive tutorial with IdP integration examples, troubleshooting, and step-by-step guides, see the [Subject Mapping Comprehensive Guide](/guides/subject-mapping-guide). +::: + As data is bound to fully qualified Attribute Values when encrypted within a TDF, entities are associated with Attribute values through a mechanism called Subject Mappings. Entities (subjects, users, machines, etc.) are represented by their identity as determined from an identity provider (IdP). After an entity has securely authenticated with the IdP, the client's token (OIDC/OAUTH2) will include claims or attributes that describe that identity. Subject Mappings define how to map these identity attributes to actions on attribute values defined in the OpenTDF platform Policy. For more details on how the platform integrates with the IdP and how entities are resolved, refer to the [Authorization documentation](../authorization). diff --git a/docs/guides/_category_.json b/docs/guides/_category_.json new file mode 100644 index 00000000..a432091d --- /dev/null +++ b/docs/guides/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Guides", + "position": 3, + "link": { + "type": "generated-index", + "description": "Comprehensive guides and tutorials for implementing OpenTDF features." + } +} diff --git a/docs/guides/subject-mapping-guide.md b/docs/guides/subject-mapping-guide.md new file mode 100644 index 00000000..0c860d55 --- /dev/null +++ b/docs/guides/subject-mapping-guide.md @@ -0,0 +1,1305 @@ +--- +title: Subject Mapping Comprehensive Guide +sidebar_position: 1 +--- + +# Subject Mapping: Comprehensive Guide + +:::info What You'll Learn +This guide explains how OpenTDF connects user identities from your Identity Provider (IdP) to attribute-based access control. You'll understand: +- **Why** Subject Mappings exist (vs. direct IdP attribute mapping) +- **How** authentication flows through Entity Resolution to authorization decisions +- **When** to use enumerated vs. dynamic attribute values +- **How to troubleshoot** common Subject Mapping errors +::: + +## The Core Problem: Why Subject Mappings Exist + +### ❌ Common Misconception + +Many developers expect this direct flow: + +``` +IdP User Attribute → OpenTDF Attribute → Access Decision + (role=admin) (clearance=top_secret) (PERMIT/DENY) +``` + +**This doesn't work.** OpenTDF attributes define **what can be accessed**, not **who can access it**. + +### ✅ How It Actually Works + +OpenTDF uses a three-layer architecture: + +``` +1. IdP Attributes → User has role=admin, department=finance + (Identity Claims) + +2. Subject Mappings → IF role=admin THEN grant clearance/top_secret + (Entitlement Rules) + +3. OpenTDF Attributes → Document requires clearance/top_secret + (Resource Protection) +``` + +**Subject Mappings** are the bridge: they convert **identity claims** into **access entitlements**. + +:::tip Key Insight +- **IdP attributes** describe WHO the user is +- **Subject Mappings** determine WHAT the user can access +- **OpenTDF attributes** define WHAT protects the resource + +Subject Mappings answer: "Given this identity, what entitlements should they receive?" +::: + +## Architecture: The Complete Flow + +### High-Level Data Flow + +```mermaid +sequenceDiagram + participant User + participant IdP as Identity Provider
(Keycloak/Auth0/Okta) + participant ERS as Entity Resolution
Service + participant Auth as Authorization
Service + participant Policy as Policy
Service + + User->>IdP: 1. Authenticate + IdP->>User: 2. Return JWT token
{email, role, groups} + + User->>ERS: 3. Decrypt request + token + ERS->>ERS: 4. Parse token into
Entity Representation + + ERS->>Auth: 5. GetEntitlements(entity) + Auth->>Policy: 6. Which Subject Mappings
match this entity? + + Policy->>Policy: 7. Evaluate Subject
Condition Sets + Policy->>Auth: 8. Return entitled
attribute values + actions + + Auth->>Auth: 9. Compare entitlements
vs resource attributes + Auth->>User: 10. PERMIT or DENY +``` + +### Detailed Step-by-Step + +#### Step 1-2: User Authentication +```json +// User authenticates with IdP, receives JWT token +{ + "sub": "alice@example.com", + "email": "alice@example.com", + "role": "vice_president", + "department": "finance", + "groups": ["executives", "finance-team"] +} +``` + +#### Step 3-4: Entity Resolution +The token is parsed into an **Entity Representation** - a normalized view of the user's identity: + +```json +{ + "ephemeral_id": "jwtentity-1", + "category": "CATEGORY_SUBJECT", + "claims": { + "email": "alice@example.com", + "role": "vice_president", + "department": "finance", + "groups": ["executives", "finance-team"] + } +} +``` + +:::warning Entity Types Confusion +You may see TWO entities in authorization logs: +- `jwtentity-0`: The **client application** (NPE - Non-Person Entity) +- `jwtentity-1`: The **user** (PE - Person Entity) + +**Both need Subject Mappings** if both need attribute access. For SDK decryption, typically the client (`jwtentity-0`) needs mappings based on `.clientId`. +::: + +#### Step 5-8: Subject Mapping Evaluation + +The Authorization Service queries the Policy Service: "Which Subject Mappings apply to this entity?" + +**Subject Mapping Example:** +```json +{ + "id": "sm-001", + "attribute_value_id": "attr-clearance-executive", + "actions": ["STANDARD_ACTION_DECRYPT"], + "subject_condition_set": { + "subject_sets": [{ + "condition_groups": [{ + "boolean_operator": "AND", + "conditions": [{ + "subject_external_selector_value": ".role", + "operator": "IN", + "subject_external_values": ["vice_president", "ceo", "cfo"] + }] + }] + }] + } +} +``` + +**Evaluation Logic:** +1. Extract `.role` from entity representation → `"vice_president"` +2. Check if `"vice_president"` is IN `["vice_president", "ceo", "cfo"]` → ✅ TRUE +3. Grant entitlement: `clearance/executive` with `DECRYPT` action + +#### Step 9-10: Authorization Decision + +```json +// Entity's Entitlements: +{ + "attribute_values": [ + { + "attribute": "https://example.com/attr/clearance/value/executive", + "actions": ["DECRYPT"] + } + ] +} + +// Resource Requirements: +{ + "attributes": [ + "https://example.com/attr/clearance/value/executive" + ] +} + +// Decision: PERMIT (entitlements satisfy requirements) +``` + +## Entity Types and Categories + +Understanding entity types is critical for Subject Mapping configuration. + +### Entity Type vs. Entity Category + +| Dimension | Options | Meaning | +|-----------|---------|---------| +| **Entity Type** | PE (Person)
NPE (Non-Person) | WHO the entity is | +| **Entity Category** | Subject
Environment | HOW it's used in decisions | + +### The Four Combinations + +```mermaid +graph TD + A[Entity] --> B{Entity Type} + B -->|PE| C[Person Entity] + B -->|NPE| D[Non-Person Entity] + + C --> E{Category} + D --> F{Category} + + E -->|Subject| G[PE + Subject
Human user in auth flow
✅ Attributes checked] + E -->|Environment| H[PE + Environment
Logged-in operator
⚠️ Always PERMIT] + + F -->|Subject| I[NPE + Subject
Service account, client app
✅ Attributes checked] + F -->|Environment| J[NPE + Environment
System component
⚠️ Always PERMIT] +``` + +### Practical Examples + +**Person Entity + Subject Category (Most Common)** +```json +{ + "type": "PE", + "category": "CATEGORY_SUBJECT", + "claims": { + "email": "alice@example.com", + "role": "engineer" + } +} +``` +→ Subject Mapping checks: Does Alice have the right entitlements? + +**Non-Person Entity + Subject Category (SDK Clients)** +```json +{ + "type": "NPE", + "category": "CATEGORY_SUBJECT", + "claims": { + "clientId": "data-processing-service", + "scope": "tdf:decrypt" + } +} +``` +→ Subject Mapping checks: Does this service account have the right entitlements? + +**Environment Category (Auto-PERMIT)** +```json +{ + "type": "NPE", + "category": "CATEGORY_ENVIRONMENT", + "claims": { + "systemComponent": "internal-backup-job" + } +} +``` +→ **No attribute checking**. Always returns PERMIT. Use for trusted system components. + +:::danger Security Warning +Environment entities **bypass all attribute checks**. Only use for fully trusted system components that should always have access (e.g., backup services, monitoring tools). +::: + +## Subject Condition Sets: The Matching Engine + +A **Subject Condition Set** is a logical expression that evaluates an entity representation to `true` or `false`. + +### Structure Hierarchy + +``` +SubjectConditionSet + └─ SubjectSets[] (OR'd together - ANY set can match) + └─ ConditionGroups[] (Combined by boolean operator) + └─ Conditions[] (Combined by boolean operator) + ├─ SubjectExternalSelectorValue (JMESPath to extract claim) + ├─ Operator (IN, NOT_IN, IN_CONTAINS) + └─ SubjectExternalValues (Values to match) +``` + +### Operators Explained + +| Operator | Value | Behavior | Example | +|----------|-------|----------|---------| +| **IN** | `1` | Exact match: value is IN list | `.role` IN `["admin", "editor"]` | +| **NOT_IN** | `2` | Exclusion: value is NOT IN list | `.department` NOT_IN `["sales"]` | +| **IN_CONTAINS** | `3` | Substring match | `.email` IN_CONTAINS `["@example.com"]` | + +### Boolean Operators + +| Operator | Value | Behavior | +|----------|-------|----------| +| **AND** | `1` | All conditions must be TRUE | +| **OR** | `2` | At least one condition must be TRUE | + +### Example 1: Simple Role Match + +**Goal:** Grant access to users with role "admin" + +```json +{ + "subject_sets": [{ + "condition_groups": [{ + "boolean_operator": 1, + "conditions": [{ + "subject_external_selector_value": ".role", + "operator": 1, + "subject_external_values": ["admin"] + }] + }] + }] +} +``` + +**Matches:** +- ✅ `{"role": "admin"}` +- ❌ `{"role": "editor"}` + +### Example 2: Multiple Roles (OR) + +**Goal:** Grant access to admins OR editors + +```json +{ + "subject_sets": [{ + "condition_groups": [{ + "boolean_operator": 2, + "conditions": [ + { + "subject_external_selector_value": ".role", + "operator": 1, + "subject_external_values": ["admin"] + }, + { + "subject_external_selector_value": ".role", + "operator": 1, + "subject_external_values": ["editor"] + } + ] + }] + }] +} +``` + +**Simpler Alternative (Same Logic):** +```json +{ + "subject_sets": [{ + "condition_groups": [{ + "boolean_operator": 1, + "conditions": [{ + "subject_external_selector_value": ".role", + "operator": 1, + "subject_external_values": ["admin", "editor"] + }] + }] + }] +} +``` + +### Example 3: Multiple Conditions (AND) + +**Goal:** Grant access to senior engineers only + +```json +{ + "subject_sets": [{ + "condition_groups": [{ + "boolean_operator": 1, + "conditions": [ + { + "subject_external_selector_value": ".level", + "operator": 1, + "subject_external_values": ["senior", "staff", "principal"] + }, + { + "subject_external_selector_value": ".department", + "operator": 1, + "subject_external_values": ["engineering"] + } + ] + }] + }] +} +``` + +**Matches:** +- ✅ `{"level": "senior", "department": "engineering"}` +- ✅ `{"level": "staff", "department": "engineering"}` +- ❌ `{"level": "senior", "department": "sales"}` +- ❌ `{"level": "junior", "department": "engineering"}` + +### Example 4: Domain Email Match (Substring) + +**Goal:** Grant access to anyone with company email + +```json +{ + "subject_sets": [{ + "condition_groups": [{ + "boolean_operator": 1, + "conditions": [{ + "subject_external_selector_value": ".email", + "operator": 3, + "subject_external_values": ["@example.com"] + }] + }] + }] +} +``` + +**Matches:** +- ✅ `{"email": "alice@example.com"}` +- ✅ `{"email": "bob@example.com"}` +- ❌ `{"email": "charlie@external.com"}` + +### Example 5: Complex Multi-Group Logic + +**Goal:** Grant access to: +- Executives (any department), OR +- Senior finance staff + +```json +{ + "subject_sets": [ + { + "condition_groups": [{ + "boolean_operator": 1, + "conditions": [{ + "subject_external_selector_value": ".role", + "operator": 1, + "subject_external_values": ["ceo", "cfo", "cto", "vp"] + }] + }] + }, + { + "condition_groups": [{ + "boolean_operator": 1, + "conditions": [ + { + "subject_external_selector_value": ".level", + "operator": 1, + "subject_external_values": ["senior", "staff"] + }, + { + "subject_external_selector_value": ".department", + "operator": 1, + "subject_external_values": ["finance"] + } + ] + }] + } + ] +} +``` + +**Logic:** `(role IN executives) OR (level IN senior-staff AND department=finance)` + +**Matches:** +- ✅ `{"role": "ceo", "department": "engineering"}` (executive) +- ✅ `{"level": "senior", "department": "finance"}` (senior finance) +- ❌ `{"level": "senior", "department": "engineering"}` (not finance) +- ❌ `{"level": "junior", "department": "finance"}` (not senior) + +## Enumerated vs. Dynamic Attribute Values + +:::info Common Question +**Question:** "Can I use freeform/dynamic values in attributes, or do they have to be pre-enumerated?" + +**Answer:** Both are supported, but they work differently. +::: + +### Enumerated Attributes (Fixed Values) + +**When to use:** Attributes with a known, limited set of values + +**Examples:** +- Clearance levels: `public`, `confidential`, `secret`, `top_secret` +- Departments: `engineering`, `finance`, `sales`, `hr` +- Regions: `us-west`, `us-east`, `eu-central`, `ap-south` + +**Definition:** +```json +{ + "namespace": "example.com", + "name": "clearance", + "rule": "HIERARCHY", + "values": [ + {"value": "public"}, + {"value": "confidential"}, + {"value": "secret"}, + {"value": "top_secret"} + ] +} +``` + +**Subject Mapping:** +```json +{ + "attribute_value_id": "attr-clearance-secret", + "actions": ["DECRYPT"], + "subject_condition_set": { + "subject_sets": [{ + "condition_groups": [{ + "boolean_operator": 1, + "conditions": [{ + "subject_external_selector_value": ".clearance_level", + "operator": 1, + "subject_external_values": ["secret", "top_secret"] + }] + }] + }] + } +} +``` + +### Dynamic Attributes (Freeform Values) + +**When to use:** Attributes with unbounded, user-specific values + +**Examples:** +- Email addresses: `alice@example.com`, `bob@company.org` +- User IDs: `user-12345`, `service-account-xyz` +- Project codes: `proj-2024-Q1-alpha`, `initiative-mars` + +**Definition (NO values array):** +```json +{ + "namespace": "example.com", + "name": "owner_email", + "rule": "ANY_OF" + // Note: No "values" array - accepts any string +} +``` + +**Creating Dynamic Attribute Value:** + +When encrypting, you can create attribute values on-the-fly: + +```bash +# Create attribute value for specific email +otdfctl policy attributes values create \ + --attribute-id \ + --value "alice@example.com" + +# Encrypt with this specific value +otdfctl encrypt file.txt -o file.tdf \ + --attr https://example.com/attr/owner_email/value/alice@example.com +``` + +**Subject Mapping for Dynamic Values:** + +Here's the key insight: **You create Subject Mappings for the PATTERN, not every individual value.** + +**Option 1: Exact Match (One Mapping Per User)** +```json +{ + "attribute_value_id": "attr-owner-alice", // Points to value="alice@example.com" + "actions": ["DECRYPT"], + "subject_condition_set": { + "subject_sets": [{ + "condition_groups": [{ + "boolean_operator": 1, + "conditions": [{ + "subject_external_selector_value": ".email", + "operator": 1, + "subject_external_values": ["alice@example.com"] + }] + }] + }] + } +} +``` + +**Problem:** This requires creating a new Subject Mapping for every user. Not scalable. + +**Option 2: Dynamic Self-Service (Recommended)** + +Use a **generic Subject Mapping** that matches the token claim to the attribute value: + +```json +{ + "attribute_value_id": "attr-owner-{{EMAIL}}", // Placeholder + "actions": ["DECRYPT"], + "subject_condition_set": { + "subject_sets": [{ + "condition_groups": [{ + "boolean_operator": 1, + "conditions": [{ + "subject_external_selector_value": ".email", + "operator": 3, // IN_CONTAINS for substring match + "subject_external_values": ["@example.com"] // Any company email + }] + }] + }] + } +} +``` + +:::tip Advanced Pattern: Self-Service Attributes +For true self-service (user can access resources tagged with their own email), use **Entity Resolution** with attribute value creation: + +1. Define attribute without enumeration +2. When encrypting, dynamically create attribute value: `owner_email/alice@example.com` +3. Subject Mapping grants access IF token email matches attribute value + +This pattern requires custom Entity Resolution logic or attribute value auto-creation. Contact OpenTDF maintainers for implementation guidance. +::: + +### Comparison Table + +| Aspect | Enumerated Attributes | Dynamic Attributes | +|--------|----------------------|-------------------| +| **Values defined** | Yes (fixed list) | No (any string) | +| **Subject Mapping** | Maps claims to specific values | Maps claims to value pattern | +| **Scalability** | Good for 5-100 values | Required for 1000s+ values | +| **Example** | `clearance/secret` | `owner/alice@example.com` | +| **Use case** | Role-based access | User-specific access | + +## IdP Integration Examples + +### Keycloak + +**Common Keycloak Token Claims:** +```json +{ + "sub": "f4d3c2b1-a098-7654-3210-fedcba098765", + "email": "alice@example.com", + "preferred_username": "alice", + "realm_access": { + "roles": ["admin", "user"] + }, + "resource_access": { + "opentdf-app": { + "roles": ["tdf-admin"] + } + }, + "groups": ["/finance/senior", "/engineering/platform"] +} +``` + +#### Example 1: Map Keycloak Realm Roles + +**IdP Configuration:** +- User "alice" has Keycloak role: `admin` + +**Subject Condition Set:** +```json +{ + "subject_sets": [{ + "condition_groups": [{ + "boolean_operator": 1, + "conditions": [{ + "subject_external_selector_value": ".realm_access.roles", + "operator": 1, + "subject_external_values": ["admin"] + }] + }] + }] +} +``` + +**Result:** Alice gets entitlement for attribute value (e.g., `clearance/top_secret`) + +#### Example 2: Map Keycloak Groups + +**IdP Configuration:** +- User "bob" is in Keycloak group: `/finance/senior` + +**Subject Condition Set:** +```json +{ + "subject_sets": [{ + "condition_groups": [{ + "boolean_operator": 1, + "conditions": [{ + "subject_external_selector_value": ".groups", + "operator": 3, + "subject_external_values": ["/finance/"] + }] + }] + }] +} +``` + +**Result:** Bob gets entitlement for `department/finance` attribute + +#### Example 3: Combine Multiple Keycloak Claims + +**Goal:** Grant access to finance admins + +**Subject Condition Set:** +```json +{ + "subject_sets": [{ + "condition_groups": [{ + "boolean_operator": 1, + "conditions": [ + { + "subject_external_selector_value": ".groups", + "operator": 3, + "subject_external_values": ["/finance/"] + }, + { + "subject_external_selector_value": ".realm_access.roles", + "operator": 1, + "subject_external_values": ["admin"] + } + ] + }] + }] +} +``` + +**Logic:** `(group contains "/finance/") AND (role is "admin")` + +### Auth0 + +**Common Auth0 Token Claims:** +```json +{ + "sub": "auth0|507f1f77bcf86cd799439011", + "email": "alice@example.com", + "email_verified": true, + "nickname": "alice", + "https://example.com/roles": ["editor", "viewer"], + "https://example.com/department": "engineering", + "https://example.com/clearance": "secret" +} +``` + +:::info Auth0 Namespaced Claims +Auth0 requires custom claims to use namespaced keys (e.g., `https://example.com/roles`). Use these in Subject Condition Sets with the full namespace. +::: + +#### Example 1: Map Auth0 Custom Roles + +**IdP Configuration:** +- User has custom claim: `https://example.com/roles: ["editor"]` + +**Subject Condition Set:** +```json +{ + "subject_sets": [{ + "condition_groups": [{ + "boolean_operator": 1, + "conditions": [{ + "subject_external_selector_value": ".\"https://example.com/roles\"", + "operator": 1, + "subject_external_values": ["editor", "admin"] + }] + }] + }] +} +``` + +:::warning Escaping Special Characters +JMESPath requires escaping keys with special characters. Use `.\""https://example.com/roles\"` for namespaced Auth0 claims. +::: + +#### Example 2: Map Auth0 Metadata + +**IdP Configuration:** +- User has `app_metadata`: `{"clearance_level": "confidential", "department": "finance"}` + +**Auth0 Rule to Add to Token:** +```javascript +function addMetadataToToken(user, context, callback) { + const namespace = 'https://example.com/'; + context.idToken[namespace + 'clearance'] = user.app_metadata.clearance_level; + context.idToken[namespace + 'department'] = user.app_metadata.department; + callback(null, user, context); +} +``` + +**Subject Condition Set:** +```json +{ + "subject_sets": [{ + "condition_groups": [{ + "boolean_operator": 1, + "conditions": [{ + "subject_external_selector_value": ".\"https://example.com/clearance\"", + "operator": 1, + "subject_external_values": ["confidential", "secret", "top_secret"] + }] + }] + }] +} +``` + +### Okta + +**Common Okta Token Claims:** +```json +{ + "sub": "00u1a2b3c4d5e6f7g8h9", + "email": "alice@example.com", + "email_verified": true, + "groups": ["Engineering", "Senior-Staff"], + "department": "engineering", + "title": "Senior Engineer", + "clearanceLevel": "confidential" +} +``` + +#### Example 1: Map Okta Groups + +**IdP Configuration:** +- User is member of Okta group: "Engineering" + +**Subject Condition Set:** +```json +{ + "subject_sets": [{ + "condition_groups": [{ + "boolean_operator": 1, + "conditions": [{ + "subject_external_selector_value": ".groups", + "operator": 1, + "subject_external_values": ["Engineering", "Product"] + }] + }] + }] +} +``` + +#### Example 2: Map Okta Profile Attributes + +**IdP Configuration:** +- User profile has custom attribute: `clearanceLevel: "confidential"` + +**Okta Configuration (Claims):** +Add custom claim mapping in Okta: +- Claim name: `clearanceLevel` +- Value: `user.clearanceLevel` + +**Subject Condition Set:** +```json +{ + "subject_sets": [{ + "condition_groups": [{ + "boolean_operator": 1, + "conditions": [{ + "subject_external_selector_value": ".clearanceLevel", + "operator": 1, + "subject_external_values": ["confidential", "secret"] + }] + }] + }] +} +``` + +#### Example 3: Combine Okta Claims + +**Goal:** Grant access to senior engineers with confidential clearance + +**Subject Condition Set:** +```json +{ + "subject_sets": [{ + "condition_groups": [{ + "boolean_operator": 1, + "conditions": [ + { + "subject_external_selector_value": ".department", + "operator": 1, + "subject_external_values": ["engineering"] + }, + { + "subject_external_selector_value": ".title", + "operator": 3, + "subject_external_values": ["Senior", "Staff", "Principal"] + }, + { + "subject_external_selector_value": ".clearanceLevel", + "operator": 1, + "subject_external_values": ["confidential", "secret", "top_secret"] + } + ] + }] + }] +} +``` + +**Logic:** `(department="engineering") AND (title contains "Senior"/"Staff"/"Principal") AND (clearance >= confidential)` + +## Creating Subject Mappings: Step-by-Step + +### Prerequisites + +1. **OpenTDF Platform running** with authentication configured +2. **otdfctl installed and authenticated** +3. **Attributes and values created** (the resources you're protecting) + +### Step 1: Create Subject Condition Set + +```bash +otdfctl policy subject-condition-sets create \ + --subject-sets '[ + { + "condition_groups": [{ + "boolean_operator": 1, + "conditions": [{ + "subject_external_selector_value": ".email", + "operator": 3, + "subject_external_values": ["@example.com"] + }] + }] + } + ]' +``` + +**Save the ID from output:** +```console +SUCCESS Created SubjectConditionSet [3c56a6c9-9635-427f-b808-5e8fd395802c] +``` + +### Step 2: Get Attribute Value ID + +```bash +# List attribute values for an attribute +otdfctl policy attributes values list \ + --attribute-id + +# Or create a new value +otdfctl policy attributes values create \ + --attribute-id \ + --value "my-value" +``` + +**Save the attribute value ID:** +```console +4c63e72a-2db9-434c-8ef6-e451473dbfe0 | clearance/secret +``` + +### Step 3: Create Subject Mapping + +```bash +otdfctl policy subject-mappings create \ + --attribute-value-id 4c63e72a-2db9-434c-8ef6-e451473dbfe0 \ + --action-standard DECRYPT \ + --subject-condition-set-id 3c56a6c9-9635-427f-b808-5e8fd395802c +``` + +**Success:** +```console +SUCCESS Created SubjectMapping [sm-789xyz] +``` + +### Step 4: Verify + +```bash +# List all subject mappings +otdfctl policy subject-mappings list + +# Get specific mapping details +otdfctl policy subject-mappings get --id sm-789xyz +``` + +## Troubleshooting + +### Error: "resource relation invalid" + +**Full Error:** +``` +rpc error: code = InvalidArgument desc = resource relation invalid +``` + +**Causes:** +1. **Invalid Attribute Value ID**: The attribute value doesn't exist +2. **Invalid Subject Condition Set ID**: The condition set doesn't exist +3. **Action mismatch**: Using incompatible action types + +**Solutions:** + +**Verify attribute value exists:** +```bash +otdfctl policy attributes values list --attribute-id +``` + +**Verify subject condition set exists:** +```bash +otdfctl policy subject-condition-sets list +``` + +**Check action format:** +```bash +# Correct +--action-standard DECRYPT + +# Also correct +--action-standard "STANDARD_ACTION_DECRYPT" + +# Wrong - don't mix action types +--action-standard DECRYPT --action-custom "custom.action" +``` + +### Error: Token Claim Not Appearing in Entitlements + +**Symptom:** User has claim in JWT, but Subject Mapping doesn't match + +**Debug Steps:** + +**1. Verify token claims:** +```bash +# Decode your JWT token +echo "" | base64 -d +``` + +**2. Check JMESPath selector:** + +The selector must match the exact structure of your token. Test with JMESPath: + +```javascript +// Token: +{ + "user": { + "profile": { + "department": "finance" + } + } +} + +// Correct selector: +".user.profile.department" + +// Wrong: +".department" // Won't find nested value +``` + +**3. Check operator type:** + +```json +// If claim is an array: +{ + "groups": ["admin", "user"] +} + +// Use IN to check array membership: +{ + "subject_external_selector_value": ".groups", + "operator": 1, + "subject_external_values": ["admin"] +} +``` + +**4. Enable debug logging:** + +Contact your OpenTDF administrator to enable debug logging for Subject Mapping evaluation. + +### Error: User Has Entitlement But Still Gets DENY + +**Symptom:** Authorization logs show: +``` +jwtentity-1 (user): PERMIT +jwtentity-0 (client): DENY +Overall decision: DENY +``` + +**Cause:** **Both entities need Subject Mappings** when using SDK clients. + +**Solution:** + +Create Subject Mapping for the **client entity** based on `.clientId`: + +```bash +# Create condition set for client +otdfctl policy subject-condition-sets create \ + --subject-sets '[ + { + "condition_groups": [{ + "boolean_operator": 1, + "conditions": [{ + "subject_external_selector_value": ".clientId", + "operator": 1, + "subject_external_values": ["my-app-client-id"] + }] + }] + } + ]' + +# Create subject mapping for client +otdfctl policy subject-mappings create \ + --attribute-value-id \ + --action-standard DECRYPT \ + --subject-condition-set-id +``` + +### Error: Subject Condition Set Not Found + +**Symptom:** +``` +subject-condition-set not found: +``` + +**Cause:** The Subject Condition Set was deleted or never created. + +**Solution:** + +**List existing condition sets:** +```bash +otdfctl policy subject-condition-sets list +``` + +**If missing, recreate:** +```bash +otdfctl policy subject-condition-sets create \ + --subject-sets '' +``` + +**Update Subject Mapping to use correct ID:** +```bash +otdfctl policy subject-mappings update \ + --id \ + --subject-condition-set-id +``` + +### Debugging Checklist + +When Subject Mappings aren't working: + +- [ ] Verify OpenTDF platform is running and accessible +- [ ] Confirm user is authenticated (valid JWT token) +- [ ] Check token contains expected claims (decode JWT) +- [ ] Verify Subject Condition Set exists (`list` command) +- [ ] Verify Attribute Value exists (`attributes values list`) +- [ ] Verify Subject Mapping exists (`subject-mappings list`) +- [ ] Check JMESPath selector matches token structure +- [ ] Confirm operator type (IN vs IN_CONTAINS) +- [ ] Test with simple condition first (single claim match) +- [ ] For SDK clients: Verify client entity has Subject Mapping +- [ ] Check attribute definition rule (HIERARCHY, ANY_OF, ALL_OF) +- [ ] Verify action matches operation (DECRYPT for decryption) + +## Best Practices + +### 1. Reusable Condition Sets + +Create **generic Subject Condition Sets** that can be shared across multiple Subject Mappings: + +**Example: "Engineering Department" Condition Set** +```bash +# Create once +otdfctl policy subject-condition-sets create \ + --subject-sets '[ + { + "condition_groups": [{ + "boolean_operator": 1, + "conditions": [{ + "subject_external_selector_value": ".department", + "operator": 1, + "subject_external_values": ["engineering"] + }] + }] + } + ]' + +# Reuse for multiple attribute values +otdfctl policy subject-mappings create \ + --attribute-value-id \ + --action-standard DECRYPT \ + --subject-condition-set-id + +otdfctl policy subject-mappings create \ + --attribute-value-id \ + --action-standard DECRYPT \ + --subject-condition-set-id +``` + +### 2. Use Hierarchical Attributes + +Leverage `HIERARCHY` rule for implicit access: + +```json +{ + "name": "clearance", + "rule": "HIERARCHY", + "values": [ + {"value": "public", "order": 1}, + {"value": "confidential", "order": 2}, + {"value": "secret", "order": 3}, + {"value": "top_secret", "order": 4} + ] +} +``` + +**Subject Mapping for top_secret:** +- User gets entitlement: `clearance/top_secret` +- Can access: `top_secret`, `secret`, `confidential`, `public` (all lower levels) + +### 3. Minimize Dynamic Mappings + +**Avoid:** Creating one Subject Mapping per user +```bash +# Don't do this for 1000s of users +create-subject-mapping --for-user alice +create-subject-mapping --for-user bob +create-subject-mapping --for-user charlie +``` + +**Instead:** Use pattern-based conditions +```json +{ + "conditions": [{ + "subject_external_selector_value": ".email", + "operator": 3, + "subject_external_values": ["@company.com"] + }] +} +``` + +### 4. Separate PE and NPE Mappings + +Create distinct Subject Mappings for: +- **Person Entities** (users): Based on `.email`, `.role`, `.groups` +- **Non-Person Entities** (services): Based on `.clientId`, `.scope` + +```bash +# For human users +otdfctl policy subject-mappings create \ + --attribute-value-id \ + --action-standard DECRYPT \ + --subject-condition-set-id + +# For service accounts +otdfctl policy subject-mappings create \ + --attribute-value-id \ + --action-standard DECRYPT \ + --subject-condition-set-id +``` + +### 5. Test in Isolation + +When debugging, test Subject Mappings individually: + +**1. Create test user with minimal claims:** +```json +{ + "email": "test@example.com", + "role": "test-role" +} +``` + +**2. Create simple Subject Mapping:** +```json +{ + "conditions": [{ + "subject_external_selector_value": ".role", + "operator": 1, + "subject_external_values": ["test-role"] + }] +} +``` + +**3. Test decryption:** +```bash +otdfctl decrypt test.tdf +``` + +**4. Gradually add complexity** once basic mapping works. + +## Next Steps + +### Essential Reading + +- [Authorization Service](/components/authorization) - Understand GetEntitlements and GetDecision APIs +- [Entity Resolution](/components/entity_resolution) - Learn how tokens become entity representations +- [Policy: Subject Mappings](/components/policy/subject_mappings) - API reference +- [Quickstart: ABAC Scenario](/quickstart#attribute-based-access-control-abac) - Hands-on example + +### Common Workflows + +**Set up RBAC (Role-Based Access Control):** +1. Define attributes for roles (`role/admin`, `role/editor`, `role/viewer`) +2. Create Subject Condition Sets matching IdP roles +3. Create Subject Mappings granting role-based entitlements + +**Set up ABAC (Attribute-Based Access Control):** +1. Define attributes for multiple dimensions (`department`, `clearance`, `region`) +2. Create Subject Condition Sets with multi-condition logic +3. Encrypt resources with multiple attributes (`--attr X --attr Y`) +4. Subject Mappings grant partial entitlements; decision evaluates all attributes + +**Set up Dynamic User Attributes:** +1. Define attributes without value enumeration +2. Encrypt with user-specific values (`owner_email/alice@example.com`) +3. Create pattern-based Subject Mappings (substring match on email domain) + +## FAQ + +**Q: Do I need to add roles in my IdP that match OpenTDF attributes?** + +**A:** No. IdP roles/claims describe WHO the user is. Subject Mappings convert those claims into OpenTDF entitlements (WHAT they can access). They are separate concerns. + +**Q: Can I use the same Subject Condition Set for multiple Subject Mappings?** + +**A:** Yes! This is recommended for reusability. One condition set (e.g., "engineering department") can grant entitlements to multiple attribute values. + +**Q: What's the difference between IN and IN_CONTAINS?** + +**A:** +- `IN` (operator=1): Exact match. Value must be IN the list. + - `"admin" IN ["admin", "editor"]` → TRUE + - `"administrator" IN ["admin", "editor"]` → FALSE +- `IN_CONTAINS` (operator=3): Substring match. Value must CONTAIN substring. + - `"administrator" IN_CONTAINS ["admin"]` → TRUE + - `"admin" IN_CONTAINS ["admin"]` → TRUE + +**Q: Why do I see both jwtentity-0 and jwtentity-1 in authorization logs?** + +**A:** `jwtentity-0` is the client application (NPE), `jwtentity-1` is the user (PE). Both participate in authorization decisions when using SDK clients. Both may need Subject Mappings depending on your access model. + +**Q: Can I update a Subject Mapping without recreating it?** + +**A:** Yes, use `otdfctl policy subject-mappings update --id --subject-condition-set-id ` to change the condition set or other properties. + +**Q: How do I handle users in multiple groups?** + +**A:** Create multiple Subject Mappings (one per group) that grant different entitlements. A user can receive entitlements from multiple mappings simultaneously. + +**Q: What happens if no Subject Mappings match a user?** + +**A:** Authorization returns DENY. The user has no entitlements, so they cannot access any protected resources. From 58336cf0e0a784de79eb8b9068f1668e01a2d399 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Mon, 9 Feb 2026 14:03:25 -0800 Subject: [PATCH 02/13] code review Signed-off-by: Mary Dickson --- docs/guides/subject-mapping-guide.md | 36 ++++++++++++++++++---------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/docs/guides/subject-mapping-guide.md b/docs/guides/subject-mapping-guide.md index 0c860d55..9e006392 100644 --- a/docs/guides/subject-mapping-guide.md +++ b/docs/guides/subject-mapping-guide.md @@ -555,22 +555,22 @@ Here's the key insight: **You create Subject Mappings for the PATTERN, not every **Problem:** This requires creating a new Subject Mapping for every user. Not scalable. -**Option 2: Dynamic Self-Service (Recommended)** +**Option 2: Group/Department-Based Access (Recommended)** -Use a **generic Subject Mapping** that matches the token claim to the attribute value: +Create Subject Mappings based on department or group claims that cover multiple users: ```json { - "attribute_value_id": "attr-owner-{{EMAIL}}", // Placeholder + "attribute_value_id": "attr-department-finance", "actions": ["DECRYPT"], "subject_condition_set": { "subject_sets": [{ "condition_groups": [{ "boolean_operator": 1, "conditions": [{ - "subject_external_selector_value": ".email", - "operator": 3, // IN_CONTAINS for substring match - "subject_external_values": ["@example.com"] // Any company email + "subject_external_selector_value": ".department", + "operator": 1, + "subject_external_values": ["finance", "accounting"] }] }] }] @@ -578,14 +578,26 @@ Use a **generic Subject Mapping** that matches the token claim to the attribute } ``` -:::tip Advanced Pattern: Self-Service Attributes -For true self-service (user can access resources tagged with their own email), use **Entity Resolution** with attribute value creation: +→ Anyone with `department: "finance"` or `department: "accounting"` in their token gets entitlement for `department/finance` attribute + +**Key:** One Subject Mapping covers all matching users. The IdP provides the claims (email domain, department, groups) that the condition evaluates. + +:::tip Advanced Pattern: True Per-User Self-Service +For true self-service access where users can only access resources tagged specifically with their own identity (e.g., Alice accesses files tagged `owner/alice@example.com` but not `owner/bob@example.com`), the standard Subject Mapping system alone is insufficient. + +Subject Mappings grant **entitlements** to attribute values, but they cannot dynamically match a user's claim to a resource's attribute value at decision time. A Subject Mapping must reference a specific `attribute_value_id` that already exists. + +**This pattern requires custom logic** in one of these places: +- **Entity Resolution Service**: Custom resolver that dynamically creates entitlements matching the user's identity claim +- **Authorization Service**: Custom decision logic that compares token claims directly to resource attribute values +- **Application layer**: Pre-authorization filtering based on user identity before calling OpenTDF -1. Define attribute without enumeration -2. When encrypting, dynamically create attribute value: `owner_email/alice@example.com` -3. Subject Mapping grants access IF token email matches attribute value +**Conceptual workflow:** +1. Define attribute without value enumeration (`owner_email`) +2. When encrypting, create attribute value dynamically: `owner_email/alice@example.com` +3. At decryption time, custom logic grants access when token's `.email` claim matches the resource's `owner_email` value -This pattern requires custom Entity Resolution logic or attribute value auto-creation. Contact OpenTDF maintainers for implementation guidance. +This requires development work beyond standard Subject Mapping configuration. Consult OpenTDF maintainers for implementation guidance. ::: ### Comparison Table From 84e313d72d53338dbc97c0a1ffc756a9e71630f6 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Mon, 9 Feb 2026 14:12:07 -0800 Subject: [PATCH 03/13] fix vale ci issues Signed-off-by: Mary Dickson --- .../config/Vocab/Opentdf/accept.txt | 24 +++++++++++++++++++ docs/guides/subject-mapping-guide.md | 3 +++ 2 files changed, 27 insertions(+) create mode 100644 .github/vale-styles/config/Vocab/Opentdf/accept.txt diff --git a/.github/vale-styles/config/Vocab/Opentdf/accept.txt b/.github/vale-styles/config/Vocab/Opentdf/accept.txt new file mode 100644 index 00000000..6062987e --- /dev/null +++ b/.github/vale-styles/config/Vocab/Opentdf/accept.txt @@ -0,0 +1,24 @@ +Docusaurus +[Oo]tdfctl +API +(?i)tdf +[Nn]amespace +Keycloak +Virtru +SDK +IdP +NPE +PE +FQN +JWT +proto +Postgres +ECDSA +[Nn]ano +Podman +assertation +[Dd]issem +JavaScript +Autoconfigure +requester(?('s)) +rewrap(?(s)) \ No newline at end of file diff --git a/docs/guides/subject-mapping-guide.md b/docs/guides/subject-mapping-guide.md index 9e006392..aa696158 100644 --- a/docs/guides/subject-mapping-guide.md +++ b/docs/guides/subject-mapping-guide.md @@ -3,6 +3,9 @@ title: Subject Mapping Comprehensive Guide sidebar_position: 1 --- + + + # Subject Mapping: Comprehensive Guide :::info What You'll Learn From b428af7c7347930edbc0ba784823dd4a98466aff Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Mon, 9 Feb 2026 14:21:10 -0800 Subject: [PATCH 04/13] go back to previous example Signed-off-by: Mary Dickson --- docs/guides/subject-mapping-guide.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/guides/subject-mapping-guide.md b/docs/guides/subject-mapping-guide.md index aa696158..ab305005 100644 --- a/docs/guides/subject-mapping-guide.md +++ b/docs/guides/subject-mapping-guide.md @@ -558,22 +558,22 @@ Here's the key insight: **You create Subject Mappings for the PATTERN, not every **Problem:** This requires creating a new Subject Mapping for every user. Not scalable. -**Option 2: Group/Department-Based Access (Recommended)** +**Option 2: Pattern-Based Access (Recommended)** -Create Subject Mappings based on department or group claims that cover multiple users: +Create Subject Mappings based on patterns in token claims (like email domains) that cover multiple users: ```json { - "attribute_value_id": "attr-department-finance", + "attribute_value_id": "attr-company-employees", "actions": ["DECRYPT"], "subject_condition_set": { "subject_sets": [{ "condition_groups": [{ "boolean_operator": 1, "conditions": [{ - "subject_external_selector_value": ".department", - "operator": 1, - "subject_external_values": ["finance", "accounting"] + "subject_external_selector_value": ".email", + "operator": 3, // IN_CONTAINS (substring match) + "subject_external_values": ["@example.com"] }] }] }] @@ -581,9 +581,9 @@ Create Subject Mappings based on department or group claims that cover multiple } ``` -→ Anyone with `department: "finance"` or `department: "accounting"` in their token gets entitlement for `department/finance` attribute +→ Anyone with an email containing `@example.com` in their token gets entitlement for the `company/employees` attribute -**Key:** One Subject Mapping covers all matching users. The IdP provides the claims (email domain, department, groups) that the condition evaluates. +**Key:** One Subject Mapping covers all matching users. The IdP provides the claims (email addresses) and the condition evaluates the pattern at decision time. :::tip Advanced Pattern: True Per-User Self-Service For true self-service access where users can only access resources tagged specifically with their own identity (e.g., Alice accesses files tagged `owner/alice@example.com` but not `owner/bob@example.com`), the standard Subject Mapping system alone is insufficient. From 5526bf07ee3527270a7f03bdc3e1d15fb74be8e7 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Tue, 10 Feb 2026 08:05:48 -0800 Subject: [PATCH 05/13] updates to agents file Signed-off-by: Mary Dickson --- AGENTS.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 22e7f204..644f3b3c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,9 +18,13 @@ - `npm run check-vendored-yaml`: Verify vendored OpenAPI YAML matches upstream. - `npm run update-vendored-yaml`: Refresh vendored specs in `specs/` from upstream. +Preview deployment: +- Deploy to preview using pattern: `surge build opentdf-docs-preview-.surge.sh` +- Extract ticket number from branch name (e.g., branch `feat/dspx-2416` → `opentdf-docs-preview-dspx-2416.surge.sh`) + Docs-only checks: - `vale sync`: Install Vale styles configured in `.vale.ini`. -- `git diff --name-only | xargs vale --glob='!blog/*'`: Lint changed docs (matches CI’s “added lines” behavior closely). +- `git diff --name-only | xargs vale --glob='!blog/*'`: Lint changed docs (matches CI's "added lines" behavior closely). ## Coding Style & Naming Conventions @@ -30,8 +34,18 @@ Docs-only checks: ## Testing Guidelines -- There is no dedicated unit test runner; CI primarily validates `npm run build` and Vale. -- If you touch `docs/getting-started/` Docker Compose instructions, sanity-check them locally when feasible. +CI runs the following tests: + +- **BATS tests**: Shell script tests in `tests/quickstart.bats` validate quickstart scripts on Ubuntu, macOS, and Windows +- **Shellcheck**: Lints shell scripts in `static/quickstart/` (check.sh, install.sh) +- **Docker Compose stack test**: Verifies the platform starts successfully on Ubuntu (triggered by changes to `docs/getting-started/`, `static/quickstart/`, or `tests/`) +- **Build validation**: `npm run build` must complete successfully +- **Vale linting**: Documentation prose style checks (run locally with `git diff --name-only | xargs vale --glob='!blog/*'`) + +If you modify quickstart scripts or Docker Compose instructions: +- Run shellcheck locally: `shellcheck static/quickstart/check.sh static/quickstart/install.sh` +- Run BATS tests if available: `bats tests/quickstart.bats` +- Test the Docker Compose stack if feasible: Follow steps in `docs/getting-started/quickstart.mdx` ## Commit & Pull Request Guidelines From 13761c3af53f3e40c331a3db94724c5aba8fb9b7 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Wed, 18 Feb 2026 09:25:40 -0800 Subject: [PATCH 06/13] npm run update-vendored-yaml Signed-off-by: Mary Dickson --- specs/policy/actions/actions.openapi.yaml | 23 --- .../policy/attributes/attributes.openapi.yaml | 23 --- .../policy/namespaces/namespaces.openapi.yaml | 179 ------------------ specs/policy/objects.openapi.yaml | 23 --- .../obligations/obligations.openapi.yaml | 23 --- .../registered_resources.openapi.yaml | 23 --- .../resource_mapping.openapi.yaml | 23 --- .../subject_mapping.openapi.yaml | 23 --- specs/policy/unsafe/unsafe.openapi.yaml | 23 --- 9 files changed, 363 deletions(-) diff --git a/specs/policy/actions/actions.openapi.yaml b/specs/policy/actions/actions.openapi.yaml index 9e04d4e3..a3efaf59 100644 --- a/specs/policy/actions/actions.openapi.yaml +++ b/specs/policy/actions/actions.openapi.yaml @@ -497,23 +497,6 @@ components: required: - rule additionalProperties: false - policy.Certificate: - type: object - properties: - id: - type: string - title: id - description: generated uuid in database - pem: - type: string - title: pem - description: PEM format certificate - metadata: - title: metadata - description: Optional metadata. - $ref: '#/components/schemas/common.Metadata' - title: Certificate - additionalProperties: false policy.Condition: type: object properties: @@ -686,12 +669,6 @@ components: $ref: '#/components/schemas/policy.SimpleKasKey' title: kas_keys description: Keys for the namespace - rootCerts: - type: array - items: - $ref: '#/components/schemas/policy.Certificate' - title: root_certs - description: Root certificates for chain of trust title: Namespace additionalProperties: false policy.Obligation: diff --git a/specs/policy/attributes/attributes.openapi.yaml b/specs/policy/attributes/attributes.openapi.yaml index aa04ee67..8788f9ef 100644 --- a/specs/policy/attributes/attributes.openapi.yaml +++ b/specs/policy/attributes/attributes.openapi.yaml @@ -1014,23 +1014,6 @@ components: required: - rule additionalProperties: false - policy.Certificate: - type: object - properties: - id: - type: string - title: id - description: generated uuid in database - pem: - type: string - title: pem - description: PEM format certificate - metadata: - title: metadata - description: Optional metadata. - $ref: '#/components/schemas/common.Metadata' - title: Certificate - additionalProperties: false policy.Condition: type: object properties: @@ -1203,12 +1186,6 @@ components: $ref: '#/components/schemas/policy.SimpleKasKey' title: kas_keys description: Keys for the namespace - rootCerts: - type: array - items: - $ref: '#/components/schemas/policy.Certificate' - title: root_certs - description: Root certificates for chain of trust title: Namespace additionalProperties: false policy.Obligation: diff --git a/specs/policy/namespaces/namespaces.openapi.yaml b/specs/policy/namespaces/namespaces.openapi.yaml index a32f5db1..51632ed0 100644 --- a/specs/policy/namespaces/namespaces.openapi.yaml +++ b/specs/policy/namespaces/namespaces.openapi.yaml @@ -325,77 +325,6 @@ paths: application/json: schema: $ref: '#/components/schemas/policy.namespaces.RemovePublicKeyFromNamespaceResponse' - /policy.namespaces.NamespaceService/AssignCertificateToNamespace: - post: - tags: - - policy.namespaces.NamespaceService - summary: AssignCertificateToNamespace - description: Namespace <> Certificate RPCs - operationId: policy.namespaces.NamespaceService.AssignCertificateToNamespace - parameters: - - name: Connect-Protocol-Version - in: header - required: true - schema: - $ref: '#/components/schemas/connect-protocol-version' - - name: Connect-Timeout-Ms - in: header - schema: - $ref: '#/components/schemas/connect-timeout-header' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/policy.namespaces.AssignCertificateToNamespaceRequest' - required: true - responses: - default: - description: Error - content: - application/json: - schema: - $ref: '#/components/schemas/connect.error' - "200": - description: Success - content: - application/json: - schema: - $ref: '#/components/schemas/policy.namespaces.AssignCertificateToNamespaceResponse' - /policy.namespaces.NamespaceService/RemoveCertificateFromNamespace: - post: - tags: - - policy.namespaces.NamespaceService - summary: RemoveCertificateFromNamespace - operationId: policy.namespaces.NamespaceService.RemoveCertificateFromNamespace - parameters: - - name: Connect-Protocol-Version - in: header - required: true - schema: - $ref: '#/components/schemas/connect-protocol-version' - - name: Connect-Timeout-Ms - in: header - schema: - $ref: '#/components/schemas/connect-timeout-header' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/policy.namespaces.RemoveCertificateFromNamespaceRequest' - required: true - responses: - default: - description: Error - content: - application/json: - schema: - $ref: '#/components/schemas/connect.error' - "200": - description: Success - content: - application/json: - schema: - $ref: '#/components/schemas/policy.namespaces.RemoveCertificateFromNamespaceResponse' components: schemas: common.ActiveStateEnum: @@ -446,20 +375,6 @@ components: Describes whether this kas is managed by the organization or if they imported the kas information from an external party. These two modes are necessary in order to encrypt a tdf dek with an external parties kas public key. - common.IdFqnIdentifier: - type: object - properties: - id: - type: string - title: id - format: uuid - fqn: - type: string - title: fqn - minLength: 1 - format: uri - title: IdFqnIdentifier - additionalProperties: false common.Metadata: type: object properties: @@ -620,23 +535,6 @@ components: the Joda Time's [`ISODateTimeFormat.dateTime()`]( http://joda-time.sourceforge.net/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime() ) to obtain a formatter capable of generating timestamps in this format. - policy.Certificate: - type: object - properties: - id: - type: string - title: id - description: generated uuid in database - pem: - type: string - title: pem - description: PEM format certificate - metadata: - title: metadata - description: Optional metadata. - $ref: '#/components/schemas/common.Metadata' - title: Certificate - additionalProperties: false policy.KasPublicKey: type: object properties: @@ -758,12 +656,6 @@ components: $ref: '#/components/schemas/policy.SimpleKasKey' title: kas_keys description: Keys for the namespace - rootCerts: - type: array - items: - $ref: '#/components/schemas/policy.Certificate' - title: root_certs - description: Root certificates for chain of trust title: Namespace additionalProperties: false policy.PageRequest: @@ -867,39 +759,6 @@ components: title: pem title: SimpleKasPublicKey additionalProperties: false - policy.namespaces.AssignCertificateToNamespaceRequest: - type: object - properties: - namespace: - title: namespace - description: Required - namespace identifier (id or fqn) - $ref: '#/components/schemas/common.IdFqnIdentifier' - pem: - type: string - title: pem - description: Required - PEM format certificate - metadata: - title: metadata - description: Optional - $ref: '#/components/schemas/common.MetadataMutable' - title: AssignCertificateToNamespaceRequest - required: - - namespace - - pem - additionalProperties: false - policy.namespaces.AssignCertificateToNamespaceResponse: - type: object - properties: - namespaceCertificate: - title: namespace_certificate - description: The mapping of the namespace to the certificate. - $ref: '#/components/schemas/policy.namespaces.NamespaceCertificate' - certificate: - title: certificate - description: Return the full certificate object for convenience - $ref: '#/components/schemas/policy.Certificate' - title: AssignCertificateToNamespaceResponse - additionalProperties: false policy.namespaces.AssignKeyAccessServerToNamespaceRequest: type: object properties: @@ -1057,24 +916,6 @@ components: $ref: '#/components/schemas/policy.PageResponse' title: ListNamespacesResponse additionalProperties: false - policy.namespaces.NamespaceCertificate: - type: object - properties: - namespace: - title: namespace - description: Required - namespace identifier (id or fqn) - $ref: '#/components/schemas/common.IdFqnIdentifier' - certificateId: - type: string - title: certificate_id - format: uuid - description: Required (The id from the Certificate object) - title: NamespaceCertificate - required: - - namespace - - certificateId - additionalProperties: false - description: Maps a namespace to a certificate (similar to NamespaceKey pattern) policy.namespaces.NamespaceKey: type: object properties: @@ -1109,26 +950,6 @@ components: title: NamespaceKeyAccessServer additionalProperties: false description: Deprecated - policy.namespaces.RemoveCertificateFromNamespaceRequest: - type: object - properties: - namespaceCertificate: - title: namespace_certificate - description: The namespace and certificate to unassign. - $ref: '#/components/schemas/policy.namespaces.NamespaceCertificate' - title: RemoveCertificateFromNamespaceRequest - required: - - namespaceCertificate - additionalProperties: false - policy.namespaces.RemoveCertificateFromNamespaceResponse: - type: object - properties: - namespaceCertificate: - title: namespace_certificate - description: The unassigned namespace and certificate. - $ref: '#/components/schemas/policy.namespaces.NamespaceCertificate' - title: RemoveCertificateFromNamespaceResponse - additionalProperties: false policy.namespaces.RemoveKeyAccessServerFromNamespaceRequest: type: object properties: diff --git a/specs/policy/objects.openapi.yaml b/specs/policy/objects.openapi.yaml index 73ff1404..c1f082cc 100644 --- a/specs/policy/objects.openapi.yaml +++ b/specs/policy/objects.openapi.yaml @@ -355,23 +355,6 @@ components: required: - rule additionalProperties: false - policy.Certificate: - type: object - properties: - id: - type: string - title: id - description: generated uuid in database - pem: - type: string - title: pem - description: PEM format certificate - metadata: - title: metadata - description: Optional metadata. - $ref: '#/components/schemas/common.Metadata' - title: Certificate - additionalProperties: false policy.Condition: type: object properties: @@ -605,12 +588,6 @@ components: $ref: '#/components/schemas/policy.SimpleKasKey' title: kas_keys description: Keys for the namespace - rootCerts: - type: array - items: - $ref: '#/components/schemas/policy.Certificate' - title: root_certs - description: Root certificates for chain of trust title: Namespace additionalProperties: false policy.Obligation: diff --git a/specs/policy/obligations/obligations.openapi.yaml b/specs/policy/obligations/obligations.openapi.yaml index 904b7706..a0bc8c8c 100644 --- a/specs/policy/obligations/obligations.openapi.yaml +++ b/specs/policy/obligations/obligations.openapi.yaml @@ -846,23 +846,6 @@ components: required: - rule additionalProperties: false - policy.Certificate: - type: object - properties: - id: - type: string - title: id - description: generated uuid in database - pem: - type: string - title: pem - description: PEM format certificate - metadata: - title: metadata - description: Optional metadata. - $ref: '#/components/schemas/common.Metadata' - title: Certificate - additionalProperties: false policy.Condition: type: object properties: @@ -1035,12 +1018,6 @@ components: $ref: '#/components/schemas/policy.SimpleKasKey' title: kas_keys description: Keys for the namespace - rootCerts: - type: array - items: - $ref: '#/components/schemas/policy.Certificate' - title: root_certs - description: Root certificates for chain of trust title: Namespace additionalProperties: false policy.Obligation: diff --git a/specs/policy/registeredresources/registered_resources.openapi.yaml b/specs/policy/registeredresources/registered_resources.openapi.yaml index 0a481f4c..676ab81a 100644 --- a/specs/policy/registeredresources/registered_resources.openapi.yaml +++ b/specs/policy/registeredresources/registered_resources.openapi.yaml @@ -707,23 +707,6 @@ components: required: - rule additionalProperties: false - policy.Certificate: - type: object - properties: - id: - type: string - title: id - description: generated uuid in database - pem: - type: string - title: pem - description: PEM format certificate - metadata: - title: metadata - description: Optional metadata. - $ref: '#/components/schemas/common.Metadata' - title: Certificate - additionalProperties: false policy.Condition: type: object properties: @@ -896,12 +879,6 @@ components: $ref: '#/components/schemas/policy.SimpleKasKey' title: kas_keys description: Keys for the namespace - rootCerts: - type: array - items: - $ref: '#/components/schemas/policy.Certificate' - title: root_certs - description: Root certificates for chain of trust title: Namespace additionalProperties: false policy.Obligation: diff --git a/specs/policy/resourcemapping/resource_mapping.openapi.yaml b/specs/policy/resourcemapping/resource_mapping.openapi.yaml index 69cdbc4a..86b0d952 100644 --- a/specs/policy/resourcemapping/resource_mapping.openapi.yaml +++ b/specs/policy/resourcemapping/resource_mapping.openapi.yaml @@ -707,23 +707,6 @@ components: required: - rule additionalProperties: false - policy.Certificate: - type: object - properties: - id: - type: string - title: id - description: generated uuid in database - pem: - type: string - title: pem - description: PEM format certificate - metadata: - title: metadata - description: Optional metadata. - $ref: '#/components/schemas/common.Metadata' - title: Certificate - additionalProperties: false policy.Condition: type: object properties: @@ -896,12 +879,6 @@ components: $ref: '#/components/schemas/policy.SimpleKasKey' title: kas_keys description: Keys for the namespace - rootCerts: - type: array - items: - $ref: '#/components/schemas/policy.Certificate' - title: root_certs - description: Root certificates for chain of trust title: Namespace additionalProperties: false policy.Obligation: diff --git a/specs/policy/subjectmapping/subject_mapping.openapi.yaml b/specs/policy/subjectmapping/subject_mapping.openapi.yaml index 4d0e7f1b..f8dcc5ba 100644 --- a/specs/policy/subjectmapping/subject_mapping.openapi.yaml +++ b/specs/policy/subjectmapping/subject_mapping.openapi.yaml @@ -743,23 +743,6 @@ components: required: - rule additionalProperties: false - policy.Certificate: - type: object - properties: - id: - type: string - title: id - description: generated uuid in database - pem: - type: string - title: pem - description: PEM format certificate - metadata: - title: metadata - description: Optional metadata. - $ref: '#/components/schemas/common.Metadata' - title: Certificate - additionalProperties: false policy.Condition: type: object properties: @@ -932,12 +915,6 @@ components: $ref: '#/components/schemas/policy.SimpleKasKey' title: kas_keys description: Keys for the namespace - rootCerts: - type: array - items: - $ref: '#/components/schemas/policy.Certificate' - title: root_certs - description: Root certificates for chain of trust title: Namespace additionalProperties: false policy.Obligation: diff --git a/specs/policy/unsafe/unsafe.openapi.yaml b/specs/policy/unsafe/unsafe.openapi.yaml index c604fa65..bf346f58 100644 --- a/specs/policy/unsafe/unsafe.openapi.yaml +++ b/specs/policy/unsafe/unsafe.openapi.yaml @@ -721,23 +721,6 @@ components: required: - rule additionalProperties: false - policy.Certificate: - type: object - properties: - id: - type: string - title: id - description: generated uuid in database - pem: - type: string - title: pem - description: PEM format certificate - metadata: - title: metadata - description: Optional metadata. - $ref: '#/components/schemas/common.Metadata' - title: Certificate - additionalProperties: false policy.Condition: type: object properties: @@ -946,12 +929,6 @@ components: $ref: '#/components/schemas/policy.SimpleKasKey' title: kas_keys description: Keys for the namespace - rootCerts: - type: array - items: - $ref: '#/components/schemas/policy.Certificate' - title: root_certs - description: Root certificates for chain of trust title: Namespace additionalProperties: false policy.Obligation: From 930f39437f2fcaa6da4d3d57043c45758d126825 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Tue, 24 Feb 2026 08:54:58 -0800 Subject: [PATCH 07/13] =?UTF-8?q?fix(subject-mapping-guide):=20address=20c?= =?UTF-8?q?ode=20review=20=E2=80=94=20correct=20hallucinated=20content=20a?= =?UTF-8?q?nd=20deprecated=20APIs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace deprecated STANDARD_ACTION_DECRYPT with `read` throughout (objects.proto) - Fix selector syntax: was incorrectly described as JMESPath; platform uses custom flattening library (platform/lib/flattening/flatten.go) - Correct entity category explanation: only CATEGORY_SUBJECT entities are evaluated in GetDecision flows; CATEGORY_ENVIRONMENT (OIDC client) is excluded (authorization.go:653-666) - Rewrite "Dynamic Attribute Values" section — feature does not exist; all values must be explicitly created (attributes.proto CreateAttributeValueRequest) - Remove Auth0 IdP section (untested) - Remove nonexistent "order" key from HIERARCHY attribute JSON (objects.proto Value message) - Fix troubleshooting DENY advice: client entity does not need Subject Mappings in user-auth flows; clarify service account (client credentials) exception - Add otdfctl dev selectors generate/test examples to troubleshooting - Add ERS multi-source note to subject_mappings.md (JWT, LDAP, SQL, IdP user info) Co-Authored-By: Claude Sonnet 4.6 --- docs/components/policy/subject_mappings.md | 10 + docs/guides/subject-mapping-guide.md | 409 +++++++-------------- 2 files changed, 148 insertions(+), 271 deletions(-) diff --git a/docs/components/policy/subject_mappings.md b/docs/components/policy/subject_mappings.md index 92dfc50a..f593ec87 100644 --- a/docs/components/policy/subject_mappings.md +++ b/docs/components/policy/subject_mappings.md @@ -4,6 +4,16 @@ For a comprehensive tutorial with IdP integration examples, troubleshooting, and step-by-step guides, see the [Subject Mapping Comprehensive Guide](/guides/subject-mapping-guide). ::: +:::note Entity attributes come from the Entity Resolution Service (ERS) +Subject Mappings evaluate conditions against the **Entity Representation** produced by the Entity Resolution Service — not directly against IdP tokens. Depending on ERS configuration, entity attributes may come from: +- Access token claims (JWT) +- IdP user info (e.g., Keycloak, Okta) +- LDAP / Active Directory +- SQL databases + +This means a Subject Condition Set matching `.emailAddress` will work regardless of whether that attribute comes from a JWT claim, an LDAP directory entry, or a SQL row — as long as ERS resolves it into the entity representation. Multi-source ERS support is available; see the [Entity Resolution](/components/entity_resolution) documentation. +::: + As data is bound to fully qualified Attribute Values when encrypted within a TDF, entities are associated with Attribute values through a mechanism called Subject Mappings. Entities (subjects, users, machines, etc.) are represented by their identity as determined from an identity provider (IdP). After an entity has securely authenticated with the IdP, the client's token (OIDC/OAUTH2) will include claims or attributes that describe that identity. Subject Mappings define how to map these identity attributes to actions on attribute values defined in the OpenTDF platform Policy. For more details on how the platform integrates with the IdP and how entities are resolved, refer to the [Authorization documentation](../authorization). diff --git a/docs/guides/subject-mapping-guide.md b/docs/guides/subject-mapping-guide.md index ab305005..928d40b5 100644 --- a/docs/guides/subject-mapping-guide.md +++ b/docs/guides/subject-mapping-guide.md @@ -12,7 +12,7 @@ sidebar_position: 1 This guide explains how OpenTDF connects user identities from your Identity Provider (IdP) to attribute-based access control. You'll understand: - **Why** Subject Mappings exist (vs. direct IdP attribute mapping) - **How** authentication flows through Entity Resolution to authorization decisions -- **When** to use enumerated vs. dynamic attribute values +- **How** to scale Subject Mappings from exact-match to pattern-based conditions - **How to troubleshoot** common Subject Mapping errors ::: @@ -112,12 +112,14 @@ The token is parsed into an **Entity Representation** - a normalized view of the } ``` -:::warning Entity Types Confusion -You may see TWO entities in authorization logs: -- `jwtentity-0`: The **client application** (NPE - Non-Person Entity) -- `jwtentity-1`: The **user** (PE - Person Entity) +:::warning Entity Types in Authorization Logs +You may see TWO entities in authorization logs. When using the Keycloak ERS, the IDs follow this format: +- `jwtentity-0-clientid-{id}`: The **OIDC client application** — assigned `CATEGORY_ENVIRONMENT` +- `jwtentity-1-username-{name}`: The **authenticated user** — assigned `CATEGORY_SUBJECT` -**Both need Subject Mappings** if both need attribute access. For SDK decryption, typically the client (`jwtentity-0`) needs mappings based on `.clientId`. +In `GetDecision` flows, **only `CATEGORY_SUBJECT` entities participate in the access decision**. The environment entity (client) is tracked in audit logs but its entitlements are not evaluated. For standard TDF decrypt flows, only the user (`CATEGORY_SUBJECT`) needs Subject Mappings. + +Exception: when using client credentials (service account) flows, the service account is assigned `CATEGORY_SUBJECT` and does need Subject Mappings. ::: #### Step 5-8: Subject Mapping Evaluation @@ -129,7 +131,7 @@ The Authorization Service queries the Policy Service: "Which Subject Mappings ap { "id": "sm-001", "attribute_value_id": "attr-clearance-executive", - "actions": ["STANDARD_ACTION_DECRYPT"], + "actions": ["read"], "subject_condition_set": { "subject_sets": [{ "condition_groups": [{ @@ -148,7 +150,7 @@ The Authorization Service queries the Policy Service: "Which Subject Mappings ap **Evaluation Logic:** 1. Extract `.role` from entity representation → `"vice_president"` 2. Check if `"vice_president"` is IN `["vice_president", "ceo", "cfo"]` → ✅ TRUE -3. Grant entitlement: `clearance/executive` with `DECRYPT` action +3. Grant entitlement: `clearance/executive` with `read` action #### Step 9-10: Authorization Decision @@ -158,7 +160,7 @@ The Authorization Service queries the Policy Service: "Which Subject Mappings ap "attribute_values": [ { "attribute": "https://example.com/attr/clearance/value/executive", - "actions": ["DECRYPT"] + "actions": ["read"] } ] } @@ -195,11 +197,11 @@ graph TD C --> E{Category} D --> F{Category} - E -->|Subject| G[PE + Subject
Human user in auth flow
✅ Attributes checked] - E -->|Environment| H[PE + Environment
Logged-in operator
⚠️ Always PERMIT] + E -->|Subject| G[PE + Subject
Human user in auth flow
✅ Attributes checked in decisions] + E -->|Environment| H[PE + Environment
Logged-in operator
⚠️ Not evaluated in GetDecision] - F -->|Subject| I[NPE + Subject
Service account, client app
✅ Attributes checked] - F -->|Environment| J[NPE + Environment
System component
⚠️ Always PERMIT] + F -->|Subject| I[NPE + Subject
Service account (client credentials flow)
✅ Attributes checked in decisions] + F -->|Environment| J[NPE + Environment
OIDC client in user-auth flow
⚠️ Not evaluated in GetDecision] ``` ### Practical Examples @@ -230,20 +232,24 @@ graph TD ``` → Subject Mapping checks: Does this service account have the right entitlements? -**Environment Category (Auto-PERMIT)** +**Environment Category (Excluded from Decision)** ```json { "type": "NPE", "category": "CATEGORY_ENVIRONMENT", "claims": { - "systemComponent": "internal-backup-job" + "clientId": "my-app-client" } } ``` -→ **No attribute checking**. Always returns PERMIT. Use for trusted system components. +→ Entitlements are tracked in audit logs but **not evaluated** in `GetDecision` flows. This is the standard category for the OIDC client in a user-authenticated request. + +:::note Environment vs Subject +In a typical TDF flow with user authentication: +- The OIDC **client** (the app calling the SDK) → `CATEGORY_ENVIRONMENT` → not checked in decisions +- The **user** (who authenticated via the client) → `CATEGORY_SUBJECT` → checked in decisions -:::danger Security Warning -Environment entities **bypass all attribute checks**. Only use for fully trusted system components that should always have access (e.g., backup services, monitoring tools). +Only create Subject Mappings for `CATEGORY_SUBJECT` entities. Environment entities do not need Subject Mappings for TDF decrypt to succeed. ::: ## Subject Condition Sets: The Matching Engine @@ -257,7 +263,7 @@ SubjectConditionSet └─ SubjectSets[] (OR'd together - ANY set can match) └─ ConditionGroups[] (Combined by boolean operator) └─ Conditions[] (Combined by boolean operator) - ├─ SubjectExternalSelectorValue (JMESPath to extract claim) + ├─ SubjectExternalSelectorValue (flattening-syntax selector to extract claim) ├─ Operator (IN, NOT_IN, IN_CONTAINS) └─ SubjectExternalValues (Values to match) ``` @@ -446,101 +452,22 @@ SubjectConditionSet - ❌ `{"level": "senior", "department": "engineering"}` (not finance) - ❌ `{"level": "junior", "department": "finance"}` (not senior) -## Enumerated vs. Dynamic Attribute Values +## Scaling Subject Mappings: Exact Match vs. Pattern-Based :::info Common Question -**Question:** "Can I use freeform/dynamic values in attributes, or do they have to be pre-enumerated?" +**Question:** "Do I need a separate Subject Mapping for every user, or can one mapping cover many users?" -**Answer:** Both are supported, but they work differently. +**Answer:** One Subject Mapping can cover many users by using pattern-based condition operators (`IN_CONTAINS`) rather than exact matches. ::: -### Enumerated Attributes (Fixed Values) - -**When to use:** Attributes with a known, limited set of values - -**Examples:** -- Clearance levels: `public`, `confidential`, `secret`, `top_secret` -- Departments: `engineering`, `finance`, `sales`, `hr` -- Regions: `us-west`, `us-east`, `eu-central`, `ap-south` - -**Definition:** -```json -{ - "namespace": "example.com", - "name": "clearance", - "rule": "HIERARCHY", - "values": [ - {"value": "public"}, - {"value": "confidential"}, - {"value": "secret"}, - {"value": "top_secret"} - ] -} -``` - -**Subject Mapping:** -```json -{ - "attribute_value_id": "attr-clearance-secret", - "actions": ["DECRYPT"], - "subject_condition_set": { - "subject_sets": [{ - "condition_groups": [{ - "boolean_operator": 1, - "conditions": [{ - "subject_external_selector_value": ".clearance_level", - "operator": 1, - "subject_external_values": ["secret", "top_secret"] - }] - }] - }] - } -} -``` - -### Dynamic Attributes (Freeform Values) - -**When to use:** Attributes with unbounded, user-specific values - -**Examples:** -- Email addresses: `alice@example.com`, `bob@company.org` -- User IDs: `user-12345`, `service-account-xyz` -- Project codes: `proj-2024-Q1-alpha`, `initiative-mars` +All attribute values in OpenTDF must be explicitly created before they can be used — there is no "freeform" or "dynamic" attribute value type. Each `attribute_value_id` in a Subject Mapping must reference an existing, named value. The flexibility comes from how Subject Condition Sets match entity claims. -**Definition (NO values array):** -```json -{ - "namespace": "example.com", - "name": "owner_email", - "rule": "ANY_OF" - // Note: No "values" array - accepts any string -} -``` - -**Creating Dynamic Attribute Value:** - -When encrypting, you can create attribute values on-the-fly: - -```bash -# Create attribute value for specific email -otdfctl policy attributes values create \ - --attribute-id \ - --value "alice@example.com" +### Option 1: Exact Match (One Mapping Per User) -# Encrypt with this specific value -otdfctl encrypt file.txt -o file.tdf \ - --attr https://example.com/attr/owner_email/value/alice@example.com -``` - -**Subject Mapping for Dynamic Values:** - -Here's the key insight: **You create Subject Mappings for the PATTERN, not every individual value.** - -**Option 1: Exact Match (One Mapping Per User)** ```json { - "attribute_value_id": "attr-owner-alice", // Points to value="alice@example.com" - "actions": ["DECRYPT"], + "attribute_value_id": "attr-owner-alice", + "actions": ["read"], "subject_condition_set": { "subject_sets": [{ "condition_groups": [{ @@ -556,23 +483,23 @@ Here's the key insight: **You create Subject Mappings for the PATTERN, not every } ``` -**Problem:** This requires creating a new Subject Mapping for every user. Not scalable. +**Limitation:** Requires creating a new Subject Mapping (and a corresponding attribute value) for every user. Not scalable for large user sets. -**Option 2: Pattern-Based Access (Recommended)** +### Option 2: Pattern-Based Access (Recommended) -Create Subject Mappings based on patterns in token claims (like email domains) that cover multiple users: +Use `IN_CONTAINS` (operator `3`) to match token claim substrings, covering many users with one Subject Mapping: ```json { "attribute_value_id": "attr-company-employees", - "actions": ["DECRYPT"], + "actions": ["read"], "subject_condition_set": { "subject_sets": [{ "condition_groups": [{ "boolean_operator": 1, "conditions": [{ "subject_external_selector_value": ".email", - "operator": 3, // IN_CONTAINS (substring match) + "operator": 3, "subject_external_values": ["@example.com"] }] }] @@ -581,37 +508,37 @@ Create Subject Mappings based on patterns in token claims (like email domains) t } ``` -→ Anyone with an email containing `@example.com` in their token gets entitlement for the `company/employees` attribute +→ Anyone whose token contains an email with `@example.com` receives entitlement for the `company/employees` attribute value. -**Key:** One Subject Mapping covers all matching users. The IdP provides the claims (email addresses) and the condition evaluates the pattern at decision time. +**Key:** One Subject Mapping covers all matching users. The condition evaluates the claim value at decision time — no per-user configuration needed. -:::tip Advanced Pattern: True Per-User Self-Service -For true self-service access where users can only access resources tagged specifically with their own identity (e.g., Alice accesses files tagged `owner/alice@example.com` but not `owner/bob@example.com`), the standard Subject Mapping system alone is insufficient. +### Pre-Creating Attribute Values for Specific Identities -Subject Mappings grant **entitlements** to attribute values, but they cannot dynamically match a user's claim to a resource's attribute value at decision time. A Subject Mapping must reference a specific `attribute_value_id` that already exists. +For use cases like tagging a resource with an owner's identity, you explicitly create the attribute value before encrypting: -**This pattern requires custom logic** in one of these places: -- **Entity Resolution Service**: Custom resolver that dynamically creates entitlements matching the user's identity claim -- **Authorization Service**: Custom decision logic that compares token claims directly to resource attribute values -- **Application layer**: Pre-authorization filtering based on user identity before calling OpenTDF +```bash +# Create attribute value for a specific user +otdfctl policy attributes values create \ + --attribute-id \ + --value "alice-at-example-com" -**Conceptual workflow:** -1. Define attribute without value enumeration (`owner_email`) -2. When encrypting, create attribute value dynamically: `owner_email/alice@example.com` -3. At decryption time, custom logic grants access when token's `.email` claim matches the resource's `owner_email` value +# Encrypt, binding the resource to that value +otdfctl encrypt file.txt -o file.tdf \ + --attr https://example.com/attr/owner/value/alice-at-example-com +``` -This requires development work beyond standard Subject Mapping configuration. Consult OpenTDF maintainers for implementation guidance. -::: +Then create a Subject Mapping linking Alice's email claim to that value: -### Comparison Table +```bash +otdfctl policy subject-mappings create \ + --attribute-value-id \ + --action read \ + --subject-condition-set-new '[{"condition_groups":[{"boolean_operator":1,"conditions":[{"subject_external_selector_value":".email","operator":1,"subject_external_values":["alice@example.com"]}]}]}]' +``` -| Aspect | Enumerated Attributes | Dynamic Attributes | -|--------|----------------------|-------------------| -| **Values defined** | Yes (fixed list) | No (any string) | -| **Subject Mapping** | Maps claims to specific values | Maps claims to value pattern | -| **Scalability** | Good for 5-100 values | Required for 1000s+ values | -| **Example** | `clearance/secret` | `owner/alice@example.com` | -| **Use case** | Role-based access | User-specific access | +:::note Attribute value naming constraint +Attribute values must match `^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$` — alphanumeric with hyphens and underscores, no special characters. Email addresses (with `@` and `.`) must be normalized to a valid format (e.g., `alice-at-example-com`). +::: ## IdP Integration Examples @@ -710,81 +637,6 @@ This requires development work beyond standard Subject Mapping configuration. Co **Logic:** `(group contains "/finance/") AND (role is "admin")` -### Auth0 - -**Common Auth0 Token Claims:** -```json -{ - "sub": "auth0|507f1f77bcf86cd799439011", - "email": "alice@example.com", - "email_verified": true, - "nickname": "alice", - "https://example.com/roles": ["editor", "viewer"], - "https://example.com/department": "engineering", - "https://example.com/clearance": "secret" -} -``` - -:::info Auth0 Namespaced Claims -Auth0 requires custom claims to use namespaced keys (e.g., `https://example.com/roles`). Use these in Subject Condition Sets with the full namespace. -::: - -#### Example 1: Map Auth0 Custom Roles - -**IdP Configuration:** -- User has custom claim: `https://example.com/roles: ["editor"]` - -**Subject Condition Set:** -```json -{ - "subject_sets": [{ - "condition_groups": [{ - "boolean_operator": 1, - "conditions": [{ - "subject_external_selector_value": ".\"https://example.com/roles\"", - "operator": 1, - "subject_external_values": ["editor", "admin"] - }] - }] - }] -} -``` - -:::warning Escaping Special Characters -JMESPath requires escaping keys with special characters. Use `.\""https://example.com/roles\"` for namespaced Auth0 claims. -::: - -#### Example 2: Map Auth0 Metadata - -**IdP Configuration:** -- User has `app_metadata`: `{"clearance_level": "confidential", "department": "finance"}` - -**Auth0 Rule to Add to Token:** -```javascript -function addMetadataToToken(user, context, callback) { - const namespace = 'https://example.com/'; - context.idToken[namespace + 'clearance'] = user.app_metadata.clearance_level; - context.idToken[namespace + 'department'] = user.app_metadata.department; - callback(null, user, context); -} -``` - -**Subject Condition Set:** -```json -{ - "subject_sets": [{ - "condition_groups": [{ - "boolean_operator": 1, - "conditions": [{ - "subject_external_selector_value": ".\"https://example.com/clearance\"", - "operator": 1, - "subject_external_values": ["confidential", "secret", "top_secret"] - }] - }] - }] -} -``` - ### Okta **Common Okta Token Claims:** @@ -935,7 +787,7 @@ otdfctl policy attributes values create \ ```bash otdfctl policy subject-mappings create \ --attribute-value-id 4c63e72a-2db9-434c-8ef6-e451473dbfe0 \ - --action-standard DECRYPT \ + --action read \ --subject-condition-set-id 3c56a6c9-9635-427f-b808-5e8fd395802c ``` @@ -982,14 +834,15 @@ otdfctl policy subject-condition-sets list **Check action format:** ```bash -# Correct ---action-standard DECRYPT +# Correct: use --action with a standard action name +--action read -# Also correct ---action-standard "STANDARD_ACTION_DECRYPT" +# Also correct: use action ID (UUID) +--action 891cfe85-b381-4f85-9699-5f7dbfe2a9ab -# Wrong - don't mix action types ---action-standard DECRYPT --action-custom "custom.action" +# Deprecated flags (still accepted but migrate away from these) +# --action-standard DECRYPT → use --action read +# --action-custom "download" → use --action download ``` ### Error: Token Claim Not Appearing in Entitlements @@ -1004,25 +857,37 @@ otdfctl policy subject-condition-sets list echo "" | base64 -d ``` -**2. Check JMESPath selector:** +**2. Check selector with `otdfctl dev selectors`:** -The selector must match the exact structure of your token. Test with JMESPath: +OpenTDF uses a custom [flattening syntax](https://github.com/opentdf/platform/blob/main/lib/flattening/flatten.go): keys are prefixed with `.`, nested paths use `.key.subkey`, and list items use `[0]` or `[]` for any index. -```javascript -// Token: -{ - "user": { - "profile": { - "department": "finance" - } - } -} +Use `otdfctl dev selectors generate` to see all valid selectors for a given JSON or JWT: -// Correct selector: -".user.profile.department" +```bash +# From a JSON object +otdfctl dev selectors generate --subject '{"role":"admin","groups":["engineering","senior-staff"]}' -// Wrong: -".department" // Won't find nested value +# From a JWT token +otdfctl dev selectors generate --subject "" +``` + +Then test a specific selector against your token with `otdfctl dev selectors test`: + +```bash +otdfctl dev selectors test \ + --subject '{"role":"admin","groups":["engineering"]}' \ + --selector '.role' \ + --selector '.groups[]' +``` + +The flattening syntax for nested structures: + +``` +Token: Selector: +{"user":{"profile": ".user.profile.department" ✅ + {"department":"finance"}}} +{"department":"finance"} ".department" ✅ + ".user.department" ❌ (wrong nesting) ``` **3. Check operator type:** @@ -1047,21 +912,24 @@ Contact your OpenTDF administrator to enable debug logging for Subject Mapping e ### Error: User Has Entitlement But Still Gets DENY -**Symptom:** Authorization logs show: -``` -jwtentity-1 (user): PERMIT -jwtentity-0 (client): DENY -Overall decision: DENY -``` +In `GetDecision` flows, only **`CATEGORY_SUBJECT`** entities participate in the access decision ([source](https://github.com/opentdf/platform/blob/main/service/authorization/authorization.go)). `CATEGORY_ENVIRONMENT` entities (the OIDC client in a user-auth flow) are tracked in audit logs but do NOT affect the decision outcome. -**Cause:** **Both entities need Subject Mappings** when using SDK clients. +**If the user (`CATEGORY_SUBJECT`) has the required entitlement and still gets DENY**, check the following: -**Solution:** +**Cause 1: Subject Condition Set doesn't match the token** -Create Subject Mapping for the **client entity** based on `.clientId`: +The selector or operator doesn't match the actual claim structure. Use `otdfctl dev selectors generate` to inspect your token: ```bash -# Create condition set for client +otdfctl dev selectors generate --subject '{"role":"admin","email":"alice@example.com"}' +``` + +**Cause 2: Service account flow — the client IS the subject** + +In **client credentials (service account) flows**, there is no separate user — the client itself is assigned `CATEGORY_SUBJECT` and needs Subject Mappings. This is the case when using NPEs (Non-Person Entities) authenticating directly with client credentials: + +```bash +# Create Subject Mapping for a service account client otdfctl policy subject-condition-sets create \ --subject-sets '[ { @@ -1070,16 +938,15 @@ otdfctl policy subject-condition-sets create \ "conditions": [{ "subject_external_selector_value": ".clientId", "operator": 1, - "subject_external_values": ["my-app-client-id"] + "subject_external_values": ["my-service-account-client-id"] }] }] } ]' -# Create subject mapping for client otdfctl policy subject-mappings create \ - --attribute-value-id \ - --action-standard DECRYPT \ + --attribute-value-id \ + --action read \ --subject-condition-set-id ``` @@ -1122,12 +989,12 @@ When Subject Mappings aren't working: - [ ] Verify Subject Condition Set exists (`list` command) - [ ] Verify Attribute Value exists (`attributes values list`) - [ ] Verify Subject Mapping exists (`subject-mappings list`) -- [ ] Check JMESPath selector matches token structure +- [ ] Check selector expression matches token structure (use `otdfctl dev selectors generate` to verify) - [ ] Confirm operator type (IN vs IN_CONTAINS) - [ ] Test with simple condition first (single claim match) -- [ ] For SDK clients: Verify client entity has Subject Mapping +- [ ] For service account (client credentials) flows: Verify the client has a Subject Mapping (it's `CATEGORY_SUBJECT` in this case) - [ ] Check attribute definition rule (HIERARCHY, ANY_OF, ALL_OF) -- [ ] Verify action matches operation (DECRYPT for decryption) +- [ ] Verify action matches operation (`read` for TDF decryption) ## Best Practices @@ -1155,12 +1022,12 @@ otdfctl policy subject-condition-sets create \ # Reuse for multiple attribute values otdfctl policy subject-mappings create \ --attribute-value-id \ - --action-standard DECRYPT \ + --action read \ --subject-condition-set-id otdfctl policy subject-mappings create \ --attribute-value-id \ - --action-standard DECRYPT \ + --action read \ --subject-condition-set-id ``` @@ -1173,14 +1040,18 @@ Leverage `HIERARCHY` rule for implicit access: "name": "clearance", "rule": "HIERARCHY", "values": [ - {"value": "public", "order": 1}, - {"value": "confidential", "order": 2}, - {"value": "secret", "order": 3}, - {"value": "top_secret", "order": 4} + {"value": "public"}, + {"value": "confidential"}, + {"value": "secret"}, + {"value": "top_secret"} ] } ``` +:::note Hierarchy ordering +For `HIERARCHY` attributes, precedence is determined by the **order values appear in the array** — first element is lowest, last is highest. There is no `order` field; array position is authoritative. +::: + **Subject Mapping for top_secret:** - User gets entitlement: `clearance/top_secret` - Can access: `top_secret`, `secret`, `confidential`, `public` (all lower levels) @@ -1206,26 +1077,22 @@ create-subject-mapping --for-user charlie } ``` -### 4. Separate PE and NPE Mappings +### 4. Subject Mappings for Service Accounts (NPE) -Create distinct Subject Mappings for: -- **Person Entities** (users): Based on `.email`, `.role`, `.groups` -- **Non-Person Entities** (services): Based on `.clientId`, `.scope` +In a standard user-authenticated flow, only the **user** (`CATEGORY_SUBJECT`) needs Subject Mappings. The OIDC client is `CATEGORY_ENVIRONMENT` and is not evaluated in access decisions. -```bash -# For human users -otdfctl policy subject-mappings create \ - --attribute-value-id \ - --action-standard DECRYPT \ - --subject-condition-set-id +For **client credentials flows** (service accounts authenticating directly, with no human user), the client is assigned `CATEGORY_SUBJECT` and does need Subject Mappings. The selector to use depends on your IdP — for Keycloak, it is typically `.clientId`: -# For service accounts +```bash +# For service accounts (client credentials flow, Keycloak ERS) otdfctl policy subject-mappings create \ --attribute-value-id \ - --action-standard DECRYPT \ - --subject-condition-set-id + --action read \ + --subject-condition-set-id ``` +The exact claim key for the client ID varies by IdP and ERS configuration — use `otdfctl dev selectors generate` with your actual token to find the right selector. + ### 5. Test in Isolation When debugging, test Subject Mappings individually: @@ -1278,10 +1145,10 @@ otdfctl decrypt test.tdf 3. Encrypt resources with multiple attributes (`--attr X --attr Y`) 4. Subject Mappings grant partial entitlements; decision evaluates all attributes -**Set up Dynamic User Attributes:** -1. Define attributes without value enumeration -2. Encrypt with user-specific values (`owner_email/alice@example.com`) -3. Create pattern-based Subject Mappings (substring match on email domain) +**Scale Subject Mappings with pattern matching:** +1. Define attribute and its values +2. Encrypt with the relevant attribute value FQN +3. Create pattern-based Subject Mappings using `IN_CONTAINS` (substring match) to cover many users with one condition ## FAQ @@ -1305,7 +1172,7 @@ otdfctl decrypt test.tdf **Q: Why do I see both jwtentity-0 and jwtentity-1 in authorization logs?** -**A:** `jwtentity-0` is the client application (NPE), `jwtentity-1` is the user (PE). Both participate in authorization decisions when using SDK clients. Both may need Subject Mappings depending on your access model. +**A:** When using the Keycloak ERS, two entities are created from a JWT token: the OIDC client (`jwtentity-0-clientid-{id}`, `CATEGORY_ENVIRONMENT`) and the authenticated user (`jwtentity-1-username-{name}`, `CATEGORY_SUBJECT`). In `GetDecision` flows, only `CATEGORY_SUBJECT` entities are evaluated. The client entity is logged for audit purposes but does not affect the decision. Only the user entity needs Subject Mappings for standard TDF decrypt flows. See [authorization.go](https://github.com/opentdf/platform/blob/main/service/authorization/authorization.go) for the implementation. **Q: Can I update a Subject Mapping without recreating it?** From d06dc61d17678cc6dd27eb0a500a6190deb34f32 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Tue, 24 Feb 2026 08:57:10 -0800 Subject: [PATCH 08/13] docs(subject-mapping-guide): add content from community discussions #2930, #1877, #1483, #1021 - Add --subject-sets-file-json alternative for complex condition sets (#1483) - Add --allow-traversal flag explanation for encrypt-before-value-exists pattern (#2930, #1877) - Link to ADR #1181 (typed entities) to explain SUBJECT/ENVIRONMENT design intent (#1021) Co-Authored-By: Claude Sonnet 4.6 --- docs/guides/subject-mapping-guide.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/guides/subject-mapping-guide.md b/docs/guides/subject-mapping-guide.md index 928d40b5..73c0e0f5 100644 --- a/docs/guides/subject-mapping-guide.md +++ b/docs/guides/subject-mapping-guide.md @@ -540,6 +540,22 @@ otdfctl policy subject-mappings create \ Attribute values must match `^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$` — alphanumeric with hyphens and underscores, no special characters. Email addresses (with `@` and `.`) must be normalized to a valid format (e.g., `alice-at-example-com`). ::: +:::tip Encrypt before the value exists with `--allow-traversal` +If your workflow creates the attribute value at encrypt time (or shortly after), set `--allow-traversal` on the attribute definition: + +```bash +otdfctl policy attributes create \ + --namespace \ + --name owner \ + --rule ANY_OF \ + --allow-traversal +``` + +With `allow_traversal=true`, a TDF can be encrypted referencing `owner/alice-at-example-com` even if that value doesn't exist in policy yet — as long as a KAS key is mapped to the attribute definition. Decryption will fail until the value is created and Subject Mappings are in place. This is useful for "encrypt first, provision access later" workflows. + +Source: [`attributes.proto:149-155`](https://github.com/opentdf/platform/blob/main/service/policy/attributes/attributes.proto) +::: + ## IdP Integration Examples ### Keycloak @@ -759,6 +775,13 @@ otdfctl policy subject-condition-sets create \ ]' ``` +For complex condition sets, use `--subject-sets-file-json` with a path to a JSON file instead of inline JSON: + +```bash +# scs.json contains the same array as above +otdfctl policy subject-condition-sets create --subject-sets-file-json scs.json +``` + **Save the ID from output:** ```console SUCCESS Created SubjectConditionSet [3c56a6c9-9635-427f-b808-5e8fd395802c] @@ -912,7 +935,7 @@ Contact your OpenTDF administrator to enable debug logging for Subject Mapping e ### Error: User Has Entitlement But Still Gets DENY -In `GetDecision` flows, only **`CATEGORY_SUBJECT`** entities participate in the access decision ([source](https://github.com/opentdf/platform/blob/main/service/authorization/authorization.go)). `CATEGORY_ENVIRONMENT` entities (the OIDC client in a user-auth flow) are tracked in audit logs but do NOT affect the decision outcome. +In `GetDecision` flows, only **`CATEGORY_SUBJECT`** entities participate in the access decision ([source](https://github.com/opentdf/platform/blob/main/service/authorization/authorization.go)). `CATEGORY_ENVIRONMENT` entities (the OIDC client in a user-auth flow) are tracked in audit logs but do NOT affect the decision outcome. This is intentional design — see [ADR: Add typed Entities (#1181)](https://github.com/opentdf/platform/issues/1181) for the rationale. **If the user (`CATEGORY_SUBJECT`) has the required entitlement and still gets DENY**, check the following: From aa8fe5891a0c2b9f3898ca4474c5a1d7282ff134 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Tue, 24 Feb 2026 12:11:41 -0800 Subject: [PATCH 09/13] fix(subject-mapping-guide): use [] selector for array claims, explain Keycloak group paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .groups → .groups[] for Keycloak and Okta group examples - .realm_access.roles → .realm_access.roles[] for Keycloak role examples - Add scalar vs array selector reference table with explanation - Fix troubleshooting array example with explanatory comment - Add note on Keycloak Full Group Path mapper and why values include slashes Evidence: lib/flattening/flatten.go produces .key[], .key[0] for arrays — there is no .key key. Confirmed by subject_mapping_builtin_actions_test.go:85 which uses SubjectExternalSelectorValue: ".groups[]" Co-Authored-By: Claude Sonnet 4.6 --- docs/guides/subject-mapping-guide.md | 32 ++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/docs/guides/subject-mapping-guide.md b/docs/guides/subject-mapping-guide.md index 73c0e0f5..e3bf1641 100644 --- a/docs/guides/subject-mapping-guide.md +++ b/docs/guides/subject-mapping-guide.md @@ -268,6 +268,18 @@ SubjectConditionSet └─ SubjectExternalValues (Values to match) ``` +### Selectors: Scalar vs. Array Claims + +The selector syntax depends on whether the token claim is a **scalar** (string) or an **array**: + +| Claim type | Example token | Selector | +|------------|--------------|---------| +| Scalar string | `"role": "admin"` | `.role` | +| Array | `"groups": ["admin", "user"]` | `.groups[]` | +| Nested scalar | `"realm_access": {"roles": [...]}` | `.realm_access.roles[]` | + +**Using `.groups` (without `[]`) on an array claim will silently match nothing.** The flattening library ([`lib/flattening/flatten.go`](https://github.com/opentdf/platform/blob/main/lib/flattening/flatten.go)) produces keys like `.groups[0]`, `.groups[1]`, and `.groups[]` for array elements — there is no `.groups` key. Use `otdfctl dev selectors generate` to see exactly what keys your token produces. + ### Operators Explained | Operator | Value | Behavior | Example | @@ -590,7 +602,7 @@ Source: [`attributes.proto:149-155`](https://github.com/opentdf/platform/blob/ma "condition_groups": [{ "boolean_operator": 1, "conditions": [{ - "subject_external_selector_value": ".realm_access.roles", + "subject_external_selector_value": ".realm_access.roles[]", "operator": 1, "subject_external_values": ["admin"] }] @@ -606,6 +618,10 @@ Source: [`attributes.proto:149-155`](https://github.com/opentdf/platform/blob/ma **IdP Configuration:** - User "bob" is in Keycloak group: `/finance/senior` +:::note Keycloak group path format +When the Keycloak "Group Membership" mapper has **Full Group Path** enabled, group names in the token include a leading slash (e.g., `/finance/senior`). The values in `subject_external_values` must match what is actually in the token. If your Keycloak mapper has Full Group Path disabled, groups appear without slashes (e.g., `finance`). The trailing slash in `/finance/` below makes the `IN_CONTAINS` match more precise, preventing false matches against group names that share a prefix (e.g., `/finance-external`). +::: + **Subject Condition Set:** ```json { @@ -613,7 +629,7 @@ Source: [`attributes.proto:149-155`](https://github.com/opentdf/platform/blob/ma "condition_groups": [{ "boolean_operator": 1, "conditions": [{ - "subject_external_selector_value": ".groups", + "subject_external_selector_value": ".groups[]", "operator": 3, "subject_external_values": ["/finance/"] }] @@ -636,12 +652,12 @@ Source: [`attributes.proto:149-155`](https://github.com/opentdf/platform/blob/ma "boolean_operator": 1, "conditions": [ { - "subject_external_selector_value": ".groups", + "subject_external_selector_value": ".groups[]", "operator": 3, "subject_external_values": ["/finance/"] }, { - "subject_external_selector_value": ".realm_access.roles", + "subject_external_selector_value": ".realm_access.roles[]", "operator": 1, "subject_external_values": ["admin"] } @@ -680,7 +696,7 @@ Source: [`attributes.proto:149-155`](https://github.com/opentdf/platform/blob/ma "condition_groups": [{ "boolean_operator": 1, "conditions": [{ - "subject_external_selector_value": ".groups", + "subject_external_selector_value": ".groups[]", "operator": 1, "subject_external_values": ["Engineering", "Product"] }] @@ -921,12 +937,14 @@ Token: Selector: "groups": ["admin", "user"] } -// Use IN to check array membership: +// Use .groups[] (not .groups) to match each element: { - "subject_external_selector_value": ".groups", + "subject_external_selector_value": ".groups[]", "operator": 1, "subject_external_values": ["admin"] } + +// .groups (without []) matches NOTHING for an array — it only works for scalar strings ``` **4. Enable debug logging:** From 47f6ae8aa67b98896c9abb07a77b0a7b6ed51a88 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Tue, 24 Feb 2026 12:48:39 -0800 Subject: [PATCH 10/13] fix(vale): repair broken regex patterns and add missing vocab terms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Scope (?i)tdf → (?i:tdf) to prevent Go regexp from applying case-insensitive flag to all subsequent vocab entries (root cause of spurious Vale.Terms errors like "Use 'JWT' instead of 'IdP'") - Fix invalid PCRE conditionals: rewrap(?(s)) → rewraps? and requester(?('s)) → requester('s)? - Add CI, [Ss]hellcheck, and Okta to suppress false spelling errors Co-Authored-By: Claude Sonnet 4.6 --- .github/vale-styles/config/Vocab/Opentdf/accept.txt | 9 ++++++--- .../vale-styles/config/vocabularies/Opentdf/accept.txt | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/vale-styles/config/Vocab/Opentdf/accept.txt b/.github/vale-styles/config/Vocab/Opentdf/accept.txt index 6062987e..3ab7371f 100644 --- a/.github/vale-styles/config/Vocab/Opentdf/accept.txt +++ b/.github/vale-styles/config/Vocab/Opentdf/accept.txt @@ -1,7 +1,7 @@ Docusaurus [Oo]tdfctl API -(?i)tdf +(?i:tdf) [Nn]amespace Keycloak Virtru @@ -20,5 +20,8 @@ assertation [Dd]issem JavaScript Autoconfigure -requester(?('s)) -rewrap(?(s)) \ No newline at end of file +requester('s)? +rewraps? +CI +[Ss]hellcheck +Okta diff --git a/.github/vale-styles/config/vocabularies/Opentdf/accept.txt b/.github/vale-styles/config/vocabularies/Opentdf/accept.txt index 6062987e..3ab7371f 100644 --- a/.github/vale-styles/config/vocabularies/Opentdf/accept.txt +++ b/.github/vale-styles/config/vocabularies/Opentdf/accept.txt @@ -1,7 +1,7 @@ Docusaurus [Oo]tdfctl API -(?i)tdf +(?i:tdf) [Nn]amespace Keycloak Virtru @@ -20,5 +20,8 @@ assertation [Dd]issem JavaScript Autoconfigure -requester(?('s)) -rewrap(?(s)) \ No newline at end of file +requester('s)? +rewraps? +CI +[Ss]hellcheck +Okta From d688d4d79facf57ea70e5593520bf7836133c052 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Tue, 24 Feb 2026 15:53:15 -0800 Subject: [PATCH 11/13] docs(subject-mapping-guide): improve architecture section readability and accuracy - Add NIST roles table with actionable "How to use it" links - Update high-level diagram to use full service names - Move NIST table below the diagram - Add per-step-group intro sentences explaining each phase - Fix JWT claims note: clarify email/role/groups are configured, not standard - Remove duplicate prose in steps 3-5 and 6-9 - Add plain-language intro to Subject Mapping JSON example - Fix ERS description: called by KAS, not Authorization Service Co-Authored-By: Claude Sonnet 4.6 --- docs/guides/subject-mapping-guide.md | 116 +++++++++++++++++++++------ 1 file changed, 93 insertions(+), 23 deletions(-) diff --git a/docs/guides/subject-mapping-guide.md b/docs/guides/subject-mapping-guide.md index e3bf1641..ef9eed4d 100644 --- a/docs/guides/subject-mapping-guide.md +++ b/docs/guides/subject-mapping-guide.md @@ -23,8 +23,8 @@ This guide explains how OpenTDF connects user identities from your Identity Prov Many developers expect this direct flow: ``` -IdP User Attribute → OpenTDF Attribute → Access Decision - (role=admin) (clearance=top_secret) (PERMIT/DENY) +IdP User Attribute → OpenTDF Attribute → Access Decision +(role=admin) (clearance=top_secret) (PERMIT/DENY) ``` **This doesn't work.** OpenTDF attributes define **what can be accessed**, not **who can access it**. @@ -59,32 +59,59 @@ Subject Mappings answer: "Given this identity, what entitlements should they rec ### High-Level Data Flow ```mermaid +%%{init: {'sequence': {'boxMargin': 20}}}%% sequenceDiagram participant User - participant IdP as Identity Provider
(Keycloak/Auth0/Okta) - participant ERS as Entity Resolution
Service - participant Auth as Authorization
Service - participant Policy as Policy
Service + participant IdP as Identity Provider + box OpenTDF Services + participant KAS as Key Access Server + participant ERS as Entity Resolution Service + participant Auth as Authorization Service + participant Policy as Policy Service + end User->>IdP: 1. Authenticate - IdP->>User: 2. Return JWT token
{email, role, groups} + IdP->>User: 2. JWT token - User->>ERS: 3. Decrypt request + token - ERS->>ERS: 4. Parse token into
Entity Representation + User->>KAS: 3. Decrypt request + token + KAS->>ERS: 4. Resolve entity + ERS->>KAS: 5. Entity representation - ERS->>Auth: 5. GetEntitlements(entity) - Auth->>Policy: 6. Which Subject Mappings
match this entity? + KAS->>Auth: 6. GetDecision + Auth->>Policy: 7. Match Subject Mappings - Policy->>Policy: 7. Evaluate Subject
Condition Sets - Policy->>Auth: 8. Return entitled
attribute values + actions + Policy->>Policy: 8. Evaluate conditions + Policy->>Auth: 9. Return entitlements - Auth->>Auth: 9. Compare entitlements
vs resource attributes - Auth->>User: 10. PERMIT or DENY + Auth->>Auth: 10. Compare vs resource attrs + Auth->>KAS: 11. PERMIT or DENY + KAS->>User: 12. Release key or deny ``` +| Service | NIST Role | How to use it | +|---|---|---| +| Key Access Server | Policy Enforcement Point (PEP) | [Send a TDF decrypt request](/sdks/tdf) | +| Entity Resolution Service | Policy Information Point (PIP) | *Used by Key Access Server* | +| Authorization Service | Policy Decision Point (PDP) | [Get authorization decisions](/sdks/authorization) | +| Policy Service | Policy Administration Point (PAP) | [Configure subject mappings](/sdks/policy) | + +For a detailed look at how these services fit together, see the [Architecture page](/architecture). + ### Detailed Step-by-Step #### Step 1-2: User Authentication + +Your Identity Provider (IdP) — such as Keycloak, Auth0, or Okta — is the source of truth for who a user is; OpenTDF relies on the JWT it issues to evaluate whatever claims you've configured (for example, `email`, `role`, or `groups`) against your policies. + +```mermaid +sequenceDiagram + participant User + participant IdP as Identity Provider + + User->>IdP: 1. Authenticate + IdP->>User: 2. Return JWT token
{email, role, groups} +``` + ```json // User authenticates with IdP, receives JWT token { @@ -96,8 +123,22 @@ sequenceDiagram } ``` -#### Step 3-4: Entity Resolution -The token is parsed into an **Entity Representation** - a normalized view of the user's identity: +#### Step 3-5: KAS & Entity Resolution + +When a user requests to decrypt a TDF, the Key Access Server receives the request and asks the Entity Resolution Service to translate the raw JWT claims into a normalized entity representation that the authorization engine can evaluate. + +```mermaid +sequenceDiagram + participant User + participant KAS as Key Access Server (PEP) + participant ERS as Entity Resolution Service (PIP) + + User->>KAS: 3. Decrypt request + token + KAS->>ERS: 4. Resolve entity from token + ERS->>KAS: 5. Return entity representation +``` + +The resulting **Entity Representation** looks like this: ```json { @@ -122,9 +163,23 @@ In `GetDecision` flows, **only `CATEGORY_SUBJECT` entities participate in the ac Exception: when using client credentials (service account) flows, the service account is assigned `CATEGORY_SUBJECT` and does need Subject Mappings. ::: -#### Step 5-8: Subject Mapping Evaluation +#### Step 6-9: Subject Mapping Evaluation + +The Authorization Service asks the Policy Service which Subject Mappings match the entity's claims, evaluates the conditions, and assembles the set of attribute values the entity is entitled to access. + +```mermaid +sequenceDiagram + participant KAS as Key Access Server (PEP) + participant Auth as Authorization Service (PDP) + participant Policy as Policy Service (PAP) + + KAS->>Auth: 6. GetDecision(entity + resource attrs) + Auth->>Policy: 7. Which Subject Mappings
match this entity? + Policy->>Policy: 8. Evaluate Subject
Condition Sets + Policy->>Auth: 9. Return entitled
attribute values + actions +``` -The Authorization Service queries the Policy Service: "Which Subject Mappings apply to this entity?" +A Subject Mapping says: "grant access to this attribute value if the entity's claims match these conditions." Here's one that grants the `clearance/executive` attribute to anyone whose `.role` claim is `vice_president`, `ceo`, or `cfo`: **Subject Mapping Example:** ```json @@ -152,7 +207,20 @@ The Authorization Service queries the Policy Service: "Which Subject Mappings ap 2. Check if `"vice_president"` is IN `["vice_president", "ceo", "cfo"]` → ✅ TRUE 3. Grant entitlement: `clearance/executive` with `read` action -#### Step 9-10: Authorization Decision +#### Step 10-12: Authorization Decision + +The Authorization Service compares the entity's entitlements against the attributes required by the resource, then sends a PERMIT or DENY back to KAS — which either releases the decryption key or rejects the request. + +```mermaid +sequenceDiagram + participant Auth as Authorization Service (PDP) + participant KAS as Key Access Server (PEP) + participant User + + Auth->>Auth: 10. Compare entitlements
vs resource attributes + Auth->>KAS: 11. PERMIT or DENY + KAS->>User: 12. Release key (PERMIT)
or deny access (DENY) +``` ```json // Entity's Entitlements: @@ -200,8 +268,8 @@ graph TD E -->|Subject| G[PE + Subject
Human user in auth flow
✅ Attributes checked in decisions] E -->|Environment| H[PE + Environment
Logged-in operator
⚠️ Not evaluated in GetDecision] - F -->|Subject| I[NPE + Subject
Service account (client credentials flow)
✅ Attributes checked in decisions] - F -->|Environment| J[NPE + Environment
OIDC client in user-auth flow
⚠️ Not evaluated in GetDecision] + F -->|Subject| I["NPE + Subject
Service account (client credentials flow)
✅ Attributes checked in decisions"] + F -->|Environment| J["NPE + Environment
OIDC client in user-auth flow
⚠️ Not evaluated in GetDecision"] ``` ### Practical Examples @@ -549,7 +617,9 @@ otdfctl policy subject-mappings create \ ``` :::note Attribute value naming constraint -Attribute values must match `^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$` — alphanumeric with hyphens and underscores, no special characters. Email addresses (with `@` and `.`) must be normalized to a valid format (e.g., `alice-at-example-com`). +Attribute values must match `^[a-zA-Z0-9]([a-zA-Z0-9_-]{0,251}[a-zA-Z0-9])?$` — alphanumeric with hyphens and underscores (not at start or end), max 253 characters, no special characters. Email addresses (with `@` and `.`) must be normalized to a valid format (e.g., `alice-at-example-com`). + +Source: [`opentdf/platform` — `lib/identifier/policyidentifier.go`](https://github.com/opentdf/platform/blob/main/lib/identifier/policyidentifier.go) ::: :::tip Encrypt before the value exists with `--allow-traversal` From a5f49dbe7ae8c84229b9e15fef9e41b93ad9fd49 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Tue, 24 Feb 2026 17:15:01 -0800 Subject: [PATCH 12/13] =?UTF-8?q?docs(subject-mapping-guide):=20improve=20?= =?UTF-8?q?step-by-step=20UX=20=E2=80=94=20explain=20operator=20codes,=20f?= =?UTF-8?q?ix=20fake=20IDs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add inline explanation of boolean_operator and operator enum values in Step 1 with link to reference section - Add otdfctl policy attributes list command to Step 2 so users can find their attribute ID - Add substitution reminder in Step 3 (IDs shown are from previous steps) - Fix sm-789xyz fake placeholder to a proper UUID format in Steps 3 and 4 - Link prerequisites (platform running, otdfctl) to /quickstart Co-Authored-By: Claude Sonnet 4.6 --- docs/guides/subject-mapping-guide.md | 70 +++++++++++++++++++++------- 1 file changed, 53 insertions(+), 17 deletions(-) diff --git a/docs/guides/subject-mapping-guide.md b/docs/guides/subject-mapping-guide.md index ef9eed4d..2e502138 100644 --- a/docs/guides/subject-mapping-guide.md +++ b/docs/guides/subject-mapping-guide.md @@ -336,15 +336,15 @@ SubjectConditionSet └─ SubjectExternalValues (Values to match) ``` -### Selectors: Scalar vs. Array Claims +### Selectors: String vs. Array Claims -The selector syntax depends on whether the token claim is a **scalar** (string) or an **array**: +The selector syntax depends on whether the token claim is a **string** or an **array**: | Claim type | Example token | Selector | |------------|--------------|---------| -| Scalar string | `"role": "admin"` | `.role` | +| String | `"role": "admin"` | `.role` | | Array | `"groups": ["admin", "user"]` | `.groups[]` | -| Nested scalar | `"realm_access": {"roles": [...]}` | `.realm_access.roles[]` | +| Nested string | `"realm_access": {"roles": [...]}` | `.realm_access.roles[]` | **Using `.groups` (without `[]`) on an array claim will silently match nothing.** The flattening library ([`lib/flattening/flatten.go`](https://github.com/opentdf/platform/blob/main/lib/flattening/flatten.go)) produces keys like `.groups[0]`, `.groups[1]`, and `.groups[]` for array elements — there is no `.groups` key. Use `otdfctl dev selectors generate` to see exactly what keys your token produces. @@ -598,6 +598,7 @@ For use cases like tagging a resource with an owner's identity, you explicitly c ```bash # Create attribute value for a specific user +# attribute FQN: https://example.com/attr/owner otdfctl policy attributes values create \ --attribute-id \ --value "alice-at-example-com" @@ -610,6 +611,7 @@ otdfctl encrypt file.txt -o file.tdf \ Then create a Subject Mapping linking Alice's email claim to that value: ```bash +# attribute value FQN: https://example.com/attr/owner/value/alice-at-example-com otdfctl policy subject-mappings create \ --attribute-value-id \ --action read \ @@ -617,7 +619,9 @@ otdfctl policy subject-mappings create \ ``` :::note Attribute value naming constraint -Attribute values must match `^[a-zA-Z0-9]([a-zA-Z0-9_-]{0,251}[a-zA-Z0-9])?$` — alphanumeric with hyphens and underscores (not at start or end), max 253 characters, no special characters. Email addresses (with `@` and `.`) must be normalized to a valid format (e.g., `alice-at-example-com`). +Attribute values are embedded in a Fully Qualified Name (FQN) — a URL of the form `https://example.com/attr/owner/value/alice-at-example-com`. Because the value becomes part of a URL, special characters like `@` and `.` are not allowed. Values must match `^[a-zA-Z0-9]([a-zA-Z0-9_-]{0,251}[a-zA-Z0-9])?$` — alphanumeric with hyphens and underscores (not at start or end), max 253 characters. Email addresses must be normalized (e.g., `alice@example.com` → `alice-at-example-com`). + +This constraint only applies to attribute values stored in the Policy Service. JWT claim values used in subject condition sets (e.g., `subject_external_values: ["alice@example.com"]`) are plain strings with no such restriction. Source: [`opentdf/platform` — `lib/identifier/policyidentifier.go`](https://github.com/opentdf/platform/blob/main/lib/identifier/policyidentifier.go) ::: @@ -681,17 +685,34 @@ Source: [`attributes.proto:149-155`](https://github.com/opentdf/platform/blob/ma } ``` -**Result:** Alice gets entitlement for attribute value (e.g., `clearance/top_secret`) +**Subject Mapping** (links the condition set to an attribute value): +```json +// attribute value FQN: https://example.com/attr/clearance/value/top_secret +{ + "attribute_value_id": "", + "actions": ["read"], + "subject_condition_set": { + "subject_sets": [{ + "condition_groups": [{ + "boolean_operator": 1, + "conditions": [{ + "subject_external_selector_value": ".realm_access.roles[]", + "operator": 1, + "subject_external_values": ["admin"] + }] + }] + }] + } +} +``` + +**Result:** Alice's `.realm_access.roles[]` contains `admin` → condition matches → Alice is entitled to `clearance/top_secret` with `read` access #### Example 2: Map Keycloak Groups **IdP Configuration:** - User "bob" is in Keycloak group: `/finance/senior` -:::note Keycloak group path format -When the Keycloak "Group Membership" mapper has **Full Group Path** enabled, group names in the token include a leading slash (e.g., `/finance/senior`). The values in `subject_external_values` must match what is actually in the token. If your Keycloak mapper has Full Group Path disabled, groups appear without slashes (e.g., `finance`). The trailing slash in `/finance/` below makes the `IN_CONTAINS` match more precise, preventing false matches against group names that share a prefix (e.g., `/finance-external`). -::: - **Subject Condition Set:** ```json { @@ -708,6 +729,12 @@ When the Keycloak "Group Membership" mapper has **Full Group Path** enabled, gro } ``` +:::note Keycloak group path format +When the Keycloak "Group Membership" mapper has **Full Group Path** enabled, group names in the token include a leading slash (e.g., `/finance/senior`). The values in `subject_external_values` must match what is actually in the token. If your Keycloak mapper has Full Group Path disabled, groups appear without slashes (e.g., `finance`). + +The trailing slash in `/finance/` makes the `IN_CONTAINS` match more precise — it prevents false matches against groups that share a prefix (e.g., `/finance-external`). However, it also means a user in the `/finance` group exactly (with no sub-group) would **not** match, since `/finance` does not contain the substring `/finance/`. If you need to match both `/finance` and `/finance/senior`, use `/finance` without the trailing slash and accept the risk of prefix collisions, or add a second condition. +::: + **Result:** Bob gets entitlement for `department/finance` attribute #### Example 3: Combine Multiple Keycloak Claims @@ -839,12 +866,14 @@ Add custom claim mapping in Okta: ### Prerequisites -1. **OpenTDF Platform running** with authentication configured -2. **otdfctl installed and authenticated** +1. **[OpenTDF Platform running](/quickstart)** with authentication configured +2. **[otdfctl installed and authenticated](/quickstart)** 3. **Attributes and values created** (the resources you're protecting) ### Step 1: Create Subject Condition Set +This example matches any user whose `.email` claim contains `@example.com`. The numeric values are enum codes — `boolean_operator: 1` = AND (all conditions must be true), `operator: 3` = IN_CONTAINS (substring match). See [Operators Explained](#operators-explained) for the full list. + ```bash otdfctl policy subject-condition-sets create \ --subject-sets '[ @@ -876,7 +905,10 @@ SUCCESS Created SubjectConditionSet [3c56a6c9-9635-427f-b808-5e8fd395802c] ### Step 2: Get Attribute Value ID ```bash -# List attribute values for an attribute +# First, find your attribute ID +otdfctl policy attributes list + +# Then list its values otdfctl policy attributes values list \ --attribute-id @@ -893,6 +925,8 @@ otdfctl policy attributes values create \ ### Step 3: Create Subject Mapping +Replace the IDs below with your own from Steps 1 and 2. + ```bash otdfctl policy subject-mappings create \ --attribute-value-id 4c63e72a-2db9-434c-8ef6-e451473dbfe0 \ @@ -902,7 +936,7 @@ otdfctl policy subject-mappings create \ **Success:** ```console -SUCCESS Created SubjectMapping [sm-789xyz] +SUCCESS Created SubjectMapping [b7e2f1a4-3c8d-4e9b-a5f2-1d6c8b3e7f9a] ``` ### Step 4: Verify @@ -911,8 +945,8 @@ SUCCESS Created SubjectMapping [sm-789xyz] # List all subject mappings otdfctl policy subject-mappings list -# Get specific mapping details -otdfctl policy subject-mappings get --id sm-789xyz +# Get specific mapping details (replace with your subject mapping ID from Step 3) +otdfctl policy subject-mappings get --id b7e2f1a4-3c8d-4e9b-a5f2-1d6c8b3e7f9a ``` ## Troubleshooting @@ -1014,7 +1048,7 @@ Token: Selector: "subject_external_values": ["admin"] } -// .groups (without []) matches NOTHING for an array — it only works for scalar strings +// .groups (without []) matches NOTHING for an array — it only works for string claims ``` **4. Enable debug logging:** @@ -1131,11 +1165,13 @@ otdfctl policy subject-condition-sets create \ ]' # Reuse for multiple attribute values +# attribute value FQN: https://example.com/attr/project/value/alpha otdfctl policy subject-mappings create \ --attribute-value-id \ --action read \ --subject-condition-set-id +# attribute value FQN: https://example.com/attr/project/value/beta otdfctl policy subject-mappings create \ --attribute-value-id \ --action read \ From 7bef8ceebb20ed29ab88da5649ce49303479b073 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Tue, 24 Feb 2026 17:38:14 -0800 Subject: [PATCH 13/13] docs(subject-mapping-guide): address step-by-step UX friction points - Explain .email selector syntax and link to Selectors section - Clarify subject_external_values are matched against JWT claim values - Show attributes list first in Step 2; explain UUID format and clearance/secret display label - Explain --action read is the standard decrypt action - Add payoff sentence in Step 3 explaining what the mapping does at runtime - Add Step 4 guidance on what a correct mapping looks like and what null subject_condition_set means - Add Next Steps section pointing to TDF SDK and troubleshooting - Fix diagram box border cutoff with mirrorActors: false - Fix intro sentence for step-by-step section Co-Authored-By: Claude Sonnet 4.6 --- docs/guides/subject-mapping-guide.md | 30 +++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/docs/guides/subject-mapping-guide.md b/docs/guides/subject-mapping-guide.md index 2e502138..6ef462e9 100644 --- a/docs/guides/subject-mapping-guide.md +++ b/docs/guides/subject-mapping-guide.md @@ -59,7 +59,7 @@ Subject Mappings answer: "Given this identity, what entitlements should they rec ### High-Level Data Flow ```mermaid -%%{init: {'sequence': {'boxMargin': 20}}}%% +%%{init: {'sequence': {'boxMargin': 20, 'mirrorActors': false}}}%% sequenceDiagram participant User participant IdP as Identity Provider @@ -864,16 +864,22 @@ Add custom claim mapping in Okta: ## Creating Subject Mappings: Step-by-Step +This walkthrough assumes basic familiarity with OpenTDF. If you haven't set up OpenTDF yet, complete the [Quickstart](/quickstart) first. + ### Prerequisites -1. **[OpenTDF Platform running](/quickstart)** with authentication configured -2. **[otdfctl installed and authenticated](/quickstart)** +1. **[OpenTDF Platform running](/quickstart#step-2-install-opentdf)** with authentication configured +2. **[otdfctl installed and authenticated](/quickstart#step-3-create-profile--authenticate)** 3. **Attributes and values created** (the resources you're protecting) ### Step 1: Create Subject Condition Set This example matches any user whose `.email` claim contains `@example.com`. The numeric values are enum codes — `boolean_operator: 1` = AND (all conditions must be true), `operator: 3` = IN_CONTAINS (substring match). See [Operators Explained](#operators-explained) for the full list. +`subject_external_selector_value` is a path into the JWT your IdP issues — `.email` selects the top-level `email` claim. If your claim is named differently, use `otdfctl dev selectors generate --subject ""` to see all available selectors. See [Selectors: String vs. Array Claims](#selectors-string-vs-array-claims) for the full syntax. + +`subject_external_values` contains the strings to match against that claim value — in this case, any email address containing `@example.com`. + ```bash otdfctl policy subject-condition-sets create \ --subject-sets '[ @@ -905,7 +911,7 @@ SUCCESS Created SubjectConditionSet [3c56a6c9-9635-427f-b808-5e8fd395802c] ### Step 2: Get Attribute Value ID ```bash -# First, find your attribute ID +# First, find your attribute ID (a UUID in the output's id column) otdfctl policy attributes list # Then list its values @@ -918,7 +924,7 @@ otdfctl policy attributes values create \ --value "my-value" ``` -**Save the attribute value ID:** +**Save the attribute value ID** — the UUID on the left. The `attribute-name/value-name` on the right is just a display label: ```console 4c63e72a-2db9-434c-8ef6-e451473dbfe0 | clearance/secret ``` @@ -934,6 +940,10 @@ otdfctl policy subject-mappings create \ --subject-condition-set-id 3c56a6c9-9635-427f-b808-5e8fd395802c ``` +`--action read` is the standard action for TDF data access (decrypt). + +This mapping means: any user whose `.email` claim contains `@example.com` will be entitled to `read` access on data tagged with the `clearance/secret` attribute value. + **Success:** ```console SUCCESS Created SubjectMapping [b7e2f1a4-3c8d-4e9b-a5f2-1d6c8b3e7f9a] @@ -949,6 +959,16 @@ otdfctl policy subject-mappings list otdfctl policy subject-mappings get --id b7e2f1a4-3c8d-4e9b-a5f2-1d6c8b3e7f9a ``` +A correctly configured mapping will show the attribute value ID, action, and a non-empty `subject_condition_set` with your conditions. If `subject_condition_set` is `null` or missing, the condition set ID was not found — double-check the ID from Step 1. + +### Next Steps + +Your subject mapping is live. To use it: + +- **Encrypt and tag data** with the attribute value you mapped — see [TDF SDK](/sdks/tdf) +- **Test access** by decrypting as a user whose token matches your condition set +- **Troubleshoot unexpected DENY** — see [Troubleshooting](#troubleshooting) below + ## Troubleshooting ### Error: "resource relation invalid"