diff --git a/README.md b/README.md index 3e929f9a..a3f04374 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,28 @@ This command generates static content into the `build` directory and can be serv ### Preview Deployment -Deploy to the preview domain for testing changes before merging to production: +Deploy to a Surge preview domain for testing changes before merging to production. **A free Surge account is required** - you'll be prompted to sign up the first time you deploy. +**Important:** Each developer should use a unique preview domain name to avoid conflicts. Use a descriptive name based on your ticket number or feature: + +```bash +# Build the site +npm run build + +# Deploy to your unique preview URL +# Replace with your ticket number or feature name +npx surge build opentdf-docs-preview-.surge.sh ``` -$ npm run build && surge build opentdf-docs-preview.surge.sh + +**Examples:** +```bash +# Using ticket number +npx surge build opentdf-docs-preview-dspx-2345.surge.sh + +# Using feature description +npx surge build opentdf-docs-preview-troubleshooting-updates.surge.sh ``` -The preview will be available at https://opentdf-docs-preview.surge.sh/ +Your preview will be available at `https://opentdf-docs-preview-.surge.sh/` + +**Note:** The first time you deploy, Surge will prompt you to create a free account or login. diff --git a/code_samples/policy_code/create_attribute.mdx b/code_samples/policy_code/create_attribute.mdx index 1680efaf..0dddecc8 100644 --- a/code_samples/policy_code/create_attribute.mdx +++ b/code_samples/policy_code/create_attribute.mdx @@ -1,7 +1,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -
+
Create Attribute diff --git a/code_samples/policy_code/create_namespace.mdx b/code_samples/policy_code/create_namespace.mdx index e3d70158..19c0e250 100644 --- a/code_samples/policy_code/create_namespace.mdx +++ b/code_samples/policy_code/create_namespace.mdx @@ -1,7 +1,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -
+
Creating a Namespace diff --git a/code_samples/policy_code/create_subject_condition_set.mdx b/code_samples/policy_code/create_subject_condition_set.mdx index 3cb88b34..174519a7 100644 --- a/code_samples/policy_code/create_subject_condition_set.mdx +++ b/code_samples/policy_code/create_subject_condition_set.mdx @@ -1,7 +1,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -
+
Create Subject Condition Set diff --git a/code_samples/policy_code/create_subject_mapping.mdx b/code_samples/policy_code/create_subject_mapping.mdx index 685cd7e3..2ecc88e7 100644 --- a/code_samples/policy_code/create_subject_mapping.mdx +++ b/code_samples/policy_code/create_subject_mapping.mdx @@ -1,7 +1,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -
+
Create Subject Mapping diff --git a/code_samples/policy_code/list_attributes.mdx b/code_samples/policy_code/list_attributes.mdx index 23c224f5..89be381a 100644 --- a/code_samples/policy_code/list_attributes.mdx +++ b/code_samples/policy_code/list_attributes.mdx @@ -1,7 +1,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -
+
List Attributes diff --git a/code_samples/policy_code/list_namespaces.mdx b/code_samples/policy_code/list_namespaces.mdx index 827d05c6..e847dc0b 100644 --- a/code_samples/policy_code/list_namespaces.mdx +++ b/code_samples/policy_code/list_namespaces.mdx @@ -1,7 +1,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -
+
List Namespaces diff --git a/code_samples/policy_code/list_subject_mapping.mdx b/code_samples/policy_code/list_subject_mapping.mdx index 44150cf6..5b20dc2e 100644 --- a/code_samples/policy_code/list_subject_mapping.mdx +++ b/code_samples/policy_code/list_subject_mapping.mdx @@ -1,7 +1,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -
+
List Subject Mappings diff --git a/docs/getting-started/index.mdx b/docs/getting-started/index.mdx index d8bb51a3..e08fdf6d 100644 --- a/docs/getting-started/index.mdx +++ b/docs/getting-started/index.mdx @@ -29,6 +29,10 @@ import CodeBlock from '@theme/CodeBlock'; `} +:::tip Try the NEW Quickstart Guide +Looking for a faster way to get started? Check out our **[new Quickstart Guide](/quickstart)** with an automated installer and streamlined setup process! +::: + # Getting Started This guide will walk you through setting up a new platform locally and walk you through some of the basic concepts within the OpenTDF platform. diff --git a/docs/getting-started/managing-platform.mdx b/docs/getting-started/managing-platform.mdx index 2174c40b..0c76873b 100644 --- a/docs/getting-started/managing-platform.mdx +++ b/docs/getting-started/managing-platform.mdx @@ -1,6 +1,5 @@ --- -sidebar_position: 3 -unlisted: true +sidebar_position: 2 --- # Managing the Platform @@ -73,7 +72,11 @@ You can also verify the platform is healthy by visiting: ### TLS Certificate Verification -If you encounter TLS certificate errors like "Failed to validate TLS certificates," you have two options: +:::tip Canonical TLS Certificate Guide +This is the comprehensive guide for resolving all certificate/TLS issues across the platform, CLI, and SDKs. Both the [Quickstart](/quickstart#troubleshooting) and [SDK Troubleshooting](/sdks/troubleshooting) guides reference this section. +::: + +If you encounter TLS certificate errors like "Failed to validate TLS certificates," "certificate signed by unknown authority," or browser "Not Secure" warnings, you have two options: 1. **Set environment variable** (recommended - works with profiles): ```shell @@ -102,3 +105,189 @@ If you encounter TLS certificate errors like "Failed to validate TLS certificate - **Windows:** Manually import `caddy-root.crt` into the "Trusted Root Certification Authorities" store using Certificate Manager (`certmgr.msc`). **Important**: The `--tls-no-verify` flag doesn't work with profiles. Use the environment variable instead. + +### "Address already in use" Error + +**Symptom:** Services fail to start, docker compose shows port binding errors + +**Cause:** Port conflict - another service is using one of OpenTDF's required ports + +**Solution:** + +```shell +# Find what's using the port +lsof -i :9443 # or whichever port is conflicting + +# Option 1: Stop the conflicting service + +# Option 2: Reconfigure OpenTDF to use different ports +# Edit ~/.opentdf/platform/.env and change KEYCLOAK_PORT +# Then restart the platform +cd ~/.opentdf/platform && docker compose restart +``` + +### "Invalid character" Error During Authentication + +**Symptom:** +```console +ERROR: invalid character '<' looking for beginning of value +``` + +**Cause:** Port conflict - you're hitting a different service (like Rancher) instead of Keycloak + +**Solution:** Fix the port conflict as described above, then restart the platform + +### "PermissionDenied" After Creating Subject Mapping + +**Symptom:** Subject mapping created but decryption still fails with `PermissionDenied` + +**Investigation steps:** + +```shell +# 1. Verify mapping exists +otdfctl policy subject-mappings list + +# 2. Check your token claims match condition set +otdfctl auth print-access-token + +# 3. Verify field name in condition set +# Your condition set must use ".clientId" to match the JWT token claim + +# 4. Check platform logs for details +docker logs platform-platform-1 --tail 50 +``` + +### Services Won't Start + +**Investigation:** + +```shell +# Check all services +docker ps --filter name=platform- + +# View logs for specific service +docker logs platform-platform-1 +docker logs platform-keycloak-1 + +# Restart everything +cd ~/.opentdf/platform +docker compose down +docker compose up -d + +# Wait for health checks +watch 'docker ps --filter name=platform- --format "table {{.Names}}\t{{.Status}}"' +``` + +### Platform Not Responding + +**Symptom:** Commands timeout or connection refused + +**Solution:** + +```shell +# Check if platform is healthy +curl -k https://platform.opentdf.local:8443/healthz + +# Expected output: {"status":"SERVING"} + +# If not responding, check Docker logs +docker logs platform-platform-1 --tail 100 + +# Restart platform service +cd ~/.opentdf/platform && docker compose restart platform +``` + +## Quick Reference + +### Platform Management Commands + +```shell +# Start platform +cd ~/.opentdf/platform && docker compose up -d + +# Stop platform +cd ~/.opentdf/platform && docker compose down + +# View logs +docker logs -f platform-platform-1 + +# Check health +curl -k https://platform.opentdf.local:8443/healthz + +# Check service status +docker ps --filter name=platform- +``` + +### Common otdfctl Commands + +```shell +# Profiles +otdfctl profile list +otdfctl profile create [--tls-no-verify] +otdfctl profile set-default + +# Authentication +otdfctl auth client-credentials +otdfctl auth print-access-token + +# Namespaces +otdfctl policy attributes namespaces list +otdfctl policy attributes namespaces create --name +otdfctl policy attributes namespaces get --id + +# Attributes +otdfctl policy attributes create \ + --name -s --rule +otdfctl policy attributes get --id +otdfctl policy attributes list + +# Values +otdfctl policy attributes values create \ + -a --value +otdfctl policy attributes values get --id + +# Subject Mappings +otdfctl policy subject-condition-sets create -j +otdfctl policy subject-mappings create \ + --action read \ + --attribute-value-id \ + --subject-condition-set-id +otdfctl policy subject-mappings list + +# Encrypt/Decrypt +echo "data" | otdfctl encrypt -o file.tdf --attr +otdfctl encrypt -o --attr +otdfctl decrypt +``` + +### Default Ports + +OpenTDF exposes the following ports on your host machine: + +- **8080** - Platform API (direct HTTP/2 access) +- **8443** - Platform API (HTTPS via Caddy reverse proxy) +- **9443** - Keycloak (HTTPS via Caddy reverse proxy) +- **2019** - Caddy admin API and metrics + +PostgreSQL (5432) runs internally in the Docker network and is not exposed to the host. + +You can configure these ports in `~/.opentdf/platform/.env` if needed: +```shell +PLATFORM_HTTP_PORT=8080 +PLATFORM_PORT=8443 +KEYCLOAK_PORT=9443 +CADDY_ADMIN_PORT=2019 +``` + +### Access Points + +- Platform API: https://platform.opentdf.local:8443 +- Platform Health: https://platform.opentdf.local:8443/healthz +- Keycloak: https://keycloak.opentdf.local:9443 + +### Default Test Credentials + +- Client ID: `opentdf` +- Client Secret: `secret` +- Username: `user1` +- Password: `testuser123` diff --git a/docs/getting-started/quickstart.mdx b/docs/getting-started/quickstart.mdx index 23d69f10..9fa5aee3 100644 --- a/docs/getting-started/quickstart.mdx +++ b/docs/getting-started/quickstart.mdx @@ -2,7 +2,8 @@ title: Quickstart slug: /quickstart sidebar_label: Quickstart -unlisted: true +sidebar_position: 1 +pagination_next: sdks/quickstart/index --- import dockerComposeContent from '!!raw-loader!./docker-compose.yaml'; @@ -32,7 +33,7 @@ import CodeBlock from '@theme/CodeBlock'; # Getting Started -This guide will walk you through **encrypting and decrypting data** using OpenTDF tools. You'll learn how to secure sensitive information through encryption and control access using **attribute-based access controls (ABAC)**. We'll set up a sandbox [OpenTDF platform](/architecture) locally to demonstrate these core concepts. +This guide will walk you through **encrypting and decrypting data** using OpenTDF tools. You'll learn how to secure sensitive information through encryption and control access using **attribute-based access controls (ABAC)**. You'll set up a sandbox [OpenTDF platform](/architecture) locally to demonstrate these core concepts. :::note Time Commitment This quickstart is designed to take **10 minutes** for basic encryption/decryption, or **30 minutes** if you complete the full ABAC scenario. @@ -102,7 +103,7 @@ flowchart TD
-**The Implementation**: We'll set up attributes and entitlements, then watch OpenTDF make authorization decisions: +**The Implementation**: You'll set up attributes and entitlements, then watch OpenTDF make authorization decisions: - **Attributes** (how we classify data): - `department`: `finance`, `engineering` @@ -139,15 +140,23 @@ The OpenTDF Platform is the core service that enforces attribute-based access co ### Installation -We'll run a local instance using Docker. This includes the [Platform](https://github.com/opentdf/platform/tree/main), [Keycloak](https://www.keycloak.org/) (for user authentication), and [PostgreSQL](https://www.postgresql.org/) (for storing attributes and policies). +You'll run a local instance using Docker. This includes the [Platform](https://github.com/opentdf/platform/tree/main), [Keycloak](https://www.keycloak.org/) (for user authentication), and [PostgreSQL](https://www.postgresql.org/) (for storing attributes and policies). -:::warning -Not for production use. +:::danger ⚠️ EVALUATION AND DEVELOPMENT ONLY +**This quickstart setup is NOT intended for production use.** + +This configuration uses: +- Self-signed certificates +- Default credentials and secrets +- Insecure TLS settings +- No data persistence guarantees + +For production deployment guidance, visit: https://opentdf.io ::: -#### Step 1: Run Pre-flight Check (Optional) +#### Step 1: Check Prerequisites -Verify your system meets the requirements: +Before installing, verify your system is ready by running the pre-flight check script: ```shell curl -fsSL https://opentdf.io/quickstart/check.sh | bash @@ -155,11 +164,15 @@ curl -fsSL https://opentdf.io/quickstart/check.sh | bash Or download and run locally: ```shell -curl -fsSL https://docs.opentdf.io/quickstart/check.sh -o check.sh +curl -fsSL https://opentdf.io/quickstart/check.sh -o check.sh chmod +x check.sh ./check.sh ``` +:::note Sudo Password Prompt +You may be prompted for your sudo password during the "Checking permissions..." step. This is needed to verify write access to `/etc/hosts` (required for adding local domain mappings) and to check Docker permissions. +::: +
Expected output @@ -173,7 +186,7 @@ Checking container runtime... ✓ Docker found and running Checking docker-compose... -✓ docker compose plugin found (Docker Compose version v2.x.x-desktop.x) +✓ docker compose plugin found (Docker Compose version v2.29.1-desktop.1) Checking required tools... ✓ curl found @@ -182,12 +195,13 @@ Checking system resources... ✓ Memory: 16GB available Checking disk space... -✓ Disk space: 100GB available +✓ Disk space: 50GB available Checking port availability... ✓ Port 8080 is available ✓ Port 8443 is available ✓ Port 9443 is available +✓ Port 2019 is available Checking permissions... ✓ sudo access available (will prompt for password) @@ -203,6 +217,18 @@ Ready to install OpenTDF:
+The script checks: +- Docker is installed and running +- Docker Compose is available +- Required ports are available (8080, 8443, 9443, 2019) +- Sufficient disk space (10GB+) and RAM (4GB+) +- curl is installed +- sudo access for /etc/hosts modification + +:::tip Port Conflicts +Port 9443 commonly conflicts with Rancher and Kubernetes dashboards. If you encounter port conflicts, see [troubleshooting](/getting-started/managing-platform#address-already-in-use-error) to resolve them before proceeding. +::: + #### Step 2: Install OpenTDF Run the automated installer: @@ -213,7 +239,7 @@ curl -fsSL https://opentdf.io/quickstart/install.sh | bash Or download and run locally: ```shell -curl -fsSL https://docs.opentdf.io/quickstart/install.sh -o install.sh +curl -fsSL https://opentdf.io/quickstart/install.sh -o install.sh chmod +x install.sh ./install.sh ``` @@ -262,6 +288,10 @@ Password: [you'll be prompted for your password again] ✓ Caddy root CA imported to System keychain Note: You may need to restart your browser +:::note Certificate Import May Fail Silently +If the certificate import fails during installation, you'll need to import it manually in the next step. Symptoms include browser "Not Secure" warnings and needing `-k` flags with curl commands. +::: + → Running connectivity test... ✓ Platform is responding @@ -307,130 +337,89 @@ Password: [you'll be prompted for your password again]
-What this does: -- Downloads and installs the [otdfctl CLI](https://github.com/opentdf/otdfctl) -- Verifies Docker is running -- Adds entries to /etc/hosts (requires sudo) -- Downloads and starts all services (Platform, Keycloak, PostgreSQL, Caddy) -- Imports SSL certificates (requires sudo) -- Takes 3-5 minutes on first run +The installer will: +- Download and install the [otdfctl CLI](https://github.com/opentdf/otdfctl) +- Verify Docker is running +- Add entries to /etc/hosts (requires sudo) +- Download and start all services (Platform, Keycloak, PostgreSQL, Caddy) +- Import SSL certificates (requires sudo) +- Take 3-5 minutes on first run -You'll be prompted for your password twice: -1. To add entries to /etc/hosts -2. To import the SSL certificate +You'll be prompted for your password twice: once for /etc/hosts and once for SSL certificate import. -#### Step 3: Add otdfctl to Your PATH (Optional) +**Add otdfctl to your PATH** (optional but recommended): -Choose one option: - -Option A: Add to shell profile (recommended) +For bash: ```shell -echo 'export PATH="$HOME/.opentdf/bin:$PATH"' >> ~/.bashrc -source ~/.bashrc +echo 'export PATH="$HOME/.opentdf/bin:$PATH"' >> ~/.bashrc && source ~/.bashrc ``` -Or for zsh: +For zsh: ```shell -echo 'export PATH="$HOME/.opentdf/bin:$PATH"' >> ~/.zshrc -source ~/.zshrc +echo 'export PATH="$HOME/.opentdf/bin:$PATH"' >> ~/.zshrc && source ~/.zshrc ``` -Option B: Create system-wide symlink +Or create a symlink: ```shell sudo ln -sf ~/.opentdf/bin/otdfctl /usr/local/bin/otdfctl ``` -Option C: Use full path (no setup needed) -```shell -~/.opentdf/bin/otdfctl --version -``` - -Expected output: -> ```console -> otdfctl version 0.x.x (2026-XX-XXT00:00:00Z) -> ``` - -#### Step 4: Verify Installation - +**Verify installation:** ```shell otdfctl --version +curl -k https://platform.opentdf.local:8443/healthz ``` -Expected output: -> ```console -> otdfctl version 0.x.x (2026-XX-XXT00:00:00Z) -> ``` - -You can verify that the platform and keycloak are running at the following locations: -- https://platform.opentdf.local:8443/healthz -- https://keycloak.opentdf.local:9443/ - -When you visit the platform healthz endpoint, you should see: +Expected: version output and `{"status":"SERVING"}` from the healthz check. -Expected output: -```json -{"status":"SERVING"} -``` +#### Step 3: Create Profile & Authenticate -When you visit the Keycloak URL, seeing "Resource not found" is expected and indicates Keycloak is running correctly. +Create a profile to store your connection details, then authenticate: -#### Step 5: Create an otdfctl Profile +```shell +# Create profile (--tls-no-verify skips certificate validation for local development) +otdfctl profile create --tls-no-verify platform-otdf-local https://platform.opentdf.local:8443 -A profile stores your platform connection details and authentication credentials, making commands shorter and easier to use. +# Authenticate with test credentials +otdfctl auth client-credentials opentdf secret +``` -Without a profile, you'd need to type the full platform URL with every command. With a profile, you simply reference it by name using `--profile platform-otdf-local`. This also allows you to manage multiple environments (like local, staging, and production) by switching between different profiles. +:::note Linux Keyring Limitation +On Linux systems, you may see a warning: `Warning: Keyring storage is not available on Linux`. This is a known limitation. As a workaround, you can create the profile with credentials in one command: ```shell -otdfctl profile create platform-otdf-local https://platform.opentdf.local:8443 +otdfctl profile create --tls-no-verify \ + --with-client-creds='{"clientId":"opentdf", "clientSecret":"secret"}' \ + --host=https://platform.opentdf.local:8443 \ + platform-otdf-local ``` +::: Expected output: > ```console > SUCCESS Profile platform-otdf-local created +> Client credentials set for profile [platform-otdf-local] > ``` -**About Default Profiles:** +**What is a profile?** A profile stores your platform URL and credentials so you don't need to type them with every command. When you have only one profile, it's automatically your default. -When you have only one profile (like most users following this guide), it automatically becomes your default profile. This means you can omit the `--profile platform-otdf-local` flag from all commands. +Your credentials are saved in the profile and persist across commands—no need to re-authenticate unless you change profiles. -If you have multiple profiles, you can set which one is the default: +#### Step 4: Trust SSL Certificate (Optional) -```shell -otdfctl profile set-default platform-otdf-local -``` +The `--tls-no-verify` flag skips certificate validation, which is fine for CLI quickstart testing. However, if you're using browsers or SDKs, you may see certificate warnings. -Expected output: -> ```console -> Set profile platform-otdf-local as default -> ``` +**Trust the certificate if you see:** +- Browser "Not Secure" warnings +- curl commands requiring `-k` flag +- SDK certificate validation errors -For the rest of this guide, commands use the default profile. If you have multiple profiles, you can specify which one to use by adding `--profile ` to any command. +**Solution:** See [TLS Certificate Verification](/getting-started/managing-platform#tls-certificate-verification) for instructions on trusting the Caddy root CA certificate on your OS. :::tip Managing the Platform -Need to start, stop, or restart the platform later? See the [Managing the Platform](/getting-started/managing-platform) guide for platform management commands. +Need to start, stop, or restart the platform later? See the [Managing the Platform](/getting-started/managing-platform) guide. ::: -## Authenticate - -Login using the pre-configured test credentials: - -```shell -otdfctl auth client-credentials opentdf secret -``` - -Expected output: -> ```console -> Client credentials set for profile [platform-otdf-local] -> ``` - -Credentials: -- Client ID: opentdf -- Client Secret: secret - -These are test credentials provisioned automatically during installation. - -**Authentication Persistence**: Your credentials are saved in the profile and will be used automatically for all future commands. You won't need to re-authenticate unless you explicitly clear the credentials or change profiles. - ## Encrypt and Decrypt with the CLI Let's start with a simple encryption example using some plain text: @@ -459,6 +448,76 @@ Expected output (encrypted binary data): As you can see, the file contains encrypted binary data and JSON metadata - not readable plain text. This confirms the encryption worked. +#### Understanding TDF File Structure + +TDF files are actually ZIP archives containing the encrypted payload and metadata. You can inspect the structure using standard zip tools: + +```shell +unzip -l example.tdf +``` + +Expected output: +> ```console +> Archive: example.tdf +> Length Date Time Name +> --------- ---------- ----- ---- +> 35 01-01-2025 12:00 0.payload +> 1234 01-01-2025 12:00 0.manifest.json +> --------- ------- +> 1269 2 files +> ``` + +To view the manifest (which contains policy and encryption metadata): + +```shell +unzip -p example.tdf 0.manifest.json | jq +``` + +Expected output: +> ```json +> { +> "encryptionInformation": { +> "type": "split", +> "policy": "eyJ1dWlkIjoiZjQyMzI4Y...", +> "method": { +> "algorithm": "AES-256-GCM", +> "isStreamable": true, +> "iv": "base64encodediv==" +> }, +> "integrityInformation": { +> "rootSignature": { +> "alg": "HS256", +> "sig": "base64encodedsignature==" +> }, +> "segmentSizeDefault": 1048576, +> "encryptedSegmentSizeDefault": 1048604 +> }, +> "keyAccess": [ +> { +> "type": "wrapped", +> "url": "https://platform.opentdf.local:8443/kas", +> "protocol": "kas", +> "wrappedKey": "base64encodedkey==", +> "policyBinding": "base64encodedbinding==", +> "encryptedMetadata": "base64encodedmetadata==" +> } +> ] +> }, +> "payload": { +> "type": "reference", +> "url": "0.payload", +> "protocol": "zip", +> "isEncrypted": true +> } +> } +> ``` + +The manifest contains: +- **Encryption details**: Algorithm (AES-256-GCM), initialization vector, integrity signatures +- **Key Access**: How to retrieve the decryption key from the Key Access Server (KAS) +- **Policy binding**: The attributes and access policies protecting this data +- **Payload reference**: Location of the encrypted content within the zip file + ### Decrypt the File Now decrypt it to retrieve the original content: @@ -556,11 +615,11 @@ SUCCESS Found namespaces: 4
-These are example namespaces that come pre-configured with the platform. We'll create our own namespace for this tutorial. +These are example namespaces that come pre-configured with the platform. You'll create your own namespace for this tutorial. ### Create a new namespace -We'll create a namespace representing our company domain: +Create a namespace representing our company domain: ```shell otdfctl policy attributes namespaces create --name opentdf.io @@ -663,9 +722,65 @@ Each attribute has: You can create values when creating the definition: `--value finance --value engineering` ::: +### How ABAC Components Work Together + +Before creating attributes, let's visualize how all the pieces of attribute-based access control fit together: + +```mermaid +graph TD + NS[Namespace: opentdf.io
Your organization's domain] + + NS -->|contains| A1[Attribute: department
Rule: ANY_OF] + NS -->|contains| A2[Attribute: clearance
Rule: HIERARCHY] + + A1 -->|has values| V1[Value: finance] + A1 -->|has values| V2[Value: engineering] + A2 -->|has values| V3[Value: executive] + A2 -->|has values| V4[Value: standard] + + SCS[Subject Condition Set
WHO can access?
e.g., .clientId = 'opentdf'] + + SCS -->|connects to| SM1[Subject Mapping
Grants: finance
Action: read] + SCS -->|connects to| SM2[Subject Mapping
Grants: engineering
Action: read] + + SM1 -.->|grants access to| V1 + SM2 -.->|grants access to| V2 + + TDF[Encrypted TDF
Requires: finance] + + V1 -.->|protects| TDF + + style NS fill:#e1f5ff + style A1 fill:#fff4e6 + style A2 fill:#fff4e6 + style V1 fill:#f0f9ff + style V2 fill:#f0f9ff + style V3 fill:#f0f9ff + style V4 fill:#f0f9ff + style SCS fill:#f0fff4 + style SM1 fill:#fffbeb + style SM2 fill:#fffbeb + style TDF fill:#fef2f2 +``` + +**Key Relationships:** + +1. **Namespace** - Your organization's domain (e.g., `opentdf.io`) +2. **Attributes** - Categories for classifying data (e.g., `department`, `clearance`) +3. **Values** - Specific options within each attribute (e.g., `finance`, `engineering`) +4. **Subject Condition Set** - Defines WHO can access by matching JWT token claims (e.g., `.clientId = "opentdf"`) +5. **Subject Mapping** - Grants entitlements by connecting WHO (subject condition set) to WHAT (attribute values) with ACTIONS (e.g., `read`) +6. **TDF** - Encrypted data that requires specific attribute values to decrypt + +When a user tries to decrypt a TDF: +- Their JWT token is evaluated against **Subject Condition Sets** +- Matching sets grant them **entitlements** via **Subject Mappings** +- The platform checks if their entitlements satisfy the TDF's required **attribute values** +- If authorized, they can decrypt the data + ### Create Department Attribute -First, let's create an attribute for departments. We'll use `ANY_OF` since a person typically belongs to one department: +First, create an attribute for departments. We use `ANY_OF` since a person typically belongs to one department: ```shell otdfctl policy attributes create --name department -s $NAMESPACE_ID -r ANY_OF @@ -694,186 +809,60 @@ export DEPT_ATTRIBUTE_ID= ### Add Department Values -Now add the specific departments. Preston works in finance, and Jack works in engineering: +Add values for finance and engineering: ```shell +# Add finance value otdfctl policy attributes values create -a $DEPT_ATTRIBUTE_ID --value finance -``` +export FINANCE_VALUE_ID= -> ```console -> SUCCESS Created values: 0fe7e8d0-a3ff-485f-ac24-a54d85904712 -> ┌────────────┬──────────────────────────────────────────────────────┐ -> │Property │Value │ -> ├────────────┼──────────────────────────────────────────────────────┤ -> │Id │0fe7e8d0-a3ff-485f-ac24-a54d85904712 │ -> │FQN │https://opentdf.io/attr/department/value/finance │ -> │Value │finance │ -> │Created At │Mon Jun 24 11:11:15 UTC 2024 │ -> │Updated At │Mon Jun 24 11:11:15 UTC 2024 │ -> └────────────┴──────────────────────────────────────────────────────┘ -> NOTE Use 'values get --id=0fe7e8d0-a3ff-485f-ac24-a54d85904712 --json' to see all properties -> ``` - -Save the finance value ID: - -```shell -export FINANCE_VALUE_ID= -``` - -```shell +# Add engineering value otdfctl policy attributes values create -a $DEPT_ATTRIBUTE_ID --value engineering +export ENGINEERING_VALUE_ID= ``` -> ```console -> SUCCESS Created values: dbfcbe15-7392-4e35-9e1d-3d06918472be -> ┌────────────┬─────────────────────────────────────────────────────────┐ -> │Property │Value │ -> ├────────────┼─────────────────────────────────────────────────────────┤ -> │Id │dbfcbe15-7392-4e35-9e1d-3d06918472be │ -> │FQN │https://opentdf.io/attr/department/value/engineering │ -> │Value │engineering │ -> │Created At │Mon Jun 24 11:12:45 UTC 2024 │ -> │Updated At │Mon Jun 24 11:12:45 UTC 2024 │ -> └────────────┴─────────────────────────────────────────────────────────┘ -> NOTE Use 'values get --id=dbfcbe15-7392-4e35-9e1d-3d06918472be --json' to see all properties -> ``` - -Save the engineering value ID: +:::tip Pro Tip +You can create attributes with values in one command: `otdfctl policy attributes create --name department -s $NAMESPACE_ID -r ANY_OF --value finance --value engineering` +::: -```shell -export ENGINEERING_VALUE_ID= -``` +### Create Clearance Attribute -### Review Your Department Attributes +Now create a clearance attribute using `HIERARCHY` rule. -Now that you've created the department attribute with its values, let's verify it: +:::tip Understanding HIERARCHY Rule +With HIERARCHY, the **order you create values** determines their privilege level: +- **First value created = highest privilege** (can access all lower levels) +- **Later values = lower privileges** (cannot access higher levels) -```shell -otdfctl policy attributes get --id=$DEPT_ATTRIBUTE_ID -``` +For example, if you create "executive" first and "standard" second: +- Users with "executive" clearance can access both "executive" and "standard" content +- Users with "standard" clearance can only access "standard" content -> ```console -> SUCCESS Found attributes: bd02d7ab-564d-4b6c-95c4-3d4a8a259000 -> ┌────────────┬──────────────────────────────────────┐ -> │Property │Value │ -> ├────────────┼──────────────────────────────────────┤ -> │Id │bd02d7ab-564d-4b6c-95c4-3d4a8a259000 │ -> │Name │department │ -> │Rule │ANY_OF │ -> │Values │[finance, engineering] │ -> │Namespace │opentdf.io │ -> │Created At │Mon Jun 24 11:09:39 UTC 2024 │ -> │Updated At │Mon Jun 24 11:14:22 UTC 2024 │ -> └────────────┴──────────────────────────────────────┘ -> NOTE Use 'attributes get --id=bd02d7ab-564d-4b6c-95c4-3d4a8a259000 --json' to see all properties -> ``` - -### Create Clearance Attribute +The order matters! Always create values from highest to lowest privilege. +::: -Now create a clearance attribute using `HIERARCHY` rule. In the next few steps you will add an executive clearance value and a standard clearance value to the hierarchy. The order that you add the values to the hierarchy matters - the order defines the hierarchy. +In the next few steps you will add an executive clearance value and a standard clearance value to the hierarchy. ```shell otdfctl policy attributes create --name clearance -s $NAMESPACE_ID -r HIERARCHY -``` - -> ```console -> SUCCESS Created attributes: af23c7bd-987d-4c6b-85f4-2a8d4a127abc -> ┌────────────┬──────────────────────────────┐ -> │Property │Value │ -> ├────────────┼──────────────────────────────┤ -> │Name │clearance │ -> │Rule │HIERARCHY │ -> │Values │[] │ -> │Namespace │opentdf.io │ -> │Created At │Mon Jun 24 11:09:39 UTC 2024 │ -> │Updated At │Mon Jun 24 11:09:39 UTC 2024 │ -> └────────────┴──────────────────────────────┘ -> NOTE Use 'attributes get --id=af23c7bd-987d-4c6b-85f4-2a8d4a127abc --json' to see all properties -> ``` - -Save the clearance attribute ID: - -```shell -export CLEARANCE_ATTRIBUTE_ID= +export CLEARANCE_ATTRIBUTE_ID= ``` ### Add Clearance Values -Add clearance levels. Order matters for HIERARCHY - list them from highest to lowest: +Add clearance levels in order from highest to lowest privilege. Create "executive" first (highest clearance), then "standard" (lower clearance): ```shell +# Add executive value (highest privilege) otdfctl policy attributes values create -a $CLEARANCE_ATTRIBUTE_ID --value executive -``` - -> ```console -> SUCCESS Created values: 654f0877-2c0b-4a62-a9c3-87ed42bf77ac -> ┌────────────┬──────────────────────────────────────────────────────┐ -> │Property │Value │ -> ├────────────┼──────────────────────────────────────────────────────┤ -> │Id │654f0877-2c0b-4a62-a9c3-87ed42bf77ac │ -> │FQN │https://opentdf.io/attr/clearance/value/executive │ -> │Value │executive │ -> │Created At │Mon Jun 24 11:14:22 UTC 2024 │ -> │Updated At │Mon Jun 24 11:14:22 UTC 2024 │ -> └────────────┴──────────────────────────────────────────────────────┘ -> NOTE Use 'values get --id=654f0877-2c0b-4a62-a9c3-87ed42bf77ac --json' to see all properties -> ``` - -Save the executive clearance value ID: +export EXECUTIVE_VALUE_ID= -```shell -export EXECUTIVE_VALUE_ID= -``` - -```shell +# Add standard value (lower privilege) otdfctl policy attributes values create -a $CLEARANCE_ATTRIBUTE_ID --value standard +export STANDARD_VALUE_ID= ``` -> ```console -> SUCCESS Created values: 789a1234-3c0b-4a62-b8d4-98fe53cf88bd -> ┌────────────┬─────────────────────────────────────────────────────┐ -> │Property │Value │ -> ├────────────┼─────────────────────────────────────────────────────┤ -> │Id │789a1234-3c0b-4a62-b8d4-98fe53cf88bd │ -> │FQN │https://opentdf.io/attr/clearance/value/standard │ -> │Value │standard │ -> │Created At │Mon Jun 24 11:14:22 UTC 2024 │ -> │Updated At │Mon Jun 24 11:14:22 UTC 2024 │ -> └────────────┴─────────────────────────────────────────────────────┘ -> NOTE Use 'values get --id=789a1234-3c0b-4a62-b8d4-98fe53cf88bd --json' to see all properties -> ``` - -Save the standard clearance value ID: - -```shell -export STANDARD_VALUE_ID= -``` - -### Review Your Clearance Attributes - -Now that you've created the clearance hierarchy attribute with its values, let's verify it: - -```shell -otdfctl policy attributes get --id=$CLEARANCE_ATTRIBUTE_ID -``` - -> ```console -> SUCCESS Found attributes: af23c7bd-987d-4c6b-85f4-2a8d4a127abc -> ┌────────────┬──────────────────────────────────────┐ -> │Property │Value │ -> ├────────────┼──────────────────────────────────────┤ -> │Id │af23c7bd-987d-4c6b-85f4-2a8d4a127abc │ -> │Name │clearance │ -> │Rule │HIERARCHY │ -> │Values │[executive, standard] │ -> │Namespace │opentdf.io │ -> │Created At │Mon Jun 24 11:09:39 UTC 2024 │ -> │Updated At │Mon Jun 24 11:14:22 UTC 2024 │ -> └────────────┴──────────────────────────────────────┘ -> NOTE Use 'attributes get --id=af23c7bd-987d-4c6b-85f4-2a8d4a127abc --json' to see all properties -> ``` - -Notice that executive is listed before standard in the Values array. This order is important - because we're using a HIERARCHY rule, someone with executive clearance automatically has standard clearance too. The hierarchy flows from highest (executive) to lowest (standard). +Because we're using a HIERARCHY rule, someone with executive clearance automatically has standard clearance too. The hierarchy flows from highest (executive) to lowest (standard). ## Create Subject Mappings @@ -883,13 +872,13 @@ A `subject mapping` connects identities to attributes, **granting them entitleme An identity can be a user (human individual), service account, application, device, or any authenticated entity making requests. -For our scenario, we'll grant these entitlements: +For our scenario, you'll grant these entitlements: - **Jen** gets `clearance/executive` (which also grants `clearance/standard` due to hierarchy) - **Preston** gets `department/finance` and `clearance/standard` - **Jack** gets `department/engineering` and `clearance/standard` :::note Production Usage -In this tutorial, Jen, Preston, and Jack are personas we're using to learn how entitlements work. We're simulating all three by using the same `opentdf` client credentials. +In this tutorial, Jen, Preston, and Jack are personas you'll use to learn how entitlements work. You're simulating all three by using the same `opentdf` client credentials. In production, each identity would have: - **Distinct credentials** - Separate user accounts, service accounts, or application identities @@ -903,7 +892,7 @@ First, we need a `subject condition set` - this defines WHO qualifies for the en ### Create Subject Condition Sets -For this tutorial, we'll simulate our three personas by creating condition sets that match the default client credentials. Here's what a subject condition set looks like: +For this tutorial, you'll simulate the three personas by creating condition sets that match the default client credentials. Here's what a subject condition set looks like: ```json title="subject_condition_set.json" [ @@ -953,7 +942,7 @@ otdfctl policy subject-condition-sets create -j /subject_condition > NOTE Use 'subject-condition-sets get --id=74bf521f-5a79-48fe-acb8-b4b63ee7950b --json' to see all properties > ``` -Save the condition set ID - we'll use this to represent all three users in our demo: +Save the condition set ID - you'll use this to represent all three users in the demo: ```shell export SUBJECT_CONDITION_SET_ID= @@ -975,7 +964,7 @@ This creates an encrypted file called `executive-strategy.tdf`. ### Try to Decrypt Without Permission (Will Fail) -Right now, we haven't granted ourselves any entitlements. Let's try to decrypt this executive document and see the authorization **decision**: +Right now, you haven't granted any entitlements. Try to decrypt this executive document and see the authorization **decision**: ```shell otdfctl decrypt executive-strategy.tdf @@ -995,7 +984,13 @@ This is an authorization **decision** in action! Here's what happened: ### Grant Jen Executive Clearance -Now let's grant an **entitlement** to represent Jen's access level. This changes what attribute values we're entitled to: +Now grant an **entitlement** to represent Jen's access level. This changes what attribute values you're entitled to: + +:::tip About the `read` Action +The `--action read` flag specifies what the entity can _do_ with this attribute value. The `read` action is one of the standard actions used for TDF decryption. Other standard actions include `create`, `update`, and `delete`. You can also define custom actions for your specific use cases. + +Learn more about actions in the [Actions documentation](/components/policy/actions). +::: ```shell otdfctl policy subject-mappings create --action read --attribute-value-id $EXECUTIVE_VALUE_ID --subject-condition-set-id $SUBJECT_CONDITION_SET_ID @@ -1022,7 +1017,7 @@ Because we're using HIERARCHY for clearance, this executive clearance automatica ### Decrypt Now That We Have Permission -Now that we've been granted an **entitlement** for executive clearance, try decrypting the executive document again: +Now that you've been granted an **entitlement** for executive clearance, try decrypting the executive document again: ```shell otdfctl decrypt executive-strategy.tdf @@ -1055,7 +1050,7 @@ Notice this document has TWO attributes - it requires BOTH finance department me ### Grant Finance Department Access -To simulate Preston's access, let's grant ourselves an additional **entitlement** for the finance department: +To simulate Preston's access, grant yourself an additional **entitlement** for the finance department: ```shell otdfctl policy subject-mappings create --action read --attribute-value-id $FINANCE_VALUE_ID --subject-condition-set-id $SUBJECT_CONDITION_SET_ID @@ -1079,7 +1074,7 @@ Expected output: ### Decrypt the Finance Report -Now we can access the derivative document (since we have executive clearance which includes standard, plus we just added finance department): +Now you can access the derivative document (since you have executive clearance which includes standard, plus you just added finance department): ```shell otdfctl decrypt finance-report.tdf @@ -1135,7 +1130,35 @@ In production, you would: ### What's Next? -- Explore [SDK integration](/category/sdk) to encrypt/decrypt in your applications +- Explore [SDK integration](/sdks) to encrypt/decrypt in your applications - Learn about [Key Access Servers (KAS)](/components/key_access) for policy enforcement - Understand [policy management](/components/policy) for access control - Build [attribute hierarchies](/components/authorization) for complex organizational structures + +## Need Help? + +**Having issues?** See the [Managing the Platform](/getting-started/managing-platform) guide for: +- 🔧 Troubleshooting common installation and setup issues +- 📚 Quick reference for platform management commands +- ⚡ Platform management tips and best practices + +For SDK-specific errors when writing code, see [SDK Troubleshooting](/sdks/troubleshooting). + +## Next Steps + +Now that you've completed the quickstart and understand how OpenTDF works with the CLI, you're ready to integrate OpenTDF into your applications using our SDKs. + +**Continue your journey:** + +📚 **[SDK Quickstart Guide](/sdks/quickstart)** - Learn how to encrypt and decrypt data in your Go, Java, or JavaScript applications + +The SDK Quickstart will show you how to: +- Set up the OpenTDF SDK in your preferred language +- Encrypt data with attribute-based access control +- Create and manage access policies programmatically +- Decrypt TDFs in your applications + +**Other helpful resources:** +- [SDK Troubleshooting](/sdks/troubleshooting) - Solutions to common SDK issues +- [Architecture Overview](/architecture) - Deep dive into how OpenTDF works +- [Managing the Platform](/getting-started/managing-platform) - Platform management commands diff --git a/docs/introduction.mdx b/docs/introduction.mdx index d42adf01..4e1e4fa2 100644 --- a/docs/introduction.mdx +++ b/docs/introduction.mdx @@ -1,5 +1,6 @@ --- sidebar_position: 1 +pagination_next: getting-started/quickstart --- import React from "react"; @@ -14,7 +15,7 @@ Find all the information you need to get started with OpenTDF. title="Quick Start" callToAction={{ label: "Learn more", - link: "/getting-started", + link: "/quickstart", }} > This guide will walk you through setting up a new OpenTDF platform locally and @@ -61,7 +62,7 @@ Find all the information you need to get started with OpenTDF. name: "SDK", description: "Learn about the Trust Data Format (TDF) and how it enables the cryptographic binding of attribute-based access control (ABAC) policy to a data object.", - url: "/category/sdk", + url: "/sdks", }, { name: "CLI", diff --git a/docs/sdks/_category_.json b/docs/sdks/_category_.json index 8da194e9..b1c39d00 100644 --- a/docs/sdks/_category_.json +++ b/docs/sdks/_category_.json @@ -2,7 +2,7 @@ "label": "SDK", "position": 6, "link": { - "type": "generated-index", - "description": "The OpenTDF platform provides Software Development Kits in the Go, Java, and JavaScript languages. The SDKs include guides and working examples for managing policy, creating Trusted Data Format (TDF) protected objects, and making authorization decisions within an application." + "type": "doc", + "id": "sdks/index" } } \ No newline at end of file diff --git a/docs/sdks/index.mdx b/docs/sdks/index.mdx new file mode 100644 index 00000000..d34d539f --- /dev/null +++ b/docs/sdks/index.mdx @@ -0,0 +1,55 @@ +--- +title: SDK +sidebar_label: SDK +--- + +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import CodeBlock from '@theme/CodeBlock' +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +:::tip Get Started with the SDK Quickstart +Ready to start building? Check out our **[SDK Quickstart Guide](/sdks/quickstart)** for step-by-step examples of encrypting and decrypting data with OpenTDF! +::: + +# SDK + +The OpenTDF platform provides Software Development Kits in the Go, Java, and JavaScript languages. The SDKs include guides and working examples for managing policy, creating Trusted Data Format (TDF) protected objects, and making authorization decisions within an application. + +Please refer to the [SDK Feature Matrix](../appendix/matrix.mdx#sdk) in the Appendix for the supported features in each SDK. + +## Repositories + +- [Go](https://pkg.go.dev/github.com/opentdf/platform/sdk) +- [Java](https://github.com/opentdf/java-sdk) +- [JavaScript](https://github.com/opentdf/client-web) + +## Install + + + + +```bash +go get github.com/opentdf/platform/sdk@latest +``` + + + + + +{` + io.opentdf.platform + sdk-pom + `}{useDocusaurusContext().siteConfig.customFields.javaSdkVersion}{` +`} + + + + + +```bash +npm i @opentdf/client +``` + + + diff --git a/docs/sdks/overview.mdx b/docs/sdks/overview.mdx index ffb88aed..464639f8 100644 --- a/docs/sdks/overview.mdx +++ b/docs/sdks/overview.mdx @@ -1,5 +1,6 @@ --- sidebar_position: 1 +unlisted: true --- import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; diff --git a/docs/sdks/quickstart/_category_.json b/docs/sdks/quickstart/_category_.json new file mode 100644 index 00000000..72964049 --- /dev/null +++ b/docs/sdks/quickstart/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "SDK Quickstart", + "position": 2, + "link": { + "type": "doc", + "id": "sdks/quickstart/index" + }, + "collapsible": true, + "collapsed": false +} diff --git a/docs/sdks/quickstart/go.mdx b/docs/sdks/quickstart/go.mdx new file mode 100644 index 00000000..35860786 --- /dev/null +++ b/docs/sdks/quickstart/go.mdx @@ -0,0 +1,649 @@ +--- +sidebar_position: 1 +title: Go +--- + +# Go SDK Quickstart + +:::tip Back to SDK Quickstart +This guide covers the **Go SDK** implementation. For other languages or general information, see the [SDK Quickstart](/sdks/quickstart) page. +::: + +## Prerequisites + +- Go 1.21 or later +- Your OpenTDF platform running locally (from Getting Started guide) + +:::warning Platform Must Be Running +Before you begin, **make sure your OpenTDF platform is running!** + +Verify it's running: +```bash +curl -k https://platform.opentdf.local:8443/healthz +``` + +Should return: `{"status":"SERVING"}` + +If not running, start it: +```bash +cd ~/.opentdf/platform && docker compose up -d +``` + +See the [Managing the Platform](/getting-started/managing-platform) guide for details. +::: + +## Step 1: Create a New Project {#step-1-go} + +Create a new directory and initialize a Go module: + +```bash +mkdir opentdf-quickstart +cd opentdf-quickstart +go mod init opentdf-quickstart +``` + +## Step 2: Install the SDK {#step-2-go} + +```bash +go get github.com/opentdf/platform/sdk@latest +``` + +Expected output: +> ```console +> go: downloading github.com/opentdf/platform/sdk v0.x.x +> go: added github.com/opentdf/platform/sdk v0.x.x +> ``` + +## Step 3: Create Your Application {#step-3-go} + +### Go Implementation Code + +Create a file named `main.go`: + +```go title="main.go" +package main + +import ( + "bytes" + "log" + "strings" + + "github.com/opentdf/platform/sdk" +) + +func main() { + log.Println("🚀 Starting OpenTDF SDK Quickstart...") + + platformEndpoint := "https://platform.opentdf.local:8443" + log.Printf("📡 Connecting to platform: %s", platformEndpoint) + + // Create a new SDK client with client credentials + log.Println("🔐 Initializing SDK client with client credentials...") + client, err := sdk.New( + platformEndpoint, + sdk.WithClientCredentials("opentdf", "secret", nil), + // WithInsecureSkipVerifyConn() disables TLS certificate verification + // This allows connections to the platform's self-signed certificate + // Only use this for local development - never in production! + sdk.WithInsecureSkipVerifyConn(), + ) + + if err != nil { + log.Fatalf("❌ Client initialization failed: %v", err) + } + + log.Println("✅ SDK client initialized successfully") + + // Encrypt data + log.Println("\n📝 Encrypting sensitive data...") + sensitiveData := "Hello from the OpenTDF Go SDK! This data is encrypted." + dataReader := strings.NewReader(sensitiveData) + encryptedBuffer := &bytes.Buffer{} + + log.Println("🔒 Creating TDF...") + _, err = client.CreateTDF( + encryptedBuffer, + dataReader, + // KASInfo specifies the Key Access Service (KAS) endpoint + // KAS manages encryption keys and enforces access policies + sdk.WithKasInformation( + sdk.KASInfo{ + URL: platformEndpoint, + }, + ), + ) + + if err != nil { + log.Fatalf("❌ Encryption failed: %v", err) + } + + log.Println("✅ Data successfully encrypted") + log.Printf("📊 Encrypted TDF size: %d bytes", encryptedBuffer.Len()) + + // Decrypt data + log.Println("\n🔓 Decrypting TDF...") + tdfReader, err := client.LoadTDF(bytes.NewReader(encryptedBuffer.Bytes())) + if err != nil { + log.Fatalf("❌ Decryption failed: %v", err) + } + + var decryptedBuffer bytes.Buffer + if _, err = tdfReader.WriteTo(&decryptedBuffer); err != nil { + log.Fatalf("❌ Failed to read decrypted data: %v", err) + } + + log.Println("✅ Data successfully decrypted") + log.Printf("📤 Decrypted content:\n\n%s\n", decryptedBuffer.String()) + + log.Println("\n🎉 Quickstart complete!") +} +``` + +## Step 4: Run Your Application {#step-4-go} + +```bash +go run main.go +``` + +
+Expected output + +```console +2026/02/02 14:30:00 🚀 Starting OpenTDF SDK Quickstart... +2026/02/02 14:30:00 📡 Connecting to platform: https://platform.opentdf.local:8443 +2026/02/02 14:30:00 🔐 Initializing SDK client with client credentials... +2026/02/02 14:30:01 ✅ SDK client initialized successfully + +2026/02/02 14:30:01 📝 Encrypting sensitive data... +2026/02/02 14:30:01 🔒 Creating TDF... +2026/02/02 14:30:01 ✅ Data successfully encrypted +2026/02/02 14:30:01 📊 Encrypted TDF size: 1234 bytes + +2026/02/02 14:30:01 🔓 Decrypting TDF... +2026/02/02 14:30:02 ✅ Data successfully decrypted +2026/02/02 14:30:02 📤 Decrypted content: + +Hello from the OpenTDF Go SDK! This data is encrypted. + +2026/02/02 14:30:02 🎉 Quickstart complete! +``` + +
+ +--- + +## Step 5: Add ABAC Features {#step-5-go} + +:::tip Go-Specific Examples +The following examples are specific to the **Go SDK**. +::: + +Now that you have basic encryption working, you can add attribute-based access control to your application. + +If you want to follow along, you can add the code to your quickstart file as we go. Otherwise, a [complete version is available below](#complete-example-create-attribute-encrypt-grant-access-and-decrypt). + +:::info More ABAC Examples +For additional policy management examples including managing attributes, namespaces, subject mappings, and more, see the [Policy SDK Guide](/sdks/policy). +::: + +### Create a New Attribute Value + +Let's work with the "department" attribute and add a "marketing" value. If you completed the [Quickstart setup guide](/quickstart), the attribute may already exist with finance and engineering values. If not, we'll create it: + +```go +import ( + "context" + "strings" + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/attributes" + "github.com/opentdf/platform/protocol/go/policy/namespaces" +) + +// First, ensure the namespace exists +nsResp, err := client.Namespaces.CreateNamespace(context.Background(), + &namespaces.CreateNamespaceRequest{ + Name: "opentdf.io", + }) +if err != nil { + if strings.Contains(err.Error(), "already_exists") { + // Namespace exists, fetch it + listNsResp, listErr := client.Namespaces.ListNamespaces(context.Background(), + &namespaces.ListNamespacesRequest{}) + if listErr != nil { + log.Fatalf("Failed to list namespaces: %v", listErr) + } + for _, ns := range listNsResp.GetNamespaces() { + if ns.GetName() == "opentdf.io" { + nsResp = &namespaces.CreateNamespaceResponse{Namespace: ns} + log.Printf("✅ Using existing namespace: %s", ns.GetId()) + break + } + } + } else { + log.Fatalf("Failed to create namespace: %v", err) + } +} else { + log.Printf("✅ Created namespace: %s", nsResp.GetNamespace().GetId()) +} + +// Get or create the department attribute +listResp, err := client.Attributes.ListAttributes(context.Background(), + &attributes.ListAttributesRequest{}) +if err != nil { + log.Fatalf("Failed to list attributes: %v", err) +} + +var getAttrResp *attributes.GetAttributeResponse +for _, attr := range listResp.GetAttributes() { + if attr.GetName() == "department" && + attr.GetNamespace().GetId() == nsResp.GetNamespace().GetId() { + getAttrResp = &attributes.GetAttributeResponse{Attribute: attr} + log.Printf("✅ Found existing attribute: %s", attr.GetName()) + break + } +} + +// Create the attribute if it doesn't exist +if getAttrResp == nil { + attrResp, err := client.Attributes.CreateAttribute(context.Background(), + &attributes.CreateAttributeRequest{ + NamespaceId: nsResp.GetNamespace().GetId(), + Name: "department", + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, + }) + if err != nil { + log.Fatalf("Failed to create attribute: %v", err) + } + getAttrResp = &attributes.GetAttributeResponse{Attribute: attrResp.GetAttribute()} + log.Printf("✅ Created attribute: %s", attrResp.GetAttribute().GetName()) +} + +// Check if "marketing" value already exists +targetValue := "marketing" +valueExists := false +for _, value := range getAttrResp.GetAttribute().GetValues() { + if value.GetValue() == targetValue { + valueExists = true + break + } +} + +if !valueExists { + // Add the "marketing" value to the existing attribute + _, err = client.Attributes.CreateAttributeValue(context.Background(), + &attributes.CreateAttributeValueRequest{ + AttributeId: getAttrResp.GetAttribute().GetId(), + Value: targetValue, + }) + if err != nil { + log.Fatalf("Failed to create attribute value: %v", err) + } + log.Printf("✅ Added '%s' value to department attribute", targetValue) +} else { + log.Printf("✅ Attribute 'department' already has '%s' value", targetValue) +} + +log.Printf("Full attribute FQN: https://opentdf.io/attr/department/value/%s", targetValue) + +// Re-fetch the attribute to get the updated values list with IDs +listResp, err = client.Attributes.ListAttributes(context.Background(), + &attributes.ListAttributesRequest{}) +if err != nil { + log.Fatalf("Failed to re-fetch attributes: %v", err) +} +for _, attr := range listResp.GetAttributes() { + if attr.GetName() == "department" && + attr.GetNamespace().GetId() == nsResp.GetNamespace().GetId() { + getAttrResp = &attributes.GetAttributeResponse{Attribute: attr} + break + } +} +``` + +:::warning +If you get a [resource not found error](/sdks/troubleshooting#resource-not-found), you may need to create the "department" attribute, along with the namespace. +::: + +### Add Attributes for Access Control + +Now that you've created the attribute, update your `CreateTDF` call to include the attribute for access control: + +```go +_, err = client.CreateTDF( + encryptedBuffer, + dataReader, + sdk.WithDataAttributes("https://opentdf.io/attr/department/value/marketing"), + // KASInfo specifies the Key Access Service (KAS) endpoint + // KAS manages encryption keys and enforces access policies + sdk.WithKasInformation( + sdk.KASInfo{ + URL: platformEndpoint, + }, + ), +) +``` + +:::tip +Only users with the `department/marketing` entitlement will be able to decrypt this TDF. If you try to decrypt before granting access to the attribute, you will see a [permission denied error](/sdks/troubleshooting#permission-denied--insufficient-entitlements). Try it now! +::: + +### Grant Yourself Access to the Attribute + +To decrypt the TDF you just created, you need to grant yourself the `marketing` entitlement by creating a subject mapping. This connects your identity to the attribute value, giving you permission to access data encrypted with it. + +:::tip About the `read` Action +The subject mapping below uses the `read` action, which specifies what you can _do_ with this attribute value. The `read` action is one of the standard actions used for TDF decryption. Other standard actions include `create`, `update`, and `delete`, and you can define custom actions for your specific use cases. + +Learn more about actions in the [Actions documentation](/components/policy/actions). +::: + +```go +import ( + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/subjectmapping" +) + +// Get the attribute value ID for "marketing" +var attributeValueId string +for _, value := range getAttrResp.GetAttribute().GetValues() { + if value.GetValue() == targetValue { + attributeValueId = value.GetId() + break + } +} + +// Create a subject condition set that matches your identity +scsResp, err := client.SubjectMapping.CreateSubjectConditionSet(context.Background(), + &subjectmapping.CreateSubjectConditionSetRequest{ + SubjectConditionSet: &subjectmapping.SubjectConditionSetCreate{ + SubjectSets: []*policy.SubjectSet{ + { + ConditionGroups: []*policy.ConditionGroup{ + { + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND, + Conditions: []*policy.Condition{ + { + SubjectExternalSelectorValue: ".clientId", + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalValues: []string{"opentdf"}, + }, + }, + }, + }, + }, + }, + }, + }) +if err != nil { + log.Fatalf("Failed to create subject condition set: %v", err) +} + +// Create the subject mapping to grant yourself the entitlement +_, err = client.SubjectMapping.CreateSubjectMapping(context.Background(), + &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: attributeValueId, + Actions: []*policy.Action{{Name: "read"}}, + ExistingSubjectConditionSetId: scsResp.GetSubjectConditionSet().GetId(), + }) +if err != nil { + log.Fatalf("Failed to create subject mapping: %v", err) +} + +log.Printf("✅ Granted yourself access to department/marketing") +``` + +Now you can decrypt the TDF you encrypted with the `marketing` attribute. 🎉 + + + +### Save TDF to a File + +In production applications, you'll often need to persist encrypted TDFs to disk for storage, transmission, or archival. This allows you to: + +- **Separate encryption from distribution**: Encrypt data once, then share the TDF file through your preferred channels (email, S3, SFTP, etc.) +- **Enable offline access**: Recipients can decrypt TDFs without needing to re-fetch data from your application +- **Archive encrypted data**: Store TDFs in backup systems or long-term storage with their access policies intact + +```go +import "os" + +// After encryption +err = os.WriteFile("encrypted.tdf", encryptedBuffer.Bytes(), 0644) +if err != nil { + log.Fatalf("Failed to save TDF: %v", err) +} + +// Later, load from file +tdfData, err := os.ReadFile("encrypted.tdf") +if err != nil { + log.Fatalf("Failed to read TDF: %v", err) +} + +tdfReader, err := client.LoadTDF(bytes.NewReader(tdfData)) +``` + +### Handle Large Files with Streaming + +For large files, use file I/O instead of in-memory buffers: + +```go +import "os" + +// Encrypt a large file +inputFile, _ := os.Open("large-file.pdf") +defer inputFile.Close() + +outputFile, _ := os.Create("large-file.pdf.tdf") +defer outputFile.Close() + +// KASInfo specifies the Key Access Service (KAS) endpoint +// KAS manages encryption keys and enforces access policies +_, err = client.CreateTDF(outputFile, inputFile, + sdk.WithKasInformation(sdk.KASInfo{URL: platformEndpoint}), +) +``` + +## Step 6: Complete Reference Implementation {#step-6-go} + +For reference, here's a complete example showing all the pieces together: + +
+Create Attribute, Encrypt, Grant Access, Decrypt, Save File + +```go title="main.go" +package main + +import ( + "bytes" + "context" + "log" + "os" + "strings" + + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/attributes" + "github.com/opentdf/platform/protocol/go/policy/namespaces" + "github.com/opentdf/platform/protocol/go/policy/subjectmapping" + "github.com/opentdf/platform/sdk" +) + +func main() { + platformEndpoint := "https://platform.opentdf.local:8443" + + // Create client + client, err := sdk.New( + platformEndpoint, + sdk.WithClientCredentials("opentdf", "secret", nil), + // WithInsecureSkipVerifyConn() disables TLS certificate verification + // This allows connections to the platform's self-signed certificate + // Only use this for local development - never in production! + sdk.WithInsecureSkipVerifyConn(), + ) + if err != nil { + log.Fatalf("Failed to create client: %v", err) + } + + // 1. Create namespace (or use existing) + nsResp, err := client.Namespaces.CreateNamespace(context.Background(), + &namespaces.CreateNamespaceRequest{ + Name: "opentdf.io", + }) + if err != nil { + if strings.Contains(err.Error(), "already_exists") { + // Namespace already exists, fetch it + listResp, listErr := client.Namespaces.ListNamespaces(context.Background(), &namespaces.ListNamespacesRequest{}) + if listErr != nil { + log.Fatalf("Failed to list namespaces: %v", listErr) + } + for _, ns := range listResp.GetNamespaces() { + if ns.GetName() == "opentdf.io" { + nsResp = &namespaces.CreateNamespaceResponse{Namespace: ns} + log.Printf("✅ Using existing namespace: %s", ns.GetId()) + break + } + } + } else { + log.Fatalf("Failed to create namespace: %v", err) + } + } else { + log.Printf("✅ Created namespace: %s", nsResp.GetNamespace().GetId()) + } + + // 2. Create attribute with marketing value (or use existing) + attrResp, err := client.Attributes.CreateAttribute(context.Background(), + &attributes.CreateAttributeRequest{ + NamespaceId: nsResp.GetNamespace().GetId(), + Name: "department", + Rule: policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF, + Values: []string{"marketing"}, + }) + if err != nil { + if strings.Contains(err.Error(), "already_exists") { + // Attribute already exists, fetch it + listResp, listErr := client.Attributes.ListAttributes(context.Background(), &attributes.ListAttributesRequest{}) + if listErr != nil { + log.Fatalf("Failed to list attributes: %v", listErr) + } + for _, attr := range listResp.GetAttributes() { + if attr.GetName() == "department" && attr.GetNamespace().GetId() == nsResp.GetNamespace().GetId() { + attrResp = &attributes.CreateAttributeResponse{Attribute: attr} + log.Printf("✅ Using existing attribute: %s", attr.GetName()) + break + } + } + } else { + log.Fatalf("Failed to create attribute: %v", err) + } + } else { + log.Printf("✅ Created attribute: %s", attrResp.GetAttribute().GetName()) + } + + // 3. Encrypt data with the marketing attribute + plaintext := []byte("Sensitive marketing campaign data") + dataReader := bytes.NewReader(plaintext) + var encryptedBuffer bytes.Buffer + + // KASInfo specifies the Key Access Service (KAS) endpoint + // KAS manages encryption keys and enforces access policies + _, err = client.CreateTDF( + &encryptedBuffer, + dataReader, + sdk.WithDataAttributes("https://opentdf.io/attr/department/value/marketing"), + sdk.WithKasInformation(sdk.KASInfo{URL: platformEndpoint}), + ) + if err != nil { + log.Fatalf("Failed to encrypt: %v", err) + } + log.Printf("✅ Data encrypted with marketing attribute") + + // 4. Save TDF to file + err = os.WriteFile("encrypted.tdf", encryptedBuffer.Bytes(), 0644) + if err != nil { + log.Fatalf("Failed to save TDF: %v", err) + } + log.Printf("✅ TDF saved to encrypted.tdf") + + // 5. Grant yourself access to the marketing attribute + // Find the marketing value by name + var attributeValueId string + for _, value := range attrResp.GetAttribute().GetValues() { + if value.GetValue() == "marketing" { + attributeValueId = value.GetId() + break + } + } + if attributeValueId == "" { + log.Fatalf("Marketing value not found in department attribute") + } + + scsResp, err := client.SubjectMapping.CreateSubjectConditionSet(context.Background(), + &subjectmapping.CreateSubjectConditionSetRequest{ + SubjectConditionSet: &subjectmapping.SubjectConditionSetCreate{ + SubjectSets: []*policy.SubjectSet{ + { + ConditionGroups: []*policy.ConditionGroup{ + { + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND, + Conditions: []*policy.Condition{ + { + SubjectExternalSelectorValue: ".clientId", + Operator: policy.SubjectMappingOperatorEnum_SUBJECT_MAPPING_OPERATOR_ENUM_IN, + SubjectExternalValues: []string{"opentdf"}, + }, + }, + }, + }, + }, + }, + }, + }) + if err != nil { + log.Fatalf("Failed to create subject condition set: %v", err) + } + + _, err = client.SubjectMapping.CreateSubjectMapping(context.Background(), + &subjectmapping.CreateSubjectMappingRequest{ + AttributeValueId: attributeValueId, + Actions: []*policy.Action{{Name: "read"}}, + ExistingSubjectConditionSetId: scsResp.GetSubjectConditionSet().GetId(), + }) + if err != nil { + if strings.Contains(err.Error(), "already_exists") { + // Subject mapping already exists + log.Printf("✅ Subject mapping already exists for department/marketing") + } else { + log.Fatalf("Failed to create subject mapping: %v", err) + } + } else { + log.Printf("✅ Granted yourself access to department/marketing") + } + + // 6. Load TDF from file + tdfData, err := os.ReadFile("encrypted.tdf") + if err != nil { + log.Fatalf("Failed to read TDF: %v", err) + } + log.Printf("✅ TDF loaded from encrypted.tdf") + + // 7. Decrypt the data + tdfReader, err := client.LoadTDF(bytes.NewReader(tdfData)) + if err != nil { + log.Fatalf("Failed to load TDF: %v", err) + } + + var decryptedBuffer bytes.Buffer + _, err = tdfReader.WriteTo(&decryptedBuffer) + if err != nil { + log.Fatalf("Failed to decrypt: %v", err) + } + + log.Printf("✅ Data successfully decrypted") + log.Printf("📤 Decrypted content: %s", decryptedBuffer.String()) +} +``` + +
+ +## Troubleshooting + +Having issues? See the **[SDK Troubleshooting](/sdks/troubleshooting)** guide for solutions to common problems. diff --git a/docs/sdks/quickstart/index.mdx b/docs/sdks/quickstart/index.mdx new file mode 100644 index 00000000..d44add9b --- /dev/null +++ b/docs/sdks/quickstart/index.mdx @@ -0,0 +1,145 @@ +--- +sidebar_position: 2 +title: SDK Quickstart +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# SDK Quickstart + +This guide shows you how to build a simple application using the OpenTDF SDKs to encrypt and decrypt data. You'll create a lightweight client that connects to your local OpenTDF platform and demonstrates the core SDK functionality. + +:::tip Prerequisites +Complete the [Getting Started](/quickstart) guide first to set up your local OpenTDF platform. This quickstart assumes: +- Platform running at `https://platform.opentdf.local:8443` +- Keycloak running at `https://keycloak.opentdf.local:9443` +- Client credentials: `opentdf` / `secret` +::: + +:::note Time Commitment +This quickstart takes **15-20 minutes** to complete. +::: + +## What You'll Build {#what-youll-build} + +A simple application that: +1. Connects to your local OpenTDF platform +2. Authenticates using client credentials +3. Encrypts sensitive data into a TDF file +4. Decrypts the TDF file back to plaintext + +## Choose Your SDK {#choose-your-sdk} + +OpenTDF provides native SDKs in three languages. Choose your language to get started with complete, working examples: + +
+
+
+
+

Go SDK

+
+
+

Native Go library with excellent performance for backend services and CLI tools.

+
+ +
+
+ +
+
+
+

Java SDK

+
+
+

Full-featured Java library for enterprise applications and Android development.

+
+ +
+
+ +
+
+
+

JavaScript/TypeScript SDK

+
+
+

Modern SDK for Node.js and browser applications with TypeScript support.

+
+ +
+
+
+ +## Key Concepts {#key-concepts} + +### Client Credentials Authentication + +All three SDKs use **client credentials** to authenticate with the platform. This is a machine-to-machine authentication flow suitable for: +- Backend services +- CLI tools +- Server-side applications +- Batch processing jobs + +In production, you would create separate client IDs for each service with appropriate permissions. + +### TDF Format + +The SDKs create **TDF (Trusted Data Format)** files, which contain: +- **Encrypted payload**: Your data, encrypted with a symmetric key +- **Manifest**: Metadata including policy attributes and Key Access Server information +- **Policy**: Access control rules (which attributes are required to decrypt) + +### Connection Security + +:::warning Development Only - Language-Specific Settings +The SDK examples use different settings for local development with self-signed certificates: + +**Go SDK**: `sdk.WithInsecureSkipVerifyConn()` - Disables TLS certificate verification +**Java SDK**: `SSLFactory.builder().withTrustingAllCertificatesWithoutValidation()` - Trusts all certificates +**JavaScript SDK**: May need `NODE_TLS_REJECT_UNAUTHORIZED=0` + +**Never use these settings in production.** + +In production: +- Use HTTPS connections with valid TLS certificates +- Remove all insecure connection flags +- Configure proper certificate validation +::: + +## What's Next? {#whats-next} + +Having issues? See the **[SDK Troubleshooting](/sdks/troubleshooting)** guide for solutions to common problems. + + + +Now that you have basic SDK integration working, explore: + +- **[Creating TDFs](/sdks/tdf)**: Learn about TDF options, attributes, and policies +- **[Managing Policy](/sdks/policy)**: Create and manage attributes programmatically +- **[Authorization](/sdks/authorization)**: Check entitlements and authorization decisions +- **[SDK Feature Matrix](/appendix/matrix#sdk)**: Compare features across SDKs +- **[Best Practices](/sdks/authorization#best-practices)**: Optimize for large-scale operations + +### Real-World Integration + +To integrate OpenTDF into your application: + +1. **Choose Authentication Method**: Client credentials, bearer token, or custom auth +2. **Define Your Attributes**: Map your organizational roles and data classifications to attributes +3. **Create Subject Mappings**: Connect users/services to their entitlements +4. **Encrypt at Rest**: Protect files, documents, and data stores +5. **Encrypt in Transit**: Protect data shared between services +6. **Audit Access**: Monitor who accessed what data and when diff --git a/docs/sdks/quickstart/java.mdx b/docs/sdks/quickstart/java.mdx new file mode 100644 index 00000000..a1a3413d --- /dev/null +++ b/docs/sdks/quickstart/java.mdx @@ -0,0 +1,851 @@ +--- +sidebar_position: 2 +title: Java +--- + +# Java SDK Quickstart + +:::tip Back to SDK Quickstart +This guide covers the **Java SDK** implementation. For other languages or general information, see the [SDK Quickstart](/sdks/quickstart) page. +::: + +## Prerequisites + +- Java 17 or later +- Maven or Gradle +- Your OpenTDF platform running locally (from Getting Started guide) + +:::warning Platform Must Be Running +Before you begin, **make sure your OpenTDF platform is running!** + +Verify it's running: +```bash +curl -k https://platform.opentdf.local:8443/healthz +``` + +Should return: `{"status":"SERVING"}` + +If not running, start it: +```bash +cd ~/.opentdf/platform && docker compose up -d +``` + +See the [Managing the Platform](/getting-started/managing-platform) guide for details. +::: + +## Step 1: Create a New Maven Project {#step-1-java} + +Create a new directory with a `pom.xml` file: + +```bash +mkdir opentdf-quickstart +cd opentdf-quickstart +``` + +Create `pom.xml`: + +```xml title="pom.xml" + + + 4.0.0 + + io.opentdf + opentdf-quickstart + 1.0-SNAPSHOT + + + 17 + 17 + UTF-8 + 1.68.0 + 4.29.2 + + + + + + io.grpc + grpc-bom + ${grpc.version} + pom + import + + + com.google.protobuf + protobuf-java + ${protobuf.version} + + + + + + + io.opentdf.platform + sdk + 0.12.0 + + + + org.slf4j + slf4j-simple + 2.0.9 + + + + io.github.hakky54 + sslcontext-kickstart + 9.2.1 + + + +``` + +## Step 2: Install Dependencies {#step-2-java} + +```bash +mvn clean install +``` + +This will download the OpenTDF Java SDK and its dependencies (including Bouncy Castle for cryptography). + +## Step 3: Create Your Application {#step-3-java} + +Create the directory structure: + +```bash +mkdir -p src/main/java/io/opentdf/quickstart +``` + +### Java Implementation Code + +Create `src/main/java/io/opentdf/quickstart/QuickstartApp.java`: + +```java title="src/main/java/io/opentdf/quickstart/QuickstartApp.java" +package io.opentdf.quickstart; + +import io.opentdf.platform.sdk.*; +import nl.altindag.ssl.SSLFactory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.nio.channels.FileChannel; + +public class QuickstartApp { + public static void main(String[] args) { + try { + System.out.println("🚀 Starting OpenTDF SDK Quickstart..."); + + String clientId = "opentdf"; + String clientSecret = "secret"; + String platformEndpoint = "https://platform.opentdf.local:8443"; + + System.out.println("📡 Connecting to platform: " + platformEndpoint); + + // Create SSL factory that trusts all certificates + // withTrustingAllCertificatesWithoutValidation() disables certificate verification + // This allows connections to the platform's self-signed certificate + // Only use this for local development - never in production! + System.out.println("🔓 Creating SSL factory for self-signed certificates..."); + SSLFactory sslFactory = SSLFactory.builder() + .withTrustingAllCertificatesWithoutValidation() + .build(); + + // Create SDK client with client credentials (try-with-resources ensures proper cleanup) + System.out.println("🔐 Initializing SDK client with client credentials..."); + SDKBuilder builder = SDKBuilder.newBuilder(); + try (SDK sdk = builder.platformEndpoint(platformEndpoint) + .clientSecret(clientId, clientSecret) + .sslFactory(sslFactory) + .build()) { + + System.out.println("✅ SDK client initialized successfully"); + + // Encrypt data + System.out.println("\n📝 Encrypting sensitive data..."); + String sensitiveData = "Hello from the OpenTDF Java SDK! This data is encrypted."; + + // KASInfo specifies the Key Access Service (KAS) endpoint + // KAS manages encryption keys and enforces access policies + var kasInfo = new Config.KASInfo(); + kasInfo.URL = platformEndpoint + "/kas"; + + var tdfConfig = Config.newTDFConfig(Config.withKasInformation(kasInfo)); + + var inputStream = new ByteArrayInputStream(sensitiveData.getBytes(StandardCharsets.UTF_8)); + var outputStream = new ByteArrayOutputStream(); + + System.out.println("🔒 Creating TDF..."); + sdk.createTDF(inputStream, outputStream, tdfConfig); + + System.out.println("✅ Data successfully encrypted"); + System.out.println("📊 Encrypted TDF size: " + outputStream.size() + " bytes"); + + // Decrypt data + System.out.println("\n🔓 Decrypting TDF..."); + byte[] encryptedData = outputStream.toByteArray(); + + // Save to temporary file for decryption + Path tempFile = Files.createTempFile("opentdf-quickstart", ".tdf"); + try { + Files.write(tempFile, encryptedData); + + try (FileChannel fileChannel = FileChannel.open(tempFile, StandardOpenOption.READ)) { + var readerConfig = Config.newTDFReaderConfig(); + var reader = sdk.loadTDF(fileChannel, readerConfig); + var decryptedOutput = new ByteArrayOutputStream(); + reader.readPayload(decryptedOutput); + + String decryptedContent = new String(decryptedOutput.toByteArray(), StandardCharsets.UTF_8); + System.out.println("✅ Data successfully decrypted"); + System.out.println("📤 Decrypted content:\n\n" + decryptedContent + "\n"); + } + } finally { + // Clean up temporary file + Files.deleteIfExists(tempFile); + } + + System.out.println("🎉 Quickstart complete!"); + } // SDK auto-closes here, shutting down gRPC connections + } catch (Exception e) { + System.err.println("❌ Error occurred: " + e.getMessage()); + e.printStackTrace(); + System.exit(1); + } + + // Force JVM exit to terminate SDK background threads immediately. + // Note: In production applications, you should configure daemon threads properly + // or implement explicit shutdown hooks instead of using System.exit(0). + // This approach is acceptable for quickstart/demo purposes only. + System.exit(0); + } +} +``` + +## Step 4: Run Your Application {#step-4-java} + +```bash +mvn compile exec:java -Dexec.mainClass="io.opentdf.quickstart.QuickstartApp" +``` + +
+Expected output + +```console +🚀 Starting OpenTDF SDK Quickstart... +📡 Connecting to platform: https://platform.opentdf.local:8443 +🔐 Initializing SDK client with client credentials... +✅ SDK client initialized successfully + +📝 Encrypting sensitive data... +🔒 Creating TDF... +✅ Data successfully encrypted +📊 Encrypted TDF size: 1234 bytes + +🔓 Decrypting TDF... +✅ Data successfully decrypted +📤 Decrypted content: + +Hello from the OpenTDF Java SDK! This data is encrypted. + +🎉 Quickstart complete! +``` + +
+ +--- + +## Step 5: Add ABAC Features {#step-5-java} + +:::tip Java-Specific Examples +The following examples are specific to the **Java SDK**. +::: + +Now that you have basic encryption working, you can add attribute-based access control to your application. + +If you want to follow along, you can add the code to your quickstart file as we go. Otherwise, a [complete version is available below](#complete-example-create-attribute-encrypt-grant-access-and-decrypt). + +:::info More ABAC Examples +For additional policy management examples including managing attributes, namespaces, subject mappings, and more, see the [Policy SDK Guide](/sdks/policy). +::: + + +### Create a New Attribute Value + +First, let's create a new attribute value that we can use for access control. If you followed the [Quickstart setup guide](/quickstart), you'll already have an attribute "department" with two values: finance and engineering. Let's add "marketing" to those values: + +```java +import io.opentdf.platform.policy.attributes.GetAttributeRequest; +import io.opentdf.platform.policy.attributes.GetAttributeResponse; +import io.opentdf.platform.policy.attributes.CreateAttributeValueRequest; +import com.connectrpc.ResponseMessageKt; +import java.util.Collections; + +// Get the existing department attribute +String attrFqn = "https://opentdf.io/attr/department"; +GetAttributeResponse getAttrResponse = ResponseMessageKt.getOrThrow( + sdk.getServices() + .attributes() + .getAttributeBlocking( + GetAttributeRequest.newBuilder() + .setFqn(attrFqn) + .build(), + Collections.emptyMap()) + .execute()); + +System.out.println("✅ Found existing attribute: " + getAttrResponse.getAttribute().getName()); + +// Check if "marketing" value already exists +String targetValue = "marketing"; +boolean valueExists = false; +for (var value : getAttrResponse.getAttribute().getValuesList()) { + if (targetValue.equals(value.getValue())) { + valueExists = true; + break; + } +} + +if (!valueExists) { + // Add the "marketing" value to the existing attribute + var createValueRequest = CreateAttributeValueRequest.newBuilder() + .setAttributeId(getAttrResponse.getAttribute().getId()) + .setValue(targetValue) + .build(); + + ResponseMessageKt.getOrThrow( + sdk.getServices() + .attributes() + .createAttributeValueBlocking(createValueRequest, Collections.emptyMap()) + .execute()); + + System.out.println("✅ Added '" + targetValue + "' value to department attribute"); +} else { + System.out.println("✅ Attribute 'department' already has '" + targetValue + "' value"); +} + +System.out.println("Full attribute FQN: https://opentdf.io/attr/department/value/" + targetValue); +``` + +:::warning +If you get a [resource not found error](/sdks/troubleshooting#resource-not-found), you may need to create the "department" attribute, along with the namespace: +::: + +
+Show full code for creating namespace and attribute + +```java +import io.opentdf.platform.policy.AttributeRuleTypeEnum; +import io.opentdf.platform.policy.namespaces.CreateNamespaceRequest; +import io.opentdf.platform.policy.namespaces.CreateNamespaceResponse; +import io.opentdf.platform.policy.namespaces.GetNamespaceRequest; +import io.opentdf.platform.policy.namespaces.GetNamespaceResponse; +import io.opentdf.platform.policy.attributes.CreateAttributeRequest; +import io.opentdf.platform.policy.attributes.CreateAttributeResponse; +import io.opentdf.platform.policy.attributes.GetAttributeRequest; +import io.opentdf.platform.policy.attributes.GetAttributeResponse; +import io.opentdf.platform.policy.attributes.CreateAttributeValueRequest; +import com.connectrpc.ResponseMessageKt; +import java.util.Arrays; +import java.util.Collections; + +// Get or create the namespace +CreateNamespaceResponse nsResponse; +try { + // Try to get existing namespace + GetNamespaceResponse getNamespaceResponse = ResponseMessageKt.getOrThrow( + sdk.getServices() + .namespaces() + .getNamespaceBlocking( + GetNamespaceRequest.newBuilder() + .setFqn("https://opentdf.io") + .build(), + Collections.emptyMap()) + .execute()); + + nsResponse = CreateNamespaceResponse.newBuilder() + .setNamespace(getNamespaceResponse.getNamespace()) + .build(); + System.out.println("✅ Using existing namespace: " + getNamespaceResponse.getNamespace().getId()); + +} catch (Exception e) { + if (e.getMessage() != null && e.getMessage().contains("not_found")) { + // Namespace doesn't exist, create it + nsResponse = ResponseMessageKt.getOrThrow( + sdk.getServices() + .namespaces() + .createNamespaceBlocking( + CreateNamespaceRequest.newBuilder() + .setName("opentdf.io") + .build(), + Collections.emptyMap()) + .execute()); + System.out.println("✅ Created namespace: " + nsResponse.getNamespace().getId()); + } else { + throw e; + } +} + +// Get or create the department attribute, and ensure it has the "marketing" value +String attrFqn = "https://opentdf.io/attr/department"; +try { + // Try to get the existing attribute + GetAttributeResponse getAttrResponse = ResponseMessageKt.getOrThrow( + sdk.getServices() + .attributes() + .getAttributeBlocking( + GetAttributeRequest.newBuilder() + .setFqn(attrFqn) + .build(), + Collections.emptyMap()) + .execute()); + + System.out.println("✅ Found existing attribute: " + getAttrResponse.getAttribute().getName()); + + // Check if "marketing" value already exists + String targetValue = "marketing"; + boolean valueExists = false; + for (var value : getAttrResponse.getAttribute().getValuesList()) { + if (targetValue.equals(value.getValue())) { + valueExists = true; + break; + } + } + + if (!valueExists) { + // Add the value to the existing attribute + var createValueRequest = CreateAttributeValueRequest.newBuilder() + .setAttributeId(getAttrResponse.getAttribute().getId()) + .setValue(targetValue) + .build(); + + ResponseMessageKt.getOrThrow( + sdk.getServices() + .attributes() + .createAttributeValueBlocking(createValueRequest, Collections.emptyMap()) + .execute()); + + System.out.println("✅ Added '" + targetValue + "' value to department attribute"); + } else { + System.out.println("✅ Attribute 'department' already has '" + targetValue + "' value"); + } + +} catch (Exception e) { + if (e.getMessage() != null && e.getMessage().contains("not_found")) { + // Attribute doesn't exist, create it with the value + String targetValue = "marketing"; + var attrRequest = CreateAttributeRequest.newBuilder() + .setNamespaceId(nsResponse.getNamespace().getId()) + .setName("department") + .setRule(AttributeRuleTypeEnum.ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF) + .addAllValues(Arrays.asList(targetValue)) + .build(); + + CreateAttributeResponse createAttrResponse = ResponseMessageKt.getOrThrow( + sdk.getServices() + .attributes() + .createAttributeBlocking(attrRequest, Collections.emptyMap()) + .execute()); + System.out.println("✅ Created attribute: " + createAttrResponse.getAttribute().getName()); + } else { + throw e; + } +} + +String targetValue = "marketing"; +System.out.println("Full attribute FQN: https://opentdf.io/attr/department/value/" + targetValue); +``` + +
+ +### Add Attributes for Access Control + +Now that you've created the attribute, update your TDF config to include the attribute for access control: + +```java +var tdfConfig = Config.newTDFConfig( + Config.withKasInformation(kasInfo), + Config.withDataAttributes("https://opentdf.io/attr/department/value/marketing") +); +``` + +:::tip +Only users with the `department/marketing` entitlement will be able to decrypt this TDF. If you try to decrypt before granting access to the attribute, you will see a [permission denied error](/sdks/troubleshooting#permission-denied--insufficient-entitlements). Try it now! +::: + +### Grant Yourself Access to the Attribute + +To decrypt the TDF you just created, you need to grant yourself the `marketing` entitlement by creating a subject mapping. This connects your identity to the attribute value, giving you permission to access data encrypted with it. + +:::tip About the `read` Action +The subject mapping below uses the `read` action, which specifies what you can _do_ with this attribute value. The `read` action is one of the standard actions used for TDF decryption. Other standard actions include `create`, `update`, and `delete`, and you can define custom actions for your specific use cases. + +Learn more about actions in the [Actions documentation](/components/policy/actions). +::: + +```java +import io.opentdf.platform.policy.Action; +import io.opentdf.platform.policy.Condition; +import io.opentdf.platform.policy.ConditionBooleanTypeEnum; +import io.opentdf.platform.policy.ConditionGroup; +import io.opentdf.platform.policy.SubjectMappingOperatorEnum; +import io.opentdf.platform.policy.SubjectSet; +import io.opentdf.platform.policy.subjectmapping.*; + +// Get the attribute value ID from the attribute you fetched +String attributeValueId = getAttrResponse.getAttribute().getValues(0).getId(); + +// Create subject condition set that matches your identity +var condition = Condition.newBuilder() + .setSubjectExternalSelectorValue(".clientId") + .setOperator(SubjectMappingOperatorEnum.SUBJECT_MAPPING_OPERATOR_ENUM_IN) + .addSubjectExternalValues("opentdf") + .build(); + +var conditionGroup = ConditionGroup.newBuilder() + .setBooleanOperator(ConditionBooleanTypeEnum.CONDITION_BOOLEAN_TYPE_ENUM_AND) + .addConditions(condition) + .build(); + +var subjectSet = SubjectSet.newBuilder() + .addConditionGroups(conditionGroup) + .build(); + +var scsRequest = CreateSubjectConditionSetRequest.newBuilder() + .setSubjectConditionSet( + SubjectConditionSetCreate.newBuilder() + .addSubjectSets(subjectSet) + .build()) + .build(); + +var scsResponse = ResponseMessageKt.getOrThrow( + sdk.getServices() + .subjectMappings() + .createSubjectConditionSetBlocking(scsRequest, Collections.emptyMap()) + .execute()); + +// Create the subject mapping to grant yourself the entitlement +var smRequest = CreateSubjectMappingRequest.newBuilder() + .setAttributeValueId(attributeValueId) + .addActions(Action.newBuilder().setName("read").build()) + .setExistingSubjectConditionSetId(scsResponse.getSubjectConditionSet().getId()) + .build(); + +ResponseMessageKt.getOrThrow( + sdk.getServices() + .subjectMappings() + .createSubjectMappingBlocking(smRequest, Collections.emptyMap()) + .execute()); + +System.out.println("✅ Granted yourself access to department/marketing"); +``` + +Now you can decrypt the TDF you encrypted with the `marketing` attribute. 🎉 + + +### Save TDF to a File + +In production applications, you'll often need to persist encrypted TDFs to disk for storage, transmission, or archival. This allows you to: + +- **Separate encryption from distribution**: Encrypt data once, then share the TDF file through your preferred channels (email, S3, SFTP, etc.) +- **Enable offline access**: Recipients can decrypt TDFs without needing to re-fetch data from your application +- **Archive encrypted data**: Store TDFs in backup systems or long-term storage with their access policies intact + +```java +import java.io.FileOutputStream; + +// After creating TDF config +try (FileOutputStream fos = new FileOutputStream("encrypted.tdf")) { + sdk.createTDF(inputStream, fos, tdfConfig); +} + +// Later, decrypt from file +try (FileChannel fileChannel = FileChannel.open( + Path.of("encrypted.tdf"), + StandardOpenOption.READ)) { + var readerConfig = Config.newTDFReaderConfig(); + var reader = sdk.loadTDF(fileChannel, readerConfig); + var decryptedOutput = new ByteArrayOutputStream(); + reader.readPayload(decryptedOutput); + + String decryptedContent = new String(decryptedOutput.toByteArray(), StandardCharsets.UTF_8); + System.out.println("✅ Data successfully decrypted"); + System.out.println("📤 Decrypted content: " + decryptedContent); +} +``` + +### Handle Large Files with Streaming + +```java +import java.nio.channels.FileChannel; +import java.nio.file.StandardOpenOption; + +// Encrypt a large file +try (FileChannel inputChannel = FileChannel.open( + Path.of("large-file.pdf"), StandardOpenOption.READ); + FileChannel outputChannel = FileChannel.open( + Path.of("large-file.pdf.tdf"), + StandardOpenOption.WRITE, StandardOpenOption.CREATE)) { + + sdk.createTDF(inputChannel, outputChannel, tdfConfig); +} +``` + +## Step 6: Complete Reference Implementation {#step-6-java} + +For reference, here's a complete example showing all the pieces together: + +
+Create Attribute, Encrypt, Grant Access, and Decrypt + +```java title="src/main/java/io/opentdf/quickstart/QuickstartApp.java" +package io.opentdf.quickstart; + +import io.opentdf.platform.sdk.*; +import nl.altindag.ssl.SSLFactory; +import io.opentdf.platform.policy.AttributeRuleTypeEnum; +import io.opentdf.platform.policy.Action; +import io.opentdf.platform.policy.Condition; +import io.opentdf.platform.policy.ConditionBooleanTypeEnum; +import io.opentdf.platform.policy.ConditionGroup; +import io.opentdf.platform.policy.SubjectMappingOperatorEnum; +import io.opentdf.platform.policy.SubjectSet; +import io.opentdf.platform.policy.Namespace; +import io.opentdf.platform.policy.Attribute; +import io.opentdf.platform.policy.namespaces.CreateNamespaceRequest; +import io.opentdf.platform.policy.namespaces.CreateNamespaceResponse; +import io.opentdf.platform.policy.namespaces.ListNamespacesRequest; +import io.opentdf.platform.policy.attributes.CreateAttributeRequest; +import io.opentdf.platform.policy.attributes.CreateAttributeResponse; +import io.opentdf.platform.policy.attributes.ListAttributesRequest; +import io.opentdf.platform.policy.subjectmapping.*; +import com.connectrpc.ResponseMessageKt; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileOutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; + +public class QuickstartApp { + public static void main(String[] args) throws Exception { + String clientId = "opentdf"; + String clientSecret = "secret"; + String platformEndpoint = "https://platform.opentdf.local:8443"; + + // Create SSL factory that trusts all certificates + // withTrustingAllCertificatesWithoutValidation() disables certificate verification + // This allows connections to the platform's self-signed certificate + // Only use this for local development - never in production! + SSLFactory sslFactory = SSLFactory.builder() + .withTrustingAllCertificatesWithoutValidation() + .build(); + + // Create SDK client (try-with-resources ensures proper cleanup) + SDKBuilder builder = SDKBuilder.newBuilder(); + try (SDK sdk = builder.platformEndpoint(platformEndpoint) + .clientSecret(clientId, clientSecret) + .sslFactory(sslFactory) + .build()) { + + // KASInfo specifies the Key Access Service (KAS) endpoint + // KAS manages encryption keys and enforces access policies + var kasInfo = new Config.KASInfo(); + kasInfo.URL = platformEndpoint + "/kas"; + + // 1. Create namespace (or use existing) + CreateNamespaceResponse nsResponse; + try { + nsResponse = ResponseMessageKt.getOrThrow( + sdk.getServices() + .namespaces() + .createNamespaceBlocking( + CreateNamespaceRequest.newBuilder() + .setName("opentdf.io") + .build(), + Collections.emptyMap()) + .execute()); + System.out.println("✅ Created namespace: " + nsResponse.getNamespace().getId()); + } catch (Exception e) { + if (e.getMessage() != null && (e.getMessage().contains("already_exists") || e.getMessage().contains("unique field violation"))) { + // Namespace already exists, fetch it + var listResponse = ResponseMessageKt.getOrThrow( + sdk.getServices() + .namespaces() + .listNamespacesBlocking( + ListNamespacesRequest.newBuilder().build(), + Collections.emptyMap()) + .execute()); + Namespace existingNs = null; + for (Namespace ns : listResponse.getNamespacesList()) { + if ("opentdf.io".equals(ns.getName())) { + existingNs = ns; + break; + } + } + nsResponse = CreateNamespaceResponse.newBuilder() + .setNamespace(existingNs) + .build(); + System.out.println("✅ Using existing namespace: " + existingNs.getId()); + } else { + throw e; + } + } + + // 2. Create attribute with marketing value (or use existing) + var attrRequest = CreateAttributeRequest.newBuilder() + .setNamespaceId(nsResponse.getNamespace().getId()) + .setName("department") + .setRule(AttributeRuleTypeEnum.ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF) + .addAllValues(Arrays.asList("marketing")) + .build(); + + CreateAttributeResponse attrResponse; + try { + attrResponse = ResponseMessageKt.getOrThrow( + sdk.getServices() + .attributes() + .createAttributeBlocking(attrRequest, Collections.emptyMap()) + .execute()); + System.out.println("✅ Created attribute: " + attrResponse.getAttribute().getName()); + } catch (Exception e) { + if (e.getMessage() != null && (e.getMessage().contains("already_exists") || e.getMessage().contains("unique field violation"))) { + // Attribute already exists, fetch it + var listResponse = ResponseMessageKt.getOrThrow( + sdk.getServices() + .attributes() + .listAttributesBlocking( + ListAttributesRequest.newBuilder().build(), + Collections.emptyMap()) + .execute()); + Attribute existingAttr = null; + for (Attribute attr : listResponse.getAttributesList()) { + if ("department".equals(attr.getName()) && + attr.getNamespace().getId().equals(nsResponse.getNamespace().getId())) { + existingAttr = attr; + break; + } + } + attrResponse = CreateAttributeResponse.newBuilder() + .setAttribute(existingAttr) + .build(); + System.out.println("✅ Using existing attribute: " + existingAttr.getName()); + } else { + throw e; + } + } + + // 3. Encrypt data with the marketing attribute + String plaintext = "Sensitive marketing campaign data"; + var inputStream = new ByteArrayInputStream(plaintext.getBytes(StandardCharsets.UTF_8)); + var outputStream = new ByteArrayOutputStream(); + + var tdfConfig = Config.newTDFConfig( + Config.withKasInformation(kasInfo), + Config.withDataAttributes("https://opentdf.io/attr/department/value/marketing") + ); + + sdk.createTDF(inputStream, outputStream, tdfConfig); + System.out.println("✅ Data encrypted with marketing attribute"); + + // 4. Save TDF to file + try (var fos = new FileOutputStream("encrypted.tdf")) { + fos.write(outputStream.toByteArray()); + } + System.out.println("✅ TDF saved to encrypted.tdf"); + + // 5. Grant yourself access to the marketing attribute + // Find the marketing value by name + String attributeValueId = null; + for (var value : attrResponse.getAttribute().getValuesList()) { + if ("marketing".equals(value.getValue())) { + attributeValueId = value.getId(); + break; + } + } + if (attributeValueId == null) { + throw new RuntimeException("Marketing value not found in department attribute"); + } + + var condition = Condition.newBuilder() + .setSubjectExternalSelectorValue(".clientId") + .setOperator(SubjectMappingOperatorEnum.SUBJECT_MAPPING_OPERATOR_ENUM_IN) + .addSubjectExternalValues("opentdf") + .build(); + + var conditionGroup = ConditionGroup.newBuilder() + .setBooleanOperator(ConditionBooleanTypeEnum.CONDITION_BOOLEAN_TYPE_ENUM_AND) + .addConditions(condition) + .build(); + + var subjectSet = SubjectSet.newBuilder() + .addConditionGroups(conditionGroup) + .build(); + + var scsRequest = CreateSubjectConditionSetRequest.newBuilder() + .setSubjectConditionSet( + SubjectConditionSetCreate.newBuilder() + .addSubjectSets(subjectSet) + .build()) + .build(); + + var scsResponse = ResponseMessageKt.getOrThrow( + sdk.getServices() + .subjectMappings() + .createSubjectConditionSetBlocking(scsRequest, Collections.emptyMap()) + .execute()); + + var smRequest = CreateSubjectMappingRequest.newBuilder() + .setAttributeValueId(attributeValueId) + .addActions(Action.newBuilder().setName("read").build()) + .setExistingSubjectConditionSetId(scsResponse.getSubjectConditionSet().getId()) + .build(); + + try { + ResponseMessageKt.getOrThrow( + sdk.getServices() + .subjectMappings() + .createSubjectMappingBlocking(smRequest, Collections.emptyMap()) + .execute()); + System.out.println("✅ Granted yourself access to department/marketing"); + } catch (Exception e) { + if (e.getMessage() != null && (e.getMessage().contains("already_exists") || e.getMessage().contains("unique field violation"))) { + System.out.println("✅ Subject mapping already exists for department/marketing"); + } else { + throw e; + } + } + + // 6. Load TDF from file and decrypt + System.out.println("✅ TDF loaded from encrypted.tdf"); + + try (java.nio.channels.FileChannel fileChannel = java.nio.channels.FileChannel.open( + Path.of("encrypted.tdf"), + java.nio.file.StandardOpenOption.READ)) { + var readerConfig = Config.newTDFReaderConfig(); + var reader = sdk.loadTDF(fileChannel, readerConfig); + var decryptedStream = new ByteArrayOutputStream(); + reader.readPayload(decryptedStream); + + System.out.println("✅ Data successfully decrypted"); + System.out.println("📤 Decrypted content: " + decryptedStream.toString(StandardCharsets.UTF_8)); + } + } // SDK auto-closes here, shutting down gRPC connections + + // Force JVM exit (SDK background threads may not shut down immediately) + System.exit(0); + } +} +``` + +
+ +## Troubleshooting + +Having issues? See the **[SDK Troubleshooting](/sdks/troubleshooting)** guide for solutions to common problems. diff --git a/docs/sdks/quickstart/javascript.mdx b/docs/sdks/quickstart/javascript.mdx new file mode 100644 index 00000000..c7b3992a --- /dev/null +++ b/docs/sdks/quickstart/javascript.mdx @@ -0,0 +1,26 @@ +--- +sidebar_position: 3 +title: JavaScript/TypeScript +--- + +# JavaScript/TypeScript SDK Quickstart + +:::tip Back to SDK Quickstart +This guide covers the **JavaScript/TypeScript SDK** implementation. For other languages or general information, see the [SDK Quickstart](/sdks/quickstart) page. +::: + +:::info Coming Soon +The JavaScript/TypeScript SDK quickstart guide is currently under development. + +In the meantime, you can: +- Review the [SDK overview](/sdks/overview) +- Check out the [SDK reference examples](https://github.com/opentdf/client-web/tree/main/examples) on GitHub +- Explore the [Go](/sdks/quickstart/go) or [Java](/sdks/quickstart/java) quickstart guides for similar concepts +- See the [Getting Started](/quickstart) guide to set up your OpenTDF platform + +Check back soon for a complete JavaScript/TypeScript quickstart guide! +::: + +## Troubleshooting + +Having issues? See the **[SDK Troubleshooting](/sdks/troubleshooting)** guide for solutions to common problems. diff --git a/docs/sdks/troubleshooting.mdx b/docs/sdks/troubleshooting.mdx new file mode 100644 index 00000000..4910e43c --- /dev/null +++ b/docs/sdks/troubleshooting.mdx @@ -0,0 +1,235 @@ +--- +sidebar_position: 10 +title: Troubleshooting +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# SDK Troubleshooting + +This guide covers common issues when working with the OpenTDF SDKs and how to resolve them. + +:::note Troubleshooting Scope +This section covers issues when **using the SDKs in your applications**. For platform installation and setup issues, see [Quickstart Troubleshooting](/quickstart#troubleshooting). +::: + +## Authentication Failed + +**Error**: `rpc error: code = Unauthenticated` or `401 Unauthorized` + +**Solution**: Verify your credentials are correct (replace `opentdf` with your client ID and `secret` with your client secret): +```bash +otdfctl auth client-credentials opentdf secret +``` + +Check that Keycloak is running: +```bash +curl https://keycloak.opentdf.local:9443/ +``` + +## Certificate Errors (SSL/TLS) + +**Error**: `x509: certificate signed by unknown authority` or `Failed to validate TLS certificates` + +**Cause**: The platform uses self-signed certificates for local development. During installation, the [setup script](/quickstart#step-2-install-opentdf) automatically imports the Caddy root CA certificate to your system keychain. + +**Solution**: If you're seeing certificate errors, the certificate import may have failed during installation. Try these steps: + +1. **Restart your browser and terminal** - The certificate import requires applications to reload their certificate stores. Close and reopen your browser and terminal windows. + +2. **Verify the certificate was imported** - During the [installation process](/quickstart#step-2-install-opentdf), you should have been prompted twice for your password: + - Once to add entries to /etc/hosts + - Once to import the SSL certificate + + If you don't remember seeing both prompts, the certificate import may have failed. + +3. **Skip certificate verification** (quick workaround for CLI): + ```bash + export OTDFCTL_TLS_NO_VERIFY=true + ``` + This allows `otdfctl` commands to work without validating certificates. + +4. **Manually trust the certificate** - For SDKs and applications, you'll need to properly trust the certificate. See the [TLS Certificate Verification](/getting-started/managing-platform#tls-certificate-verification) guide for detailed steps on extracting and trusting the Caddy root CA certificate on macOS, Linux, or Windows. + +## Connection Refused + +**Error**: `connection refused` or `platform unreachable` + +**Solution**: Verify your platform is running: +```bash +curl https://platform.opentdf.local:8443/healthz +``` + +Should return: `{"status":"SERVING"}` + +If not running, restart the platform: +```bash +cd ~/.opentdf/platform +docker compose up -d +``` + +## Import Errors + +**Go**: `package github.com/opentdf/platform/sdk is not in GOROOT` +```bash +go mod tidy +go get github.com/opentdf/platform/sdk@latest +``` + +**Java**: `package io.opentdf.platform.sdk does not exist` +```bash +mvn clean install -U +``` + +**JavaScript**: `Cannot find module '@opentdf/client'` +```bash +npm install @opentdf/client +``` + +## Permission Denied / Insufficient Entitlements + +**Error**: When attempting to decrypt a TDF, you may see different error formats depending on your SDK: + + + + +``` +reader.WriteTo failed: splitKey.unable to reconstruct split key: map[{https://platform.opentdf.local:8443 }:tdf: rewrap request 403 +kao unwrap failed for split {https://platform.opentdf.local:8443 }: permission_denied: request error +rpc error: code = PermissionDenied desc = forbidden] +``` + +**What to Look For**: The meaningful information is buried in the error. Look for: +- `PermissionDenied` or `forbidden` +- `rewrap request 403` +- `permission_denied: request error` + + + + +``` +io.opentdf.platform.sdk.SDK$SplitKeyException: splitKey.unable to reconstruct split key: +{KeySplitStep{kas='https://platform.opentdf.local:8443/kas', splitID=''}=io.opentdf.platform.sdk.SDKException: error unwrapping key} +error unwrapping key +``` + +**What to Look For**: The meaningful information is: +- `splitKey.unable to reconstruct split key` +- `error unwrapping key` +- `SplitKeyException` + + + + +``` +Error: splitKey.unable to reconstruct split key +error unwrapping key +``` + +**What to Look For**: The meaningful information is: +- `unable to reconstruct split key` +- `error unwrapping key` + + + + +These errors indicate an authorization failure, not a cryptographic or network issue. + +**Cause**: Your identity lacks the required entitlements (attribute values) to decrypt the TDF. This is ABAC (Attribute-Based Access Control) working correctly. + +For example, if you encrypted data with `https://opentdf.io/attr/department/value/marketing`, you need a subject mapping that grants you the `marketing` entitlement. + +**Solution**: Grant yourself (or the entity) entitlements by [creating a subject mapping](/sdks/policy#create-subject-mapping). + +**Quick fix using the CLI:** + +```bash +# List your attributes to get the attribute value ID +otdfctl policy attributes list --namespace opentdf.io + +# Create a subject condition set (if needed) +otdfctl policy subject-condition-sets create \ + --subject-set '[".clientId == \"opentdf\""]' \ + --label "OpenTDF Service Account" + +export SUBJECT_CONDITION_SET_ID= + +# Create the subject mapping to grant the entitlement +otdfctl policy subject-mappings create \ + --action read \ + --attribute-value-fqn https://opentdf.io/attr/department/value/marketing \ + --subject-condition-set-id $SUBJECT_CONDITION_SET_ID +``` + +## Resource Already Exists + +**Error**: `already_exists: resource unique field violation` + +**Example**: +``` +2026/02/02 15:45:14 Failed to create namespace: already_exists: resource unique field violation +exit status 1 +``` + +**Cause**: You're trying to create a resource (namespace, attribute, subject mapping, etc.) that already exists on the platform. This commonly happens when running the quickstart examples multiple times. + +**Solution**: This is expected behavior when re-running examples. You have several options: + +1. **Handle the error in your code** (recommended): The complete examples in the language-specific quickstart guides show how to handle `already_exists` errors gracefully by catching the error and fetching the existing resource instead. + +2. **Deactivate or delete the existing resource**: + + :::tip Deactivate vs Delete + In production environments, **deactivate** attributes instead of deleting them to preserve access to historical TDFs. Deletion is an unsafe operation that permanently removes the resource and can break decryption of existing TDFs. + + Use **delete** only in development/testing contexts when you need to completely remove test data. + ::: + + ```bash + # List attributes to find the ID + otdfctl policy attributes list + + # Deactivate the attribute (recommended - preserves historical TDFs) + otdfctl policy attributes deactivate --id + + # OR delete the attribute (development/testing only - unsafe operation) + otdfctl policy attributes unsafe delete --id + ``` + +3. **Do nothing**: In some cases, failing when a resource already exists is the correct behavior. Not all code should be idempotent - sometimes you want to be alerted when attempting to create a duplicate resource. + +## Resource Not Found + +**Error**: `not_found: resource not found` or `attribute not found` + +**Cause**: You're trying to use a resource (namespace, attribute, or attribute value) that doesn't exist on the platform. + +**Solution**: Create the resource first using otdfctl or the SDK policy functions. For attributes, see the [SDK Create Attribute function](/sdks/policy#create-attribute). + +**Quick fix using the CLI:** + +```bash +# Get the namespace ID for opentdf.io +# (Namespace should exist from platform initialization - see /quickstart) +export NAMESPACE_ID=$(otdfctl policy attributes list --json | jq -r '.attributes[0].namespace.id // empty') + +# Create the attribute definition with initial values +otdfctl policy attributes create \ + --name department \ + --namespace $NAMESPACE_ID \ + --rule ANY_OF \ + --value finance \ + --value engineering \ + --value marketing +``` + +Alternatively, to add values after creating the attribute: + +```bash +# Get the attribute ID for 'department' in the 'opentdf.io' namespace +export DEPT_ATTRIBUTE_ID=$(otdfctl policy attributes list --json | jq -r '[.attributes[] | select(.name=="department" and .namespace.name=="opentdf.io")][0].id') + +# Add individual value(s) +otdfctl policy attributes values create --attribute-id $DEPT_ATTRIBUTE_ID --value marketing +``` diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 361848c5..cb8d5e40 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -175,7 +175,7 @@ const config: Config = { }, { label: "Roadmap", - href: "https://github.com/orgs/opentdf/discussions/1806", + href: "https://opentdf.io/appendix/matrix", }, ], }, @@ -383,6 +383,9 @@ ${updatedContent}`, ], require.resolve("docusaurus-lunr-search"), ], + clientModules: [ + require.resolve('./src/theme/TabsHashSync.js'), + ], }; export default config; diff --git a/sidebars.js b/sidebars.js index 33275803..9bea9d04 100644 --- a/sidebars.js +++ b/sidebars.js @@ -13,21 +13,12 @@ /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ const sidebars = { - // By default, Docusaurus generates a sidebar from the docs folder structure - tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], - - // But you can create a sidebar manually - /* tutorialSidebar: [ - 'intro', - 'hello', { - type: 'category', - label: 'Tutorial', - items: ['tutorial-basics/create-a-document'], + type: 'autogenerated', + dirName: '.', }, ], - */ }; export default sidebars; diff --git a/src/theme/TabsHashSync.js b/src/theme/TabsHashSync.js new file mode 100644 index 00000000..4f5bcc15 --- /dev/null +++ b/src/theme/TabsHashSync.js @@ -0,0 +1,99 @@ +import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; + +if (ExecutionEnvironment.canUseDOM) { + // Function to determine which tab should be active based on the hash + function getTabFromHash(hash) { + if (!hash) return null; + + // Remove the # from the hash + const anchor = hash.substring(1); + + // Map hash patterns to tab values + // Check if the hash contains specific SDK identifiers + // IMPORTANT: Check -javascript BEFORE -java since -javascript contains -java + if (anchor.includes('-javascript')) { + return 'js'; // Tab value is "js" not "javascript" + } else if (anchor.includes('-java')) { + return 'java'; + } else if (anchor.includes('-go')) { + return 'go'; + } + + // Check for section IDs + if (anchor === 'java-sdk-implementation' || anchor.startsWith('next-steps-with-java')) { + return 'java'; + } else if (anchor === 'javascript-sdk-implementation' || anchor.startsWith('next-steps-with-javascript')) { + return 'js'; // Tab value is "js" not "javascript" + } else if (anchor === 'go-sdk-implementation' || anchor.startsWith('next-steps-with-go')) { + return 'go'; + } + + return null; + } + + // Function to activate a tab + function activateTab(tabValue) { + // Find all tab buttons + const tabButtons = document.querySelectorAll('[role="tab"]'); + + for (const button of tabButtons) { + // Try different possible attribute names + const buttonValue = button.getAttribute('data-value') || + button.getAttribute('value') || + button.getAttribute('data-tab-value'); + + // Try matching by text content as fallback + const buttonText = button.textContent?.trim().toLowerCase(); + + // Match logic: + // 1. Exact match on value attribute + // 2. Exact match on button text + // 3. Special case: 'js' matches button text starting with 'javascript' + const isMatch = (buttonValue && buttonValue.toLowerCase() === tabValue.toLowerCase()) || + (buttonText === tabValue.toLowerCase()) || + (tabValue === 'js' && buttonText?.startsWith('javascript')); + + if (isMatch) { + // Click the tab to activate it + button.click(); + return true; + } + } + + return false; + } + + // Function to handle hash-based tab switching + function handleHashTabSwitch() { + const hash = window.location.hash; + const targetTab = getTabFromHash(hash); + + if (targetTab) { + // Wait a bit for Docusaurus to render the tabs + setTimeout(() => { + if (activateTab(targetTab)) { + // Wait for tab content to be visible, then scroll to the hash + setTimeout(() => { + const element = document.getElementById(hash.substring(1)); + if (element) { + element.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + }, 200); + } + }, 300); // Increased wait time for tabs to render + } + } + + // Run on page load + window.addEventListener('load', handleHashTabSwitch); + + // Run on hash change (when clicking anchor links on the same page) + window.addEventListener('hashchange', handleHashTabSwitch); + + // Also run immediately in case tabs are already rendered + if (document.readyState === 'complete') { + handleHashTabSwitch(); + } +} + +export default {}; diff --git a/static/quickstart/check.sh b/static/quickstart/check.sh index 01b44aac..ac3764cd 100644 --- a/static/quickstart/check.sh +++ b/static/quickstart/check.sh @@ -154,6 +154,7 @@ check_port() { check_port 8080 || true check_port 8443 || true check_port 9443 || true +check_port 2019 || true echo "" # Check for sudo access (needed for /etc/hosts) diff --git a/tests/quickstart.bats b/tests/quickstart.bats index bf4e9077..64b34d68 100644 --- a/tests/quickstart.bats +++ b/tests/quickstart.bats @@ -90,6 +90,9 @@ setup() { run bash -c "grep -q 'check_port.*9443' $SCRIPT_DIR/check.sh" [ "$status" -eq 0 ] + + run bash -c "grep -q 'check_port.*2019' $SCRIPT_DIR/check.sh" + [ "$status" -eq 0 ] } # Script structure tests