From d8cf3758b58c5f41e31caedb8eecd457f40d311c Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Fri, 30 Jan 2026 14:02:27 -0800 Subject: [PATCH 01/18] feat: DSPX-2347 improvements to quickstart guide Signed-off-by: Mary Dickson --- docs/getting-started/quickstart.mdx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/getting-started/quickstart.mdx b/docs/getting-started/quickstart.mdx index 23d69f1..db9a495 100644 --- a/docs/getting-started/quickstart.mdx +++ b/docs/getting-started/quickstart.mdx @@ -692,6 +692,8 @@ Save the department attribute ID: export DEPT_ATTRIBUTE_ID= ``` +No output expected. + ### Add Department Values Now add the specific departments. Preston works in finance, and Jack works in engineering: @@ -720,6 +722,8 @@ Save the finance value ID: export FINANCE_VALUE_ID= ``` +No output expected. + ```shell otdfctl policy attributes values create -a $DEPT_ATTRIBUTE_ID --value engineering ``` From f3af2491100b6eaee87e1b7bb8896cef2ea784f5 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Fri, 30 Jan 2026 15:56:03 -0800 Subject: [PATCH 02/18] feat: DSPX-2347 formatting and styling Signed-off-by: Mary Dickson --- docs/getting-started/quickstart.mdx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/getting-started/quickstart.mdx b/docs/getting-started/quickstart.mdx index db9a495..23d69f1 100644 --- a/docs/getting-started/quickstart.mdx +++ b/docs/getting-started/quickstart.mdx @@ -692,8 +692,6 @@ Save the department attribute ID: export DEPT_ATTRIBUTE_ID= ``` -No output expected. - ### Add Department Values Now add the specific departments. Preston works in finance, and Jack works in engineering: @@ -722,8 +720,6 @@ Save the finance value ID: export FINANCE_VALUE_ID= ``` -No output expected. - ```shell otdfctl policy attributes values create -a $DEPT_ATTRIBUTE_ID --value engineering ``` From 6e823f9981212e3b9cf162b0490b17895bfb2de8 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Mon, 2 Feb 2026 16:48:55 -0800 Subject: [PATCH 03/18] feat: DSPX-2345 SDK Quickstart Guide Signed-off-by: Mary Dickson --- code_samples/policy_code/create_attribute.mdx | 2 +- code_samples/policy_code/create_namespace.mdx | 2 +- .../create_subject_condition_set.mdx | 2 +- .../policy_code/create_subject_mapping.mdx | 2 +- code_samples/policy_code/list_attributes.mdx | 2 +- code_samples/policy_code/list_namespaces.mdx | 2 +- .../policy_code/list_subject_mapping.mdx | 2 +- docs/sdks/quickstart.mdx | 1862 +++++++++++++++++ 8 files changed, 1869 insertions(+), 7 deletions(-) create mode 100644 docs/sdks/quickstart.mdx diff --git a/code_samples/policy_code/create_attribute.mdx b/code_samples/policy_code/create_attribute.mdx index 1680efa..0dddecc 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 e3d7015..19c0e25 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 3cb88b3..174519a 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 685cd7e..2ecc88e 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 23c224f..89be381 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 827d05c..e847dc0 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 44150cf..5b20dc2 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/sdks/quickstart.mdx b/docs/sdks/quickstart.mdx new file mode 100644 index 0000000..acfd5fd --- /dev/null +++ b/docs/sdks/quickstart.mdx @@ -0,0 +1,1862 @@ +--- +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 + +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 + +OpenTDF provides native SDKs in three languages. **Use the tabs below to select your language** - all examples are complete and ready to run. + +:::note +Each tab contains a complete, working example for that specific SDK. The code and commands are different for each language. +::: + + + + +## Go SDK Implementation + +:::info Language: Go +This section covers the **Go SDK** implementation. Switch tabs above to see Java or JavaScript examples. +::: + +### Prerequisites + +- Go 1.21 or later +- Your OpenTDF platform running locally (from Getting Started guide) + +### Step 1: Create a New Project + +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 + +```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 + +#### 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), + sdk.WithInsecurePlaintextConn(), // Only for local development! + ) + + 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, + 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 + +```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! +``` + +
+ +--- + +### Next Steps with Go SDK + +:::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 an Attribute** + +First, let's create an attribute that we can use for access control. We'll create a department attribute with a marketing value: + +```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" +) + +// Create the 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()) +} + +// Create the attribute with the marketing value +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 { + log.Fatalf("Failed to create attribute: %v", err) +} + +log.Printf("Created attribute: %s", attrResp.GetAttribute().GetName()) +log.Printf("Full attribute FQN: https://opentdf.io/attr/department/value/marketing") +``` + +:::note +You may encounter errors if attempting to create a resource that already exists. The [complete example below](#complete-example-create-attribute-encrypt-grant-access-and-decrypt) includes full error handling for idempotency. +::: + +**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"), + 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. 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. + +```go +import ( + "github.com/opentdf/platform/protocol/go/policy/subjectmapping" +) + +// Get the attribute value ID from the attribute you created +attributeValueId := attrResp.GetAttribute().GetValues()[0].GetId() + +// 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** + +```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() + +_, err = client.CreateTDF(outputFile, inputFile, + sdk.WithKasInformation(sdk.KASInfo{URL: platformEndpoint}), +) +``` + +
+Complete Example: Create Attribute, Encrypt, Grant Access, and Decrypt + +Here's a complete example that brings all the pieces together: + +```go +package main + +import ( + "bytes" + "context" + "log" + "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), + sdk.WithInsecurePlaintextConn(), + ) + 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 + + _, 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. Grant yourself access to the marketing attribute + attributeValueId := attrResp.GetAttribute().GetValues()[0].GetId() + + scsResp, err := client.SubjectMapping.CreateSubjectConditionSet(context.Background(), + &subjectmapping.CreateSubjectConditionSetRequest{ + SubjectConditionSet: &subjectmapping.SubjectConditionSetCreate{ + SubjectSets: []*policy.SubjectSet{ + { + ConditionGroups: []*policy.ConditionGroup{ + { + 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") + } + + // 5. Decrypt the data + tdfReader, err := client.LoadTDF(bytes.NewReader(encryptedBuffer.Bytes())) + 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()) +} +``` + +
+ +
+ + +## Java SDK Implementation + +:::info Language: Java +This section covers the **Java SDK** implementation. Switch tabs above to see Go or JavaScript examples. +::: + +### Prerequisites + +- Java 17 or later +- Maven or Gradle +- Your OpenTDF platform running locally (from Getting Started guide) + +### Step 1: Create a New Maven Project + +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 + + + + + io.opentdf.platform + sdk + 0.6.2 + + + +``` + +### Step 2: Install Dependencies + +```bash +mvn clean install +``` + +This will download the OpenTDF Java SDK and its dependencies (including Bouncy Castle for cryptography). + +### Step 3: Create Your Application + +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 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 SDK client with client credentials + System.out.println("πŸ” Initializing SDK client with client credentials..."); + SDKBuilder builder = SDKBuilder.newBuilder(); + SDK sdk = builder.platformEndpoint(platformEndpoint) + .clientSecret(clientId, clientSecret) + .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."; + + 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!"); + + } catch (Exception e) { + System.err.println("❌ Error occurred: " + e.getMessage()); + e.printStackTrace(); + System.exit(1); + } + } +} +``` + +### Step 4: Run Your Application + +```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! +``` + +
+ +--- + +### Next Steps with Java SDK + +:::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 an Attribute** + +First, let's create an attribute that we can use for access control. We'll create a department attribute with a marketing value: + +```java +import io.opentdf.platform.policy.AttributeRuleTypeEnum; +import io.opentdf.platform.policy.Namespace; +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 com.connectrpc.ResponseMessageKt; +import java.util.Arrays; +import java.util.Collections; + +// Create the 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")) { + // 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; + } +} + +// Create the attribute with the marketing value +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 = ResponseMessageKt.getOrThrow( + sdk.getServices() + .attributes() + .createAttributeBlocking(attrRequest, Collections.emptyMap()) + .execute()); + +System.out.println("Created attribute: " + attrResponse.getAttribute().getName()); +System.out.println("Full attribute FQN: https://opentdf.io/attr/department/value/marketing"); +``` + +:::note +You may encounter errors if attempting to create a resource that already exists. The [complete example below](#complete-example-create-attribute-encrypt-grant-access-and-decrypt) includes full error handling for idempotency. +::: + +**Add Attributes for Access Control** + +Now that you've created the attribute, update your `createTDF` call 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. 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. + +```java +import io.opentdf.platform.policy.Action; +import io.opentdf.platform.policy.SubjectMappingOperatorEnum; +import io.opentdf.platform.policy.subjectmapping.*; + +// Get the attribute value ID from the attribute you created +String attributeValueId = attrResponse.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() + .addConditions(condition) + .build(); + +var subjectSet = SubjectSet.newBuilder() + .addConditionGroups(conditionGroup) + .build(); + +var scsRequest = CreateSubjectConditionSetRequest.newBuilder() + .setSubjectConditionSet( + SubjectConditionSet.newBuilder() + .addSubjectSets(subjectSet) + .build()) + .build(); + +var scsResponse = ResponseMessageKt.getOrThrow( + sdk.getServices() + .subjectMapping() + .createSubjectConditionSetBlocking(scsRequest, Collections.emptyMap()) + .execute()); + +// Create the subject mapping to grant yourself the entitlement +var smRequest = CreateSubjectMappingRequest.newBuilder() + .setAttributeValueId(attributeValueId) + .addActions(Action.newBuilder() + .setStandard(Action.StandardAction.STANDARD_ACTION_DECRYPT)) + .setSubjectConditionSetId(scsResponse.getSubjectConditionSet().getId()) + .build(); + +ResponseMessageKt.getOrThrow( + sdk.getServices() + .subjectMapping() + .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** + +```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 reader = sdk.loadTDF(fileChannel, Config.newTDFReaderConfig()); + reader.readPayload(System.out); +} +``` + +**Handle Large Files** + +```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); +} +``` + +
+Complete Example: Create Attribute, Encrypt, Grant Access, and Decrypt + +Here's a complete example that brings all the pieces together: + +```java +package io.opentdf.quickstart; + +import io.opentdf.platform.sdk.*; +import io.opentdf.platform.policy.AttributeRuleTypeEnum; +import io.opentdf.platform.policy.Action; +import io.opentdf.platform.policy.SubjectMappingOperatorEnum; +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.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; + +public class CompleteExample { + public static void main(String[] args) throws Exception { + String clientId = "opentdf"; + String clientSecret = "secret"; + String platformEndpoint = "localhost:8443"; + + // Create SDK client + SDK sdk = new SDKBuilder() + .platformEndpoint(platformEndpoint) + .clientSecret(clientId, clientSecret) + .useInsecurePlaintextConnection(true) + .build(); + + var kasInfo = new Config.KASInfo(); + kasInfo.URL = "http://localhost:8080/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")) { + // 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")) { + // 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. Grant yourself access to the marketing attribute + String attributeValueId = attrResponse.getAttribute().getValues(0).getId(); + + var condition = Condition.newBuilder() + .setSubjectExternalSelectorValue(".clientId") + .setOperator(SubjectMappingOperatorEnum.SUBJECT_MAPPING_OPERATOR_ENUM_IN) + .addSubjectExternalValues("opentdf") + .build(); + + var scsRequest = CreateSubjectConditionSetRequest.newBuilder() + .setSubjectConditionSet( + SubjectConditionSet.newBuilder() + .addSubjectSets(SubjectSet.newBuilder() + .addConditionGroups(ConditionGroup.newBuilder() + .addConditions(condition) + .build()) + .build()) + .build()) + .build(); + + var scsResponse = ResponseMessageKt.getOrThrow( + sdk.getServices() + .subjectMapping() + .createSubjectConditionSetBlocking(scsRequest, Collections.emptyMap()) + .execute()); + + var smRequest = CreateSubjectMappingRequest.newBuilder() + .setAttributeValueId(attributeValueId) + .addActions(Action.newBuilder() + .setStandard(Action.StandardAction.STANDARD_ACTION_DECRYPT)) + .setSubjectConditionSetId(scsResponse.getSubjectConditionSet().getId()) + .build(); + + try { + ResponseMessageKt.getOrThrow( + sdk.getServices() + .subjectMapping() + .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")) { + System.out.println("βœ… Subject mapping already exists for department/marketing"); + } else { + throw e; + } + } + + // 5. Decrypt the data + var encryptedBytes = outputStream.toByteArray(); + var decryptedStream = new ByteArrayOutputStream(); + + var reader = sdk.loadTDF(new ByteArrayInputStream(encryptedBytes)); + reader.readPayload(decryptedStream); + + System.out.println("βœ… Data successfully decrypted"); + System.out.println("πŸ“€ Decrypted content: " + decryptedStream.toString(StandardCharsets.UTF_8)); + } +} +``` + +
+ +
+ + +## JavaScript/TypeScript SDK Implementation + +:::info Language: JavaScript/TypeScript +This section covers the **JavaScript/TypeScript SDK** implementation. Switch tabs above to see Go or Java examples. +::: + +### Prerequisites + +- Node.js 18 or later +- npm or yarn +- Your OpenTDF platform running locally (from Getting Started guide) + +### Step 1: Create a New Project + +Create a new directory and initialize a Node.js project: + +```bash +mkdir opentdf-quickstart +cd opentdf-quickstart +npm init -y +``` + +### Step 2: Install the SDK + +```bash +npm install @opentdf/client +``` + +Expected output: +> ```console +> added 45 packages, and audited 46 packages in 3s +> ``` + +### Step 3: Create Your Application + +#### TypeScript Implementation Code + +For **TypeScript** projects, create `quickstart.ts`: + +```typescript title="quickstart.ts" +import { AuthProviders, OpenTDF } from '@opentdf/client'; + +async function main() { + try { + console.log("πŸš€ Starting OpenTDF SDK Quickstart..."); + + const platformEndpoint = "https://platform.opentdf.local:8443"; + const oidcOrigin = "https://keycloak.opentdf.local:9443/realms/opentdf"; + const clientId = "opentdf"; + const clientSecret = "secret"; + + console.log("πŸ“‘ Connecting to platform: " + platformEndpoint); + + // Create authentication provider with client credentials + console.log("πŸ” Setting up client credentials authentication..."); + const authProvider = await AuthProviders.clientSecretAuthProvider({ + clientId, + clientSecret, + oidcOrigin, + exchange: 'client', + }); + console.log("βœ… Authentication provider created"); + + // Create OpenTDF client + console.log("πŸ”§ Initializing SDK client..."); + const client = new OpenTDF({ + authProvider: authProvider, + kasEndpoint: platformEndpoint, + }); + console.log("βœ… SDK client initialized successfully"); + + // Encrypt data + console.log("\nπŸ“ Encrypting sensitive data..."); + const sensitiveData = "Hello from the OpenTDF JavaScript SDK! This data is encrypted."; + const dataBuffer = new TextEncoder().encode(sensitiveData); + + console.log("πŸ”’ Creating TDF..."); + const encryptedStream = await client.encrypt({ + offline: false, + source: dataBuffer, + }); + + // Convert stream to buffer + const encryptedBuffer = await new Response(encryptedStream).arrayBuffer(); + const encryptedBytes = new Uint8Array(encryptedBuffer); + + console.log("βœ… Data successfully encrypted"); + console.log("πŸ“Š Encrypted TDF size: " + encryptedBytes.length + " bytes"); + + // Decrypt data + console.log("\nπŸ”“ Decrypting TDF..."); + const decryptedStream = await client.decrypt({ + source: encryptedBytes, + }); + + const decryptedText = await new Response(decryptedStream).text(); + + console.log("βœ… Data successfully decrypted"); + console.log("πŸ“€ Decrypted content:\n\n" + decryptedText + "\n"); + + console.log("πŸŽ‰ Quickstart complete!"); + process.exit(0); + + } catch (error) { + console.error("❌ Error occurred:", error); + process.exit(1); + } +} + +main(); +``` + +#### JavaScript Implementation Code + +For **JavaScript** projects, create `quickstart.mjs`: + +```javascript title="quickstart.mjs" +import { AuthProviders, OpenTDF } from '@opentdf/client'; + +async function main() { + try { + console.log("πŸš€ Starting OpenTDF SDK Quickstart..."); + + const platformEndpoint = "https://platform.opentdf.local:8443"; + const oidcOrigin = "https://keycloak.opentdf.local:9443/realms/opentdf"; + const clientId = "opentdf"; + const clientSecret = "secret"; + + console.log("πŸ“‘ Connecting to platform: " + platformEndpoint); + + // Create authentication provider with client credentials + console.log("πŸ” Setting up client credentials authentication..."); + const authProvider = await AuthProviders.clientSecretAuthProvider({ + clientId, + clientSecret, + oidcOrigin, + exchange: 'client', + }); + console.log("βœ… Authentication provider created"); + + // Create OpenTDF client + console.log("πŸ”§ Initializing SDK client..."); + const client = new OpenTDF({ + authProvider: authProvider, + kasEndpoint: platformEndpoint, + }); + console.log("βœ… SDK client initialized successfully"); + + // Encrypt data + console.log("\nπŸ“ Encrypting sensitive data..."); + const sensitiveData = "Hello from the OpenTDF JavaScript SDK! This data is encrypted."; + const dataBuffer = new TextEncoder().encode(sensitiveData); + + console.log("πŸ”’ Creating TDF..."); + const encryptedStream = await client.encrypt({ + offline: false, + source: dataBuffer, + }); + + // Convert stream to buffer + const encryptedBuffer = await new Response(encryptedStream).arrayBuffer(); + const encryptedBytes = new Uint8Array(encryptedBuffer); + + console.log("βœ… Data successfully encrypted"); + console.log("πŸ“Š Encrypted TDF size: " + encryptedBytes.length + " bytes"); + + // Decrypt data + console.log("\nπŸ”“ Decrypting TDF..."); + const decryptedStream = await client.decrypt({ + source: encryptedBytes, + }); + + const decryptedText = await new Response(decryptedStream).text(); + + console.log("βœ… Data successfully decrypted"); + console.log("πŸ“€ Decrypted content:\n\n" + decryptedText + "\n"); + + console.log("πŸŽ‰ Quickstart complete!"); + process.exit(0); + + } catch (error) { + console.error("❌ Error occurred:", error); + process.exit(1); + } +} + +main(); +``` + +### Step 4: Run Your Application + +For TypeScript: +```bash +npx tsx quickstart.ts +``` + +For JavaScript: +```bash +node quickstart.mjs +``` + +
+Expected output + +```console +πŸš€ Starting OpenTDF SDK Quickstart... +πŸ“‘ Connecting to platform: https://platform.opentdf.local:8443 +πŸ” Setting up client credentials authentication... +βœ… Authentication provider created +πŸ”§ Initializing SDK client... +βœ… 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 JavaScript SDK! This data is encrypted. + +πŸŽ‰ Quickstart complete! +``` + +
+ +--- + +### Next Steps with JavaScript SDK + +:::tip JavaScript-Specific Examples +The following examples are specific to the **JavaScript/TypeScript 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 an Attribute** + +First, let's create an attribute that we can use for access control. We'll create a department attribute with a marketing value: + +```typescript +// Create the namespace (or use existing) +let nsResponse; +try { + nsResponse = await client.namespaces.createNamespace({ + name: 'opentdf.io', + }); + console.log(`Created namespace: ${nsResponse.namespace.id}`); +} catch (error: any) { + if (error.message && error.message.includes('already_exists')) { + // Namespace already exists, fetch it + const listResponse = await client.namespaces.listNamespaces(); + const existingNs = listResponse.namespaces.find( + (ns: any) => ns.name === 'opentdf.io' + ); + nsResponse = { namespace: existingNs }; + console.log(`Using existing namespace: ${existingNs.id}`); + } else { + throw error; + } +} + +// Create the attribute with the marketing value +const attrResponse = await client.attributes.createAttribute({ + namespaceId: nsResponse.namespace.id, + name: 'department', + rule: 'ANY_OF', + values: ['marketing'], +}); + +console.log(`Created attribute: ${attrResponse.attribute.name}`); +console.log('Full attribute FQN: https://opentdf.io/attr/department/value/marketing'); +``` + +:::note +You may encounter errors if attempting to create a resource that already exists. The [complete example below](#complete-example-create-attribute-encrypt-grant-access-and-decrypt) includes full error handling for idempotency. +::: + +**Add Attributes for Access Control** + +Now that you've created the attribute, update your `encrypt` call to include the attribute for access control: + +```typescript +const encryptedStream = await client.encrypt({ + offline: false, + source: dataBuffer, + attributes: ["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. 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. + +```typescript +// Get the attribute value ID from the attribute you created +const attributeValueId = attrResponse.attribute.values[0].id; + +// Create subject condition set that matches your identity +const scsResponse = await client.subjectMapping.createSubjectConditionSet({ + subjectConditionSet: { + subjectSets: [{ + conditionGroups: [{ + conditions: [{ + subjectExternalSelectorValue: '.clientId', + operator: 'SUBJECT_MAPPING_OPERATOR_ENUM_IN', + subjectExternalValues: ['opentdf'], + }], + }], + }], + }, +}); + +// Create the subject mapping to grant yourself the entitlement +await client.subjectMapping.createSubjectMapping({ + attributeValueId: attributeValueId, + actions: [{ standard: 'STANDARD_ACTION_DECRYPT' }], + subjectConditionSetId: scsResponse.subjectConditionSet.id, +}); + +console.log('βœ… Granted yourself access to department/marketing'); +``` + +Now you can decrypt the TDF you encrypted with the `marketing` attribute. + +**Save TDF to a File** + +```typescript +import * as fs from 'fs'; + +// After encryption +fs.writeFileSync('encrypted.tdf', encryptedBytes); + +// Later, decrypt from file +const fileData = fs.readFileSync('encrypted.tdf'); +const decryptedStream = await client.decrypt({ + source: new Uint8Array(fileData), +}); +``` + +**Handle Large Files with Streaming** + +```typescript +import * as fs from 'fs'; +import { Readable } from 'stream'; + +// Encrypt a large file +const fileStream = fs.createReadStream('large-file.pdf'); +const encryptedStream = await client.encrypt({ + offline: false, + source: fileStream, +}); + +// Save encrypted stream to file +const writeStream = fs.createWriteStream('large-file.pdf.tdf'); +Readable.fromWeb(encryptedStream).pipe(writeStream); +``` + +
+Complete Example: Create Attribute, Encrypt, Grant Access, and Decrypt + +Here's a complete example that brings all the pieces together: + +```typescript +import { AuthProviders, OpenTDF } from '@opentdf/client'; + +async function main() { + try { + const platformEndpoint = 'http://localhost:8080'; + const oidcEndpoint = 'http://localhost:8888/realms/opentdf'; + + // Create authentication provider + const authProvider = await AuthProviders.clientSecretAuthProvider({ + clientId: 'opentdf', + clientSecret: 'secret', + oidcOrigin: oidcEndpoint, + exchange: 'client', + }); + + // Create SDK client + const client = new OpenTDF({ + authProvider: authProvider, + platformUrl: platformEndpoint, + }); + + // 1. Create namespace (or use existing) + let nsResponse; + try { + nsResponse = await client.namespaces.createNamespace({ + name: 'opentdf.io', + }); + console.log(`βœ… Created namespace: ${nsResponse.namespace.id}`); + } catch (error: any) { + if (error.message && error.message.includes('already_exists')) { + // Namespace already exists, fetch it + const listResponse = await client.namespaces.listNamespaces(); + const existingNs = listResponse.namespaces.find( + (ns: any) => ns.name === 'opentdf.io' + ); + nsResponse = { namespace: existingNs }; + console.log(`βœ… Using existing namespace: ${existingNs.id}`); + } else { + throw error; + } + } + + // 2. Create attribute with marketing value (or use existing) + let attrResponse; + try { + attrResponse = await client.attributes.createAttribute({ + namespaceId: nsResponse.namespace.id, + name: 'department', + rule: 'ANY_OF', + values: ['marketing'], + }); + console.log(`βœ… Created attribute: ${attrResponse.attribute.name}`); + } catch (error: any) { + if (error.message && error.message.includes('already_exists')) { + // Attribute already exists, fetch it + const listResponse = await client.attributes.listAttributes(); + const existingAttr = listResponse.attributes.find( + (attr: any) => attr.name === 'department' && attr.namespace.id === nsResponse.namespace.id + ); + attrResponse = { attribute: existingAttr }; + console.log(`βœ… Using existing attribute: ${existingAttr.name}`); + } else { + throw error; + } + } + + // 3. Encrypt data with the marketing attribute + const plaintext = 'Sensitive marketing campaign data'; + const dataBuffer = new TextEncoder().encode(plaintext); + + const encryptedStream = await client.encrypt({ + offline: false, + source: dataBuffer, + attributes: ['https://opentdf.io/attr/department/value/marketing'], + }); + + const encryptedBuffer = await new Response(encryptedStream).arrayBuffer(); + const encryptedBytes = new Uint8Array(encryptedBuffer); + console.log('βœ… Data encrypted with marketing attribute'); + + // 4. Grant yourself access to the marketing attribute + const attributeValueId = attrResponse.attribute.values[0].id; + + const scsResponse = await client.subjectMapping.createSubjectConditionSet({ + subjectConditionSet: { + subjectSets: [{ + conditionGroups: [{ + conditions: [{ + subjectExternalSelectorValue: '.clientId', + operator: 'SUBJECT_MAPPING_OPERATOR_ENUM_IN', + subjectExternalValues: ['opentdf'], + }], + }], + }], + }, + }); + + try { + await client.subjectMapping.createSubjectMapping({ + attributeValueId: attributeValueId, + actions: [{ standard: 'STANDARD_ACTION_DECRYPT' }], + subjectConditionSetId: scsResponse.subjectConditionSet.id, + }); + console.log('βœ… Granted yourself access to department/marketing'); + } catch (error: any) { + if (error.message && error.message.includes('already_exists')) { + console.log('βœ… Subject mapping already exists for department/marketing'); + } else { + throw error; + } + } + + // 5. Decrypt the data + const decryptedStream = await client.decrypt({ + source: encryptedBytes, + }); + + const decryptedText = await new Response(decryptedStream).text(); + console.log('βœ… Data successfully decrypted'); + console.log(`πŸ“€ Decrypted content: ${decryptedText}`); + } catch (error) { + console.error('❌ Error:', error); + process.exit(1); + } +} + +main(); +``` + +
+ +
+
+ +## 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**: `.WithInsecurePlaintextConn()` +**Java SDK**: `.useInsecurePlaintextConnection(true)` +**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 +::: + +## Troubleshooting + +### Attribute Not Found + +**Error**: `not_found: resource not found` or `attribute not found` + +**Cause**: You're trying to encrypt with an attribute that doesn't exist on the platform. + +**Solution**: Create the attribute first using otdfctl or the [SDK Create Attribute function](/sdks/policy#create-attribute). + +**Quick fix using the CLI:** + +```bash +# Create the attribute definition +otdfctl policy attributes create --name department -s $NAMESPACE_ID -r ANY_OF +export DEPT_ATTRIBUTE_ID= + +# Add the specific value(s) +otdfctl policy attributes values create -a $DEPT_ATTRIBUTE_ID --value finance +``` + +### Permission Denied / Insufficient Entitlements + +**Error**: When attempting to decrypt a TDF, you may see a verbose error like: + +``` +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` + +These 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](#complete-example-create-attribute-encrypt-grant-access-and-decrypt) in this guide show how to handle `already_exists` errors gracefully by catching the error and fetching the existing resource instead. + +2. **Delete the existing resource first**: Use the CLI to remove the existing resource: + ```bash + # List resources to find the ID + otdfctl policy attributes namespaces list + + # Delete the resource + otdfctl policy attributes namespaces 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. + +### 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 +``` + +### Authentication Failed + +**Error**: `rpc error: code = Unauthenticated` or `401 Unauthorized` + +**Solution**: Verify your credentials are correct: +```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` + +**Solution**: The platform uses self-signed certificates for local development. The installation script should have imported these. If you're still seeing errors: + +1. Verify the certificate was imported during installation +2. Restart your terminal/IDE +3. For Go, use `sdk.WithInsecurePlaintextConn()` (development only) +4. For Java, the SDK handles this automatically +5. For Node.js, you may need: `NODE_TLS_REJECT_UNAUTHORIZED=0` (development only) + +### 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 +``` + +## What's Next? + +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 +- **[Performance Guide](https://github.com/opentdf/sdk-planning/blob/main/9-performance-characteristics-guide.md)**: 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 From 21f0a2af6fe49ee3a26e344e08ecd96a7141dae7 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Tue, 3 Feb 2026 08:31:31 -0800 Subject: [PATCH 04/18] updates to Java code and fix sidebar links Signed-off-by: Mary Dickson --- docs/sdks/quickstart.mdx | 87 +++++++++++++++++++++++----------- docusaurus.config.ts | 3 ++ src/theme/TabsHashSync.js | 99 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 162 insertions(+), 27 deletions(-) create mode 100644 src/theme/TabsHashSync.js diff --git a/docs/sdks/quickstart.mdx b/docs/sdks/quickstart.mdx index acfd5fd..781c926 100644 --- a/docs/sdks/quickstart.mdx +++ b/docs/sdks/quickstart.mdx @@ -21,7 +21,7 @@ Complete the [Getting Started](/quickstart) guide first to set up your local Ope This quickstart takes **15-20 minutes** to complete. ::: -## What You'll Build +## What You'll Build {#what-youll-build} A simple application that: 1. Connects to your local OpenTDF platform @@ -29,7 +29,7 @@ A simple application that: 3. Encrypts sensitive data into a TDF file 4. Decrypts the TDF file back to plaintext -## Choose Your SDK +## Choose Your SDK {#choose-your-sdk} OpenTDF provides native SDKs in three languages. **Use the tabs below to select your language** - all examples are complete and ready to run. @@ -40,7 +40,7 @@ Each tab contains a complete, working example for that specific SDK. The code an -## Go SDK Implementation +## Go SDK Implementation {#go-sdk-implementation} :::info Language: Go This section covers the **Go SDK** implementation. Switch tabs above to see Java or JavaScript examples. @@ -51,7 +51,7 @@ This section covers the **Go SDK** implementation. Switch tabs above to see Java - Go 1.21 or later - Your OpenTDF platform running locally (from Getting Started guide) -### Step 1: Create a New Project +### Step 1: Create a New Project {#step-1-go} Create a new directory and initialize a Go module: @@ -61,7 +61,7 @@ cd opentdf-quickstart go mod init opentdf-quickstart ``` -### Step 2: Install the SDK +### Step 2: Install the SDK {#step-2-go} ```bash go get github.com/opentdf/platform/sdk@latest @@ -73,7 +73,7 @@ Expected output: > go: added github.com/opentdf/platform/sdk v0.x.x > ``` -### Step 3: Create Your Application +### Step 3: Create Your Application {#step-3-go} #### Go Implementation Code @@ -153,7 +153,7 @@ func main() { } ``` -### Step 4: Run Your Application +### Step 4: Run Your Application {#step-4-go} ```bash go run main.go @@ -186,7 +186,7 @@ Hello from the OpenTDF Go SDK! This data is encrypted. --- -### Next Steps with Go SDK +### Next Steps with Go SDK {#next-steps-with-go-sdk} :::tip Go-Specific Examples The following examples are specific to the **Go SDK**. @@ -545,7 +545,7 @@ func main() { -## Java SDK Implementation +## Java SDK Implementation {#java-sdk-implementation} :::info Language: Java This section covers the **Java SDK** implementation. Switch tabs above to see Go or JavaScript examples. @@ -557,7 +557,7 @@ This section covers the **Java SDK** implementation. Switch tabs above to see Go - Maven or Gradle - Your OpenTDF platform running locally (from Getting Started guide) -### Step 1: Create a New Maven Project +### Step 1: Create a New Maven Project {#step-1-java} Create a new directory with a `pom.xml` file: @@ -583,19 +583,38 @@ Create `pom.xml`: 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.6.2 + 0.12.0 ``` -### Step 2: Install Dependencies +### Step 2: Install Dependencies {#step-2-java} ```bash mvn clean install @@ -603,7 +622,7 @@ 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: Create Your Application {#step-3-java} Create the directory structure: @@ -619,6 +638,7 @@ Create `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; @@ -638,11 +658,18 @@ public class QuickstartApp { System.out.println("πŸ“‘ Connecting to platform: " + platformEndpoint); + // Create SSL factory that trusts all certificates (DEV ONLY - DO NOT USE IN PRODUCTION) + System.out.println("πŸ”“ Creating SSL factory for self-signed certificates..."); + SSLFactory sslFactory = SSLFactory.builder() + .withTrustingAllCertificatesWithoutValidation() + .build(); + // Create SDK client with client credentials System.out.println("πŸ” Initializing SDK client with client credentials..."); SDKBuilder builder = SDKBuilder.newBuilder(); SDK sdk = builder.platformEndpoint(platformEndpoint) .clientSecret(clientId, clientSecret) + .sslFactory(sslFactory) .build(); System.out.println("βœ… SDK client initialized successfully"); @@ -700,7 +727,7 @@ public class QuickstartApp { } ``` -### Step 4: Run Your Application +### Step 4: Run Your Application {#step-4-java} ```bash mvn compile exec:java -Dexec.mainClass="io.opentdf.quickstart.QuickstartApp" @@ -733,7 +760,7 @@ Hello from the OpenTDF Java SDK! This data is encrypted. --- -### Next Steps with Java SDK +### Next Steps with Java SDK {#next-steps-with-java-sdk} :::tip Java-Specific Examples The following examples are specific to the **Java SDK**. @@ -944,6 +971,7 @@ Here's a complete example that brings all the pieces together: 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.SubjectMappingOperatorEnum; @@ -968,17 +996,22 @@ public class CompleteExample { public static void main(String[] args) throws Exception { String clientId = "opentdf"; String clientSecret = "secret"; - String platformEndpoint = "localhost:8443"; + String platformEndpoint = "https://platform.opentdf.local:8443"; + + // Create SSL factory that trusts all certificates (DEV ONLY - DO NOT USE IN PRODUCTION) + SSLFactory sslFactory = SSLFactory.builder() + .withTrustingAllCertificatesWithoutValidation() + .build(); // Create SDK client SDK sdk = new SDKBuilder() .platformEndpoint(platformEndpoint) .clientSecret(clientId, clientSecret) - .useInsecurePlaintextConnection(true) + .sslFactory(sslFactory) .build(); var kasInfo = new Config.KASInfo(); - kasInfo.URL = "http://localhost:8080/kas"; + kasInfo.URL = platformEndpoint + "/kas"; // 1. Create namespace (or use existing) CreateNamespaceResponse nsResponse; @@ -1141,7 +1174,7 @@ public class CompleteExample { -## JavaScript/TypeScript SDK Implementation +## JavaScript/TypeScript SDK Implementation {#javascript-sdk-implementation} :::info Language: JavaScript/TypeScript This section covers the **JavaScript/TypeScript SDK** implementation. Switch tabs above to see Go or Java examples. @@ -1153,7 +1186,7 @@ This section covers the **JavaScript/TypeScript SDK** implementation. Switch tab - npm or yarn - Your OpenTDF platform running locally (from Getting Started guide) -### Step 1: Create a New Project +### Step 1: Create a New Project {#step-1-javascript} Create a new directory and initialize a Node.js project: @@ -1163,7 +1196,7 @@ cd opentdf-quickstart npm init -y ``` -### Step 2: Install the SDK +### Step 2: Install the SDK {#step-2-javascript} ```bash npm install @opentdf/client @@ -1174,7 +1207,7 @@ Expected output: > added 45 packages, and audited 46 packages in 3s > ``` -### Step 3: Create Your Application +### Step 3: Create Your Application {#step-3-javascript} #### TypeScript Implementation Code @@ -1330,7 +1363,7 @@ async function main() { main(); ``` -### Step 4: Run Your Application +### Step 4: Run Your Application {#step-4-javascript} For TypeScript: ```bash @@ -1371,7 +1404,7 @@ Hello from the OpenTDF JavaScript SDK! This data is encrypted. --- -### Next Steps with JavaScript SDK +### Next Steps with JavaScript SDK {#next-steps-with-javascript-sdk} :::tip JavaScript-Specific Examples The following examples are specific to the **JavaScript/TypeScript SDK**. @@ -1652,7 +1685,7 @@ main(); -## Key Concepts +## Key Concepts {#key-concepts} ### Client Credentials Authentication @@ -1688,7 +1721,7 @@ In production: - Configure proper certificate validation ::: -## Troubleshooting +## Troubleshooting {#troubleshooting} ### Attribute Not Found @@ -1840,7 +1873,7 @@ mvn clean install -U npm install @opentdf/client ``` -## What's Next? +## What's Next? {#whats-next} Now that you have basic SDK integration working, explore: diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 361848c..5cad766 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -383,6 +383,9 @@ ${updatedContent}`, ], require.resolve("docusaurus-lunr-search"), ], + clientModules: [ + require.resolve('./src/theme/TabsHashSync.js'), + ], }; export default config; diff --git a/src/theme/TabsHashSync.js b/src/theme/TabsHashSync.js new file mode 100644 index 0000000..4f5bcc1 --- /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 {}; From ac76bd206a1d1d75fc7bd03ac44e4ae508fc0aeb Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Tue, 3 Feb 2026 10:36:51 -0800 Subject: [PATCH 05/18] working java example Signed-off-by: Mary Dickson --- docs/sdks/quickstart.mdx | 482 ++++++++++++++++++++++++++------------- 1 file changed, 327 insertions(+), 155 deletions(-) diff --git a/docs/sdks/quickstart.mdx b/docs/sdks/quickstart.mdx index 781c926..a4b1f54 100644 --- a/docs/sdks/quickstart.mdx +++ b/docs/sdks/quickstart.mdx @@ -201,63 +201,58 @@ For additional policy management examples including managing attributes, namespa ::: -**Create an Attribute** +**Create a New Attribute Value** -First, let's create an attribute that we can use for access control. We'll create a department attribute with a marketing 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: ```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" ) -// Create the namespace (or use existing) -nsResp, err := client.Namespaces.CreateNamespace(context.Background(), - &namespaces.CreateNamespaceRequest{ - Name: "opentdf.io", +// Get the existing department attribute +attrFqn := "https://opentdf.io/attr/department" +getAttrResp, err := client.Attributes.GetAttribute(context.Background(), + &attributes.GetAttributeRequest{ + Fqn: attrFqn, }) 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) + log.Fatalf("Failed to get attribute: %v", err) +} + +log.Printf("βœ… Found existing attribute: %s", getAttrResp.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 } -} else { - log.Printf("Created namespace: %s", nsResp.GetNamespace().GetId()) } -// Create the attribute with the marketing value -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 { - log.Fatalf("Failed to create attribute: %v", err) +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("Created attribute: %s", attrResp.GetAttribute().GetName()) -log.Printf("Full attribute FQN: https://opentdf.io/attr/department/value/marketing") +log.Printf("Full attribute FQN: https://opentdf.io/attr/department/value/%s", targetValue) ``` -:::note -You may encounter errors if attempting to create a resource that already exists. The [complete example below](#complete-example-create-attribute-encrypt-grant-access-and-decrypt) includes full error handling for idempotency. +:::warning +If you get a [resource not found error](#resource-not-found), you may need to create the "department" attribute, along with the namespace. ::: **Add Attributes for Access Control** @@ -278,7 +273,7 @@ _, err = client.CreateTDF( ``` :::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. Try it now! +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](#permission-denied--insufficient-entitlements). Try it now! ::: **Grant Yourself Access to the Attribute** @@ -290,8 +285,14 @@ import ( "github.com/opentdf/platform/protocol/go/policy/subjectmapping" ) -// Get the attribute value ID from the attribute you created -attributeValueId := attrResp.GetAttribute().GetValues()[0].GetId() +// 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(), @@ -333,7 +334,7 @@ if err != nil { log.Printf("βœ… Granted yourself access to department/marketing") ``` -Now you can decrypt the TDF you encrypted with the `marketing` attribute. +Now you can decrypt the TDF you encrypted with the `marketing` attribute. πŸŽ‰ **Save TDF to a File** @@ -489,6 +490,7 @@ func main() { { ConditionGroups: []*policy.ConditionGroup{ { + BooleanOperator: policy.ConditionBooleanTypeEnum_CONDITION_BOOLEAN_TYPE_ENUM_AND, Conditions: []*policy.Condition{ { SubjectExternalSelectorValue: ".clientId", @@ -775,86 +777,196 @@ For additional policy management examples including managing attributes, namespa ::: -**Create an Attribute** +**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()); -First, let's create an attribute that we can use for access control. We'll create a department attribute with a marketing value: + 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](#attribute-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.Namespace; 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.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; -// Create the namespace (or use existing) +// Get or create the namespace CreateNamespaceResponse nsResponse; try { - nsResponse = ResponseMessageKt.getOrThrow( + // Try to get existing namespace + GetNamespaceResponse getNamespaceResponse = ResponseMessageKt.getOrThrow( sdk.getServices() .namespaces() - .createNamespaceBlocking( - CreateNamespaceRequest.newBuilder() - .setName("opentdf.io") + .getNamespaceBlocking( + GetNamespaceRequest.newBuilder() + .setFqn("https://opentdf.io") .build(), Collections.emptyMap()) .execute()); - System.out.println("Created namespace: " + nsResponse.getNamespace().getId()); + + 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("already_exists")) { - // Namespace already exists, fetch it - var listResponse = ResponseMessageKt.getOrThrow( + if (e.getMessage() != null && e.getMessage().contains("not_found")) { + // Namespace doesn't exist, create it + nsResponse = ResponseMessageKt.getOrThrow( sdk.getServices() .namespaces() - .listNamespacesBlocking( - ListNamespacesRequest.newBuilder().build(), + .createNamespaceBlocking( + CreateNamespaceRequest.newBuilder() + .setName("opentdf.io") + .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()); + System.out.println("βœ… Created namespace: " + nsResponse.getNamespace().getId()); } else { throw e; } } -// Create the attribute with the marketing value -var attrRequest = CreateAttributeRequest.newBuilder() - .setNamespaceId(nsResponse.getNamespace().getId()) - .setName("department") - .setRule(AttributeRuleTypeEnum.ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF) - .addAllValues(Arrays.asList("marketing")) - .build(); +// 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()); -CreateAttributeResponse attrResponse = ResponseMessageKt.getOrThrow( - sdk.getServices() - .attributes() - .createAttributeBlocking(attrRequest, 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; + } +} -System.out.println("Created attribute: " + attrResponse.getAttribute().getName()); -System.out.println("Full attribute FQN: https://opentdf.io/attr/department/value/marketing"); +String targetValue = "marketing"; +System.out.println("Full attribute FQN: https://opentdf.io/attr/department/value/" + targetValue); ``` -:::note -You may encounter errors if attempting to create a resource that already exists. The [complete example below](#complete-example-create-attribute-encrypt-grant-access-and-decrypt) includes full error handling for idempotency. -::: +
**Add Attributes for Access Control** -Now that you've created the attribute, update your `createTDF` call to include the attribute 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( @@ -864,7 +976,7 @@ var tdfConfig = Config.newTDFConfig( ``` :::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. Try it now! +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](#permission-denied--insufficient-entitlements). Try it now! ::: **Grant Yourself Access to the Attribute** @@ -873,7 +985,11 @@ To decrypt the TDF you just created, you need to grant yourself the `marketing` ```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 created @@ -887,6 +1003,7 @@ var condition = Condition.newBuilder() .build(); var conditionGroup = ConditionGroup.newBuilder() + .setBooleanOperator(ConditionBooleanTypeEnum.CONDITION_BOOLEAN_TYPE_ENUM_AND) .addConditions(condition) .build(); @@ -896,35 +1013,34 @@ var subjectSet = SubjectSet.newBuilder() var scsRequest = CreateSubjectConditionSetRequest.newBuilder() .setSubjectConditionSet( - SubjectConditionSet.newBuilder() + SubjectConditionSetCreate.newBuilder() .addSubjectSets(subjectSet) .build()) .build(); var scsResponse = ResponseMessageKt.getOrThrow( sdk.getServices() - .subjectMapping() + .subjectMappings() .createSubjectConditionSetBlocking(scsRequest, Collections.emptyMap()) .execute()); // Create the subject mapping to grant yourself the entitlement var smRequest = CreateSubjectMappingRequest.newBuilder() .setAttributeValueId(attributeValueId) - .addActions(Action.newBuilder() - .setStandard(Action.StandardAction.STANDARD_ACTION_DECRYPT)) - .setSubjectConditionSetId(scsResponse.getSubjectConditionSet().getId()) + .addActions(Action.newBuilder().setName("read").build()) + .setExistingSubjectConditionSetId(scsResponse.getSubjectConditionSet().getId()) .build(); ResponseMessageKt.getOrThrow( sdk.getServices() - .subjectMapping() + .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. +Now you can decrypt the TDF you encrypted with the `marketing` attribute. πŸŽ‰ **Save TDF to a File** @@ -974,7 +1090,11 @@ 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; @@ -1027,7 +1147,7 @@ public class CompleteExample { .execute()); System.out.println("βœ… Created namespace: " + nsResponse.getNamespace().getId()); } catch (Exception e) { - if (e.getMessage() != null && e.getMessage().contains("already_exists")) { + 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() @@ -1069,7 +1189,7 @@ public class CompleteExample { .execute()); System.out.println("βœ… Created attribute: " + attrResponse.getAttribute().getName()); } catch (Exception e) { - if (e.getMessage() != null && e.getMessage().contains("already_exists")) { + 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() @@ -1117,39 +1237,43 @@ public class CompleteExample { .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( - SubjectConditionSet.newBuilder() - .addSubjectSets(SubjectSet.newBuilder() - .addConditionGroups(ConditionGroup.newBuilder() - .addConditions(condition) - .build()) - .build()) + SubjectConditionSetCreate.newBuilder() + .addSubjectSets(subjectSet) .build()) .build(); var scsResponse = ResponseMessageKt.getOrThrow( sdk.getServices() - .subjectMapping() + .subjectMappings() .createSubjectConditionSetBlocking(scsRequest, Collections.emptyMap()) .execute()); var smRequest = CreateSubjectMappingRequest.newBuilder() .setAttributeValueId(attributeValueId) - .addActions(Action.newBuilder() - .setStandard(Action.StandardAction.STANDARD_ACTION_DECRYPT)) - .setSubjectConditionSetId(scsResponse.getSubjectConditionSet().getId()) + .addActions(Action.newBuilder().setName("read").build()) + .setExistingSubjectConditionSetId(scsResponse.getSubjectConditionSet().getId()) .build(); try { ResponseMessageKt.getOrThrow( sdk.getServices() - .subjectMapping() + .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")) { + 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; @@ -1419,46 +1543,41 @@ For additional policy management examples including managing attributes, namespa ::: -**Create an Attribute** +**Create a New Attribute Value** -First, let's create an attribute that we can use for access control. We'll create a department attribute with a marketing 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: ```typescript -// Create the namespace (or use existing) -let nsResponse; -try { - nsResponse = await client.namespaces.createNamespace({ - name: 'opentdf.io', +// Get the existing department attribute +const attrFqn = 'https://opentdf.io/attr/department'; +const getAttrResponse = await client.attributes.getAttribute({ + fqn: attrFqn, +}); + +console.log(`βœ… Found existing attribute: ${getAttrResponse.attribute.name}`); + +// Check if "marketing" value already exists +const targetValue = 'marketing'; +const valueExists = getAttrResponse.attribute.values.some( + (value: any) => value.value === targetValue +); + +if (!valueExists) { + // Add the "marketing" value to the existing attribute + await client.attributes.createAttributeValue({ + attributeId: getAttrResponse.attribute.id, + value: targetValue, }); - console.log(`Created namespace: ${nsResponse.namespace.id}`); -} catch (error: any) { - if (error.message && error.message.includes('already_exists')) { - // Namespace already exists, fetch it - const listResponse = await client.namespaces.listNamespaces(); - const existingNs = listResponse.namespaces.find( - (ns: any) => ns.name === 'opentdf.io' - ); - nsResponse = { namespace: existingNs }; - console.log(`Using existing namespace: ${existingNs.id}`); - } else { - throw error; - } + console.log(`βœ… Added '${targetValue}' value to department attribute`); +} else { + console.log(`βœ… Attribute 'department' already has '${targetValue}' value`); } -// Create the attribute with the marketing value -const attrResponse = await client.attributes.createAttribute({ - namespaceId: nsResponse.namespace.id, - name: 'department', - rule: 'ANY_OF', - values: ['marketing'], -}); - -console.log(`Created attribute: ${attrResponse.attribute.name}`); -console.log('Full attribute FQN: https://opentdf.io/attr/department/value/marketing'); +console.log(`Full attribute FQN: https://opentdf.io/attr/department/value/${targetValue}`); ``` -:::note -You may encounter errors if attempting to create a resource that already exists. The [complete example below](#complete-example-create-attribute-encrypt-grant-access-and-decrypt) includes full error handling for idempotency. +:::warning +If you get a [resource not found error](#resource-not-found), you may need to create the "department" attribute, along with the namespace. ::: **Add Attributes for Access Control** @@ -1474,7 +1593,7 @@ const encryptedStream = await client.encrypt({ ``` :::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. Try it now! +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](#permission-denied--insufficient-entitlements). Try it now! ::: **Grant Yourself Access to the Attribute** @@ -1482,14 +1601,18 @@ Only users with the `department/marketing` entitlement will be able to decrypt t 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. ```typescript -// Get the attribute value ID from the attribute you created -const attributeValueId = attrResponse.attribute.values[0].id; +// Get the attribute value ID for "marketing" +const attributeValue = getAttrResponse.attribute.values.find( + (value: any) => value.value === targetValue +); +const attributeValueId = attributeValue.id; // Create subject condition set that matches your identity const scsResponse = await client.subjectMapping.createSubjectConditionSet({ subjectConditionSet: { subjectSets: [{ conditionGroups: [{ + booleanOperator: 'CONDITION_BOOLEAN_TYPE_ENUM_AND', conditions: [{ subjectExternalSelectorValue: '.clientId', operator: 'SUBJECT_MAPPING_OPERATOR_ENUM_IN', @@ -1503,14 +1626,14 @@ const scsResponse = await client.subjectMapping.createSubjectConditionSet({ // Create the subject mapping to grant yourself the entitlement await client.subjectMapping.createSubjectMapping({ attributeValueId: attributeValueId, - actions: [{ standard: 'STANDARD_ACTION_DECRYPT' }], - subjectConditionSetId: scsResponse.subjectConditionSet.id, + actions: [{ name: 'read' }], + existingSubjectConditionSetId: scsResponse.subjectConditionSet.id, }); console.log('βœ… Granted yourself access to department/marketing'); ``` -Now you can decrypt the TDF you encrypted with the `marketing` attribute. +Now you can decrypt the TDF you encrypted with the `marketing` attribute. πŸŽ‰ **Save TDF to a File** @@ -1638,6 +1761,7 @@ async function main() { subjectConditionSet: { subjectSets: [{ conditionGroups: [{ + booleanOperator: 'CONDITION_BOOLEAN_TYPE_ENUM_AND', conditions: [{ subjectExternalSelectorValue: '.clientId', operator: 'SUBJECT_MAPPING_OPERATOR_ENUM_IN', @@ -1651,8 +1775,8 @@ async function main() { try { await client.subjectMapping.createSubjectMapping({ attributeValueId: attributeValueId, - actions: [{ standard: 'STANDARD_ACTION_DECRYPT' }], - subjectConditionSetId: scsResponse.subjectConditionSet.id, + actions: [{ name: 'read' }], + existingSubjectConditionSetId: scsResponse.subjectConditionSet.id, }); console.log('βœ… Granted yourself access to department/marketing'); } catch (error: any) { @@ -1723,13 +1847,13 @@ In production: ## Troubleshooting {#troubleshooting} -### Attribute Not Found +### Resource Not Found **Error**: `not_found: resource not found` or `attribute not found` -**Cause**: You're trying to encrypt with an attribute that doesn't exist on the platform. +**Cause**: You're trying to use a resource (namespace, attribute, or attribute value) that doesn't exist on the platform. -**Solution**: Create the attribute first using otdfctl or the [SDK Create Attribute function](/sdks/policy#create-attribute). +**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:** @@ -1744,7 +1868,10 @@ otdfctl policy attributes values create -a $DEPT_ATTRIBUTE_ID --value finance ### Permission Denied / Insufficient Entitlements -**Error**: When attempting to decrypt a TDF, you may see a verbose error like: +**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 @@ -1757,7 +1884,36 @@ rpc error: code = PermissionDenied desc = forbidden] - `rewrap request 403` - `permission_denied: request error` -These indicate an authorization failure, not a cryptographic or network issue. + + + +``` +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. @@ -1765,6 +1921,22 @@ For example, if you encrypted data with `https://opentdf.io/attr/department/valu **Solution**: Grant yourself (or the entity) entitlements by [creating a subject mapping](/sdks/policy#create-subject-mapping). +:::warning Common Mistake: Using "decrypt" Instead of "read" +When creating subject mappings for TDF decryption, the action must be `"read"`, not `"decrypt"`. While "decrypt" seems intuitive, the OpenTDF platform uses `"read"` as the action name for TDF decryption entitlements. + +**Incorrect:** +```java +.addActions(Action.newBuilder().setName("decrypt").build()) +``` + +**Correct:** +```java +.addActions(Action.newBuilder().setName("read").build()) +``` + +Using the wrong action name will result in the "error unwrapping key" error shown above. +::: + **Quick fix using the CLI:** ```bash From c3813fd75e750ffc33406669efdea86f59a03caf Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Tue, 3 Feb 2026 14:22:49 -0800 Subject: [PATCH 06/18] refactor the SDK guide to 3 pages and move troubleshooting to root Signed-off-by: Mary Dickson --- docs/sdks/quickstart.mdx | 2067 -------------------------- docs/sdks/quickstart/_category_.json | 10 + docs/sdks/quickstart/go.mdx | 537 +++++++ docs/sdks/quickstart/index.mdx | 146 ++ docs/sdks/quickstart/java.mdx | 790 ++++++++++ docs/sdks/quickstart/javascript.mdx | 535 +++++++ docs/sdks/troubleshooting.mdx | 231 +++ sidebars.js | 13 +- 8 files changed, 2251 insertions(+), 2078 deletions(-) delete mode 100644 docs/sdks/quickstart.mdx create mode 100644 docs/sdks/quickstart/_category_.json create mode 100644 docs/sdks/quickstart/go.mdx create mode 100644 docs/sdks/quickstart/index.mdx create mode 100644 docs/sdks/quickstart/java.mdx create mode 100644 docs/sdks/quickstart/javascript.mdx create mode 100644 docs/sdks/troubleshooting.mdx diff --git a/docs/sdks/quickstart.mdx b/docs/sdks/quickstart.mdx deleted file mode 100644 index a4b1f54..0000000 --- a/docs/sdks/quickstart.mdx +++ /dev/null @@ -1,2067 +0,0 @@ ---- -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. **Use the tabs below to select your language** - all examples are complete and ready to run. - -:::note -Each tab contains a complete, working example for that specific SDK. The code and commands are different for each language. -::: - - - - -## Go SDK Implementation {#go-sdk-implementation} - -:::info Language: Go -This section covers the **Go SDK** implementation. Switch tabs above to see Java or JavaScript examples. -::: - -### Prerequisites - -- Go 1.21 or later -- Your OpenTDF platform running locally (from Getting Started guide) - -### 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), - sdk.WithInsecurePlaintextConn(), // Only for local development! - ) - - 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, - 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! -``` - -
- ---- - -### Next Steps with Go SDK {#next-steps-with-go-sdk} - -:::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** - -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: - -```go -import ( - "context" - "github.com/opentdf/platform/protocol/go/policy/attributes" -) - -// Get the existing department attribute -attrFqn := "https://opentdf.io/attr/department" -getAttrResp, err := client.Attributes.GetAttribute(context.Background(), - &attributes.GetAttributeRequest{ - Fqn: attrFqn, - }) -if err != nil { - log.Fatalf("Failed to get attribute: %v", err) -} - -log.Printf("βœ… Found existing attribute: %s", getAttrResp.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) -``` - -:::warning -If you get a [resource not found error](#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"), - 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](#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. - -```go -import ( - "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** - -```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() - -_, err = client.CreateTDF(outputFile, inputFile, - sdk.WithKasInformation(sdk.KASInfo{URL: platformEndpoint}), -) -``` - -
-Complete Example: Create Attribute, Encrypt, Grant Access, and Decrypt - -Here's a complete example that brings all the pieces together: - -```go -package main - -import ( - "bytes" - "context" - "log" - "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), - sdk.WithInsecurePlaintextConn(), - ) - 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 - - _, 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. Grant yourself access to the marketing attribute - attributeValueId := attrResp.GetAttribute().GetValues()[0].GetId() - - 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") - } - - // 5. Decrypt the data - tdfReader, err := client.LoadTDF(bytes.NewReader(encryptedBuffer.Bytes())) - 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()) -} -``` - -
- -
- - -## Java SDK Implementation {#java-sdk-implementation} - -:::info Language: Java -This section covers the **Java SDK** implementation. Switch tabs above to see Go or JavaScript examples. -::: - -### Prerequisites - -- Java 17 or later -- Maven or Gradle -- Your OpenTDF platform running locally (from Getting Started guide) - -### 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 - - - -``` - -### 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 (DEV ONLY - DO NOT USE IN PRODUCTION) - System.out.println("πŸ”“ Creating SSL factory for self-signed certificates..."); - SSLFactory sslFactory = SSLFactory.builder() - .withTrustingAllCertificatesWithoutValidation() - .build(); - - // Create SDK client with client credentials - System.out.println("πŸ” Initializing SDK client with client credentials..."); - SDKBuilder builder = SDKBuilder.newBuilder(); - 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."; - - 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!"); - - } catch (Exception e) { - System.err.println("❌ Error occurred: " + e.getMessage()); - e.printStackTrace(); - System.exit(1); - } - } -} -``` - -### 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! -``` - -
- ---- - -### Next Steps with Java SDK {#next-steps-with-java-sdk} - -:::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](#attribute-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](#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. - -```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 created -String attributeValueId = attrResponse.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** - -```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 reader = sdk.loadTDF(fileChannel, Config.newTDFReaderConfig()); - reader.readPayload(System.out); -} -``` - -**Handle Large Files** - -```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); -} -``` - -
-Complete Example: Create Attribute, Encrypt, Grant Access, and Decrypt - -Here's a complete example that brings all the pieces together: - -```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.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Collections; - -public class CompleteExample { - 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 (DEV ONLY - DO NOT USE IN PRODUCTION) - SSLFactory sslFactory = SSLFactory.builder() - .withTrustingAllCertificatesWithoutValidation() - .build(); - - // Create SDK client - SDK sdk = new SDKBuilder() - .platformEndpoint(platformEndpoint) - .clientSecret(clientId, clientSecret) - .sslFactory(sslFactory) - .build(); - - 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. Grant yourself access to the marketing attribute - String attributeValueId = attrResponse.getAttribute().getValues(0).getId(); - - 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; - } - } - - // 5. Decrypt the data - var encryptedBytes = outputStream.toByteArray(); - var decryptedStream = new ByteArrayOutputStream(); - - var reader = sdk.loadTDF(new ByteArrayInputStream(encryptedBytes)); - reader.readPayload(decryptedStream); - - System.out.println("βœ… Data successfully decrypted"); - System.out.println("πŸ“€ Decrypted content: " + decryptedStream.toString(StandardCharsets.UTF_8)); - } -} -``` - -
- -
- - -## JavaScript/TypeScript SDK Implementation {#javascript-sdk-implementation} - -:::info Language: JavaScript/TypeScript -This section covers the **JavaScript/TypeScript SDK** implementation. Switch tabs above to see Go or Java examples. -::: - -### Prerequisites - -- Node.js 18 or later -- npm or yarn -- Your OpenTDF platform running locally (from Getting Started guide) - -### Step 1: Create a New Project {#step-1-javascript} - -Create a new directory and initialize a Node.js project: - -```bash -mkdir opentdf-quickstart -cd opentdf-quickstart -npm init -y -``` - -### Step 2: Install the SDK {#step-2-javascript} - -```bash -npm install @opentdf/client -``` - -Expected output: -> ```console -> added 45 packages, and audited 46 packages in 3s -> ``` - -### Step 3: Create Your Application {#step-3-javascript} - -#### TypeScript Implementation Code - -For **TypeScript** projects, create `quickstart.ts`: - -```typescript title="quickstart.ts" -import { AuthProviders, OpenTDF } from '@opentdf/client'; - -async function main() { - try { - console.log("πŸš€ Starting OpenTDF SDK Quickstart..."); - - const platformEndpoint = "https://platform.opentdf.local:8443"; - const oidcOrigin = "https://keycloak.opentdf.local:9443/realms/opentdf"; - const clientId = "opentdf"; - const clientSecret = "secret"; - - console.log("πŸ“‘ Connecting to platform: " + platformEndpoint); - - // Create authentication provider with client credentials - console.log("πŸ” Setting up client credentials authentication..."); - const authProvider = await AuthProviders.clientSecretAuthProvider({ - clientId, - clientSecret, - oidcOrigin, - exchange: 'client', - }); - console.log("βœ… Authentication provider created"); - - // Create OpenTDF client - console.log("πŸ”§ Initializing SDK client..."); - const client = new OpenTDF({ - authProvider: authProvider, - kasEndpoint: platformEndpoint, - }); - console.log("βœ… SDK client initialized successfully"); - - // Encrypt data - console.log("\nπŸ“ Encrypting sensitive data..."); - const sensitiveData = "Hello from the OpenTDF JavaScript SDK! This data is encrypted."; - const dataBuffer = new TextEncoder().encode(sensitiveData); - - console.log("πŸ”’ Creating TDF..."); - const encryptedStream = await client.encrypt({ - offline: false, - source: dataBuffer, - }); - - // Convert stream to buffer - const encryptedBuffer = await new Response(encryptedStream).arrayBuffer(); - const encryptedBytes = new Uint8Array(encryptedBuffer); - - console.log("βœ… Data successfully encrypted"); - console.log("πŸ“Š Encrypted TDF size: " + encryptedBytes.length + " bytes"); - - // Decrypt data - console.log("\nπŸ”“ Decrypting TDF..."); - const decryptedStream = await client.decrypt({ - source: encryptedBytes, - }); - - const decryptedText = await new Response(decryptedStream).text(); - - console.log("βœ… Data successfully decrypted"); - console.log("πŸ“€ Decrypted content:\n\n" + decryptedText + "\n"); - - console.log("πŸŽ‰ Quickstart complete!"); - process.exit(0); - - } catch (error) { - console.error("❌ Error occurred:", error); - process.exit(1); - } -} - -main(); -``` - -#### JavaScript Implementation Code - -For **JavaScript** projects, create `quickstart.mjs`: - -```javascript title="quickstart.mjs" -import { AuthProviders, OpenTDF } from '@opentdf/client'; - -async function main() { - try { - console.log("πŸš€ Starting OpenTDF SDK Quickstart..."); - - const platformEndpoint = "https://platform.opentdf.local:8443"; - const oidcOrigin = "https://keycloak.opentdf.local:9443/realms/opentdf"; - const clientId = "opentdf"; - const clientSecret = "secret"; - - console.log("πŸ“‘ Connecting to platform: " + platformEndpoint); - - // Create authentication provider with client credentials - console.log("πŸ” Setting up client credentials authentication..."); - const authProvider = await AuthProviders.clientSecretAuthProvider({ - clientId, - clientSecret, - oidcOrigin, - exchange: 'client', - }); - console.log("βœ… Authentication provider created"); - - // Create OpenTDF client - console.log("πŸ”§ Initializing SDK client..."); - const client = new OpenTDF({ - authProvider: authProvider, - kasEndpoint: platformEndpoint, - }); - console.log("βœ… SDK client initialized successfully"); - - // Encrypt data - console.log("\nπŸ“ Encrypting sensitive data..."); - const sensitiveData = "Hello from the OpenTDF JavaScript SDK! This data is encrypted."; - const dataBuffer = new TextEncoder().encode(sensitiveData); - - console.log("πŸ”’ Creating TDF..."); - const encryptedStream = await client.encrypt({ - offline: false, - source: dataBuffer, - }); - - // Convert stream to buffer - const encryptedBuffer = await new Response(encryptedStream).arrayBuffer(); - const encryptedBytes = new Uint8Array(encryptedBuffer); - - console.log("βœ… Data successfully encrypted"); - console.log("πŸ“Š Encrypted TDF size: " + encryptedBytes.length + " bytes"); - - // Decrypt data - console.log("\nπŸ”“ Decrypting TDF..."); - const decryptedStream = await client.decrypt({ - source: encryptedBytes, - }); - - const decryptedText = await new Response(decryptedStream).text(); - - console.log("βœ… Data successfully decrypted"); - console.log("πŸ“€ Decrypted content:\n\n" + decryptedText + "\n"); - - console.log("πŸŽ‰ Quickstart complete!"); - process.exit(0); - - } catch (error) { - console.error("❌ Error occurred:", error); - process.exit(1); - } -} - -main(); -``` - -### Step 4: Run Your Application {#step-4-javascript} - -For TypeScript: -```bash -npx tsx quickstart.ts -``` - -For JavaScript: -```bash -node quickstart.mjs -``` - -
-Expected output - -```console -πŸš€ Starting OpenTDF SDK Quickstart... -πŸ“‘ Connecting to platform: https://platform.opentdf.local:8443 -πŸ” Setting up client credentials authentication... -βœ… Authentication provider created -πŸ”§ Initializing SDK client... -βœ… 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 JavaScript SDK! This data is encrypted. - -πŸŽ‰ Quickstart complete! -``` - -
- ---- - -### Next Steps with JavaScript SDK {#next-steps-with-javascript-sdk} - -:::tip JavaScript-Specific Examples -The following examples are specific to the **JavaScript/TypeScript 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: - -```typescript -// Get the existing department attribute -const attrFqn = 'https://opentdf.io/attr/department'; -const getAttrResponse = await client.attributes.getAttribute({ - fqn: attrFqn, -}); - -console.log(`βœ… Found existing attribute: ${getAttrResponse.attribute.name}`); - -// Check if "marketing" value already exists -const targetValue = 'marketing'; -const valueExists = getAttrResponse.attribute.values.some( - (value: any) => value.value === targetValue -); - -if (!valueExists) { - // Add the "marketing" value to the existing attribute - await client.attributes.createAttributeValue({ - attributeId: getAttrResponse.attribute.id, - value: targetValue, - }); - console.log(`βœ… Added '${targetValue}' value to department attribute`); -} else { - console.log(`βœ… Attribute 'department' already has '${targetValue}' value`); -} - -console.log(`Full attribute FQN: https://opentdf.io/attr/department/value/${targetValue}`); -``` - -:::warning -If you get a [resource not found error](#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 `encrypt` call to include the attribute for access control: - -```typescript -const encryptedStream = await client.encrypt({ - offline: false, - source: dataBuffer, - attributes: ["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](#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. - -```typescript -// Get the attribute value ID for "marketing" -const attributeValue = getAttrResponse.attribute.values.find( - (value: any) => value.value === targetValue -); -const attributeValueId = attributeValue.id; - -// Create subject condition set that matches your identity -const scsResponse = await client.subjectMapping.createSubjectConditionSet({ - subjectConditionSet: { - subjectSets: [{ - conditionGroups: [{ - booleanOperator: 'CONDITION_BOOLEAN_TYPE_ENUM_AND', - conditions: [{ - subjectExternalSelectorValue: '.clientId', - operator: 'SUBJECT_MAPPING_OPERATOR_ENUM_IN', - subjectExternalValues: ['opentdf'], - }], - }], - }], - }, -}); - -// Create the subject mapping to grant yourself the entitlement -await client.subjectMapping.createSubjectMapping({ - attributeValueId: attributeValueId, - actions: [{ name: 'read' }], - existingSubjectConditionSetId: scsResponse.subjectConditionSet.id, -}); - -console.log('βœ… Granted yourself access to department/marketing'); -``` - -Now you can decrypt the TDF you encrypted with the `marketing` attribute. πŸŽ‰ - -**Save TDF to a File** - -```typescript -import * as fs from 'fs'; - -// After encryption -fs.writeFileSync('encrypted.tdf', encryptedBytes); - -// Later, decrypt from file -const fileData = fs.readFileSync('encrypted.tdf'); -const decryptedStream = await client.decrypt({ - source: new Uint8Array(fileData), -}); -``` - -**Handle Large Files with Streaming** - -```typescript -import * as fs from 'fs'; -import { Readable } from 'stream'; - -// Encrypt a large file -const fileStream = fs.createReadStream('large-file.pdf'); -const encryptedStream = await client.encrypt({ - offline: false, - source: fileStream, -}); - -// Save encrypted stream to file -const writeStream = fs.createWriteStream('large-file.pdf.tdf'); -Readable.fromWeb(encryptedStream).pipe(writeStream); -``` - -
-Complete Example: Create Attribute, Encrypt, Grant Access, and Decrypt - -Here's a complete example that brings all the pieces together: - -```typescript -import { AuthProviders, OpenTDF } from '@opentdf/client'; - -async function main() { - try { - const platformEndpoint = 'http://localhost:8080'; - const oidcEndpoint = 'http://localhost:8888/realms/opentdf'; - - // Create authentication provider - const authProvider = await AuthProviders.clientSecretAuthProvider({ - clientId: 'opentdf', - clientSecret: 'secret', - oidcOrigin: oidcEndpoint, - exchange: 'client', - }); - - // Create SDK client - const client = new OpenTDF({ - authProvider: authProvider, - platformUrl: platformEndpoint, - }); - - // 1. Create namespace (or use existing) - let nsResponse; - try { - nsResponse = await client.namespaces.createNamespace({ - name: 'opentdf.io', - }); - console.log(`βœ… Created namespace: ${nsResponse.namespace.id}`); - } catch (error: any) { - if (error.message && error.message.includes('already_exists')) { - // Namespace already exists, fetch it - const listResponse = await client.namespaces.listNamespaces(); - const existingNs = listResponse.namespaces.find( - (ns: any) => ns.name === 'opentdf.io' - ); - nsResponse = { namespace: existingNs }; - console.log(`βœ… Using existing namespace: ${existingNs.id}`); - } else { - throw error; - } - } - - // 2. Create attribute with marketing value (or use existing) - let attrResponse; - try { - attrResponse = await client.attributes.createAttribute({ - namespaceId: nsResponse.namespace.id, - name: 'department', - rule: 'ANY_OF', - values: ['marketing'], - }); - console.log(`βœ… Created attribute: ${attrResponse.attribute.name}`); - } catch (error: any) { - if (error.message && error.message.includes('already_exists')) { - // Attribute already exists, fetch it - const listResponse = await client.attributes.listAttributes(); - const existingAttr = listResponse.attributes.find( - (attr: any) => attr.name === 'department' && attr.namespace.id === nsResponse.namespace.id - ); - attrResponse = { attribute: existingAttr }; - console.log(`βœ… Using existing attribute: ${existingAttr.name}`); - } else { - throw error; - } - } - - // 3. Encrypt data with the marketing attribute - const plaintext = 'Sensitive marketing campaign data'; - const dataBuffer = new TextEncoder().encode(plaintext); - - const encryptedStream = await client.encrypt({ - offline: false, - source: dataBuffer, - attributes: ['https://opentdf.io/attr/department/value/marketing'], - }); - - const encryptedBuffer = await new Response(encryptedStream).arrayBuffer(); - const encryptedBytes = new Uint8Array(encryptedBuffer); - console.log('βœ… Data encrypted with marketing attribute'); - - // 4. Grant yourself access to the marketing attribute - const attributeValueId = attrResponse.attribute.values[0].id; - - const scsResponse = await client.subjectMapping.createSubjectConditionSet({ - subjectConditionSet: { - subjectSets: [{ - conditionGroups: [{ - booleanOperator: 'CONDITION_BOOLEAN_TYPE_ENUM_AND', - conditions: [{ - subjectExternalSelectorValue: '.clientId', - operator: 'SUBJECT_MAPPING_OPERATOR_ENUM_IN', - subjectExternalValues: ['opentdf'], - }], - }], - }], - }, - }); - - try { - await client.subjectMapping.createSubjectMapping({ - attributeValueId: attributeValueId, - actions: [{ name: 'read' }], - existingSubjectConditionSetId: scsResponse.subjectConditionSet.id, - }); - console.log('βœ… Granted yourself access to department/marketing'); - } catch (error: any) { - if (error.message && error.message.includes('already_exists')) { - console.log('βœ… Subject mapping already exists for department/marketing'); - } else { - throw error; - } - } - - // 5. Decrypt the data - const decryptedStream = await client.decrypt({ - source: encryptedBytes, - }); - - const decryptedText = await new Response(decryptedStream).text(); - console.log('βœ… Data successfully decrypted'); - console.log(`πŸ“€ Decrypted content: ${decryptedText}`); - } catch (error) { - console.error('❌ Error:', error); - process.exit(1); - } -} - -main(); -``` - -
- -
-
- -## 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**: `.WithInsecurePlaintextConn()` -**Java SDK**: `.useInsecurePlaintextConnection(true)` -**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 -::: - -## Troubleshooting {#troubleshooting} - -### 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 -# Create the attribute definition -otdfctl policy attributes create --name department -s $NAMESPACE_ID -r ANY_OF -export DEPT_ATTRIBUTE_ID= - -# Add the specific value(s) -otdfctl policy attributes values create -a $DEPT_ATTRIBUTE_ID --value finance -``` - -### 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). - -:::warning Common Mistake: Using "decrypt" Instead of "read" -When creating subject mappings for TDF decryption, the action must be `"read"`, not `"decrypt"`. While "decrypt" seems intuitive, the OpenTDF platform uses `"read"` as the action name for TDF decryption entitlements. - -**Incorrect:** -```java -.addActions(Action.newBuilder().setName("decrypt").build()) -``` - -**Correct:** -```java -.addActions(Action.newBuilder().setName("read").build()) -``` - -Using the wrong action name will result in the "error unwrapping key" error shown above. -::: - -**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](#complete-example-create-attribute-encrypt-grant-access-and-decrypt) in this guide show how to handle `already_exists` errors gracefully by catching the error and fetching the existing resource instead. - -2. **Delete the existing resource first**: Use the CLI to remove the existing resource: - ```bash - # List resources to find the ID - otdfctl policy attributes namespaces list - - # Delete the resource - otdfctl policy attributes namespaces 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. - -### 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 -``` - -### Authentication Failed - -**Error**: `rpc error: code = Unauthenticated` or `401 Unauthorized` - -**Solution**: Verify your credentials are correct: -```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` - -**Solution**: The platform uses self-signed certificates for local development. The installation script should have imported these. If you're still seeing errors: - -1. Verify the certificate was imported during installation -2. Restart your terminal/IDE -3. For Go, use `sdk.WithInsecurePlaintextConn()` (development only) -4. For Java, the SDK handles this automatically -5. For Node.js, you may need: `NODE_TLS_REJECT_UNAUTHORIZED=0` (development only) - -### 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 -``` - -## What's Next? {#whats-next} - -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 -- **[Performance Guide](https://github.com/opentdf/sdk-planning/blob/main/9-performance-characteristics-guide.md)**: 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/_category_.json b/docs/sdks/quickstart/_category_.json new file mode 100644 index 0000000..7296404 --- /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 0000000..6987c9e --- /dev/null +++ b/docs/sdks/quickstart/go.mdx @@ -0,0 +1,537 @@ +--- +sidebar_position: 1 +title: Go +unlisted: true +--- + +# 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) + +## 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), + sdk.WithInsecurePlaintextConn(), // Only for local development! + ) + + 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, + 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 + +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: + +```go +import ( + "context" + "github.com/opentdf/platform/protocol/go/policy/attributes" +) + +// Get the existing department attribute +attrFqn := "https://opentdf.io/attr/department" +getAttrResp, err := client.Attributes.GetAttribute(context.Background(), + &attributes.GetAttributeRequest{ + Fqn: attrFqn, + }) +if err != nil { + log.Fatalf("Failed to get attribute: %v", err) +} + +log.Printf("βœ… Found existing attribute: %s", getAttrResp.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) +``` + +:::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"), + 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. + +```go +import ( + "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() + +_, err = client.CreateTDF(outputFile, inputFile, + sdk.WithKasInformation(sdk.KASInfo{URL: platformEndpoint}), +) +``` + +## Step 6: Putting it Together {#step-6-go} + +Here's a complete Go code example that brings all the pieces above 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), + sdk.WithInsecurePlaintextConn(), + ) + 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 + + _, 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 + attributeValueId := attrResp.GetAttribute().GetValues()[0].GetId() + + 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 0000000..0957496 --- /dev/null +++ b/docs/sdks/quickstart/index.mdx @@ -0,0 +1,146 @@ +--- +sidebar_position: 2 +title: SDK Quickstart +unlisted: true +--- + +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**: `.WithInsecurePlaintextConn()` +**Java SDK**: `.useInsecurePlaintextConnection(true)` +**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 0000000..2b5d93e --- /dev/null +++ b/docs/sdks/quickstart/java.mdx @@ -0,0 +1,790 @@ +--- +sidebar_position: 2 +title: Java +unlisted: true +--- + +# 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) + +## 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 + + + +``` + +## 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 (DEV ONLY - DO NOT USE 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."; + + 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 (SDK background threads may not shut down immediately) + 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. + +```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: Putting it Together {#step-6-java} + +Here's a complete Java code example that brings 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 (DEV ONLY - DO NOT USE IN PRODUCTION) + SSLFactory sslFactory = SSLFactory.builder() + .withTrustingAllCertificatesWithoutValidation() + .build(); + + // Create SDK client (try-with-resources ensures proper cleanup) + try (SDK sdk = new SDKBuilder() + .platformEndpoint(platformEndpoint) + .clientSecret(clientId, clientSecret) + .sslFactory(sslFactory) + .build()) { + + 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 + String attributeValueId = attrResponse.getAttribute().getValues(0).getId(); + + 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 + byte[] tdfData = Files.readAllBytes(Path.of("encrypted.tdf")); + System.out.println("βœ… TDF loaded from encrypted.tdf"); + + // 7. Decrypt the data + var decryptedStream = new ByteArrayOutputStream(); + var reader = sdk.loadTDF(new ByteArrayInputStream(tdfData)); + 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 0000000..fcff61a --- /dev/null +++ b/docs/sdks/quickstart/javascript.mdx @@ -0,0 +1,535 @@ +--- +sidebar_position: 3 +title: JavaScript/TypeScript +unlisted: true +--- + +# 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. +::: + +## Prerequisites + +- Node.js 18 or later +- npm or yarn +- Your OpenTDF platform running locally (from Getting Started guide) + +## Step 1: Create a New Project {#step-1-javascript} + +Create a new directory and initialize a Node.js project: + +```bash +mkdir opentdf-quickstart +cd opentdf-quickstart +npm init -y +``` + +## Step 2: Install the SDK {#step-2-javascript} + +```bash +npm install @opentdf/client +``` + +Expected output: +> ```console +> added 45 packages, and audited 46 packages in 3s +> ``` + +## Step 3: Create Your Application {#step-3-javascript} + +### TypeScript Implementation Code + +For **TypeScript** projects, create `quickstart.ts`: + +```typescript title="quickstart.ts" +import { AuthProviders, OpenTDF } from '@opentdf/client'; + +async function main() { + try { + console.log("πŸš€ Starting OpenTDF SDK Quickstart..."); + + const platformEndpoint = "https://platform.opentdf.local:8443"; + const oidcOrigin = "https://keycloak.opentdf.local:9443/realms/opentdf"; + const clientId = "opentdf"; + const clientSecret = "secret"; + + console.log("πŸ“‘ Connecting to platform: " + platformEndpoint); + + // Create authentication provider with client credentials + console.log("πŸ” Setting up client credentials authentication..."); + const authProvider = await AuthProviders.clientSecretAuthProvider({ + clientId, + clientSecret, + oidcOrigin, + exchange: 'client', + }); + console.log("βœ… Authentication provider created"); + + // Create OpenTDF client + console.log("πŸ”§ Initializing SDK client..."); + const client = new OpenTDF({ + authProvider: authProvider, + kasEndpoint: platformEndpoint, + }); + console.log("βœ… SDK client initialized successfully"); + + // Encrypt data + console.log("\nπŸ“ Encrypting sensitive data..."); + const sensitiveData = "Hello from the OpenTDF JavaScript SDK! This data is encrypted."; + const dataBuffer = new TextEncoder().encode(sensitiveData); + + console.log("πŸ”’ Creating TDF..."); + const encryptedStream = await client.encrypt({ + offline: false, + source: dataBuffer, + }); + + // Convert stream to buffer + const encryptedBuffer = await new Response(encryptedStream).arrayBuffer(); + const encryptedBytes = new Uint8Array(encryptedBuffer); + + console.log("βœ… Data successfully encrypted"); + console.log("πŸ“Š Encrypted TDF size: " + encryptedBytes.length + " bytes"); + + // Decrypt data + console.log("\nπŸ”“ Decrypting TDF..."); + const decryptedStream = await client.decrypt({ + source: encryptedBytes, + }); + + const decryptedText = await new Response(decryptedStream).text(); + + console.log("βœ… Data successfully decrypted"); + console.log("πŸ“€ Decrypted content:\n\n" + decryptedText + "\n"); + + console.log("πŸŽ‰ Quickstart complete!"); + process.exit(0); + + } catch (error) { + console.error("❌ Error occurred:", error); + process.exit(1); + } +} + +main(); +``` + +### JavaScript Implementation Code + +For **JavaScript** projects, create `quickstart.mjs`: + +```javascript title="quickstart.mjs" +import { AuthProviders, OpenTDF } from '@opentdf/client'; + +async function main() { + try { + console.log("πŸš€ Starting OpenTDF SDK Quickstart..."); + + const platformEndpoint = "https://platform.opentdf.local:8443"; + const oidcOrigin = "https://keycloak.opentdf.local:9443/realms/opentdf"; + const clientId = "opentdf"; + const clientSecret = "secret"; + + console.log("πŸ“‘ Connecting to platform: " + platformEndpoint); + + // Create authentication provider with client credentials + console.log("πŸ” Setting up client credentials authentication..."); + const authProvider = await AuthProviders.clientSecretAuthProvider({ + clientId, + clientSecret, + oidcOrigin, + exchange: 'client', + }); + console.log("βœ… Authentication provider created"); + + // Create OpenTDF client + console.log("πŸ”§ Initializing SDK client..."); + const client = new OpenTDF({ + authProvider: authProvider, + kasEndpoint: platformEndpoint, + }); + console.log("βœ… SDK client initialized successfully"); + + // Encrypt data + console.log("\nπŸ“ Encrypting sensitive data..."); + const sensitiveData = "Hello from the OpenTDF JavaScript SDK! This data is encrypted."; + const dataBuffer = new TextEncoder().encode(sensitiveData); + + console.log("πŸ”’ Creating TDF..."); + const encryptedStream = await client.encrypt({ + offline: false, + source: dataBuffer, + }); + + // Convert stream to buffer + const encryptedBuffer = await new Response(encryptedStream).arrayBuffer(); + const encryptedBytes = new Uint8Array(encryptedBuffer); + + console.log("βœ… Data successfully encrypted"); + console.log("πŸ“Š Encrypted TDF size: " + encryptedBytes.length + " bytes"); + + // Decrypt data + console.log("\nπŸ”“ Decrypting TDF..."); + const decryptedStream = await client.decrypt({ + source: encryptedBytes, + }); + + const decryptedText = await new Response(decryptedStream).text(); + + console.log("βœ… Data successfully decrypted"); + console.log("πŸ“€ Decrypted content:\n\n" + decryptedText + "\n"); + + console.log("πŸŽ‰ Quickstart complete!"); + process.exit(0); + + } catch (error) { + console.error("❌ Error occurred:", error); + process.exit(1); + } +} + +main(); +``` + +## Step 4: Run Your Application {#step-4-javascript} + +For TypeScript: +```bash +npx tsx quickstart.ts +``` + +For JavaScript: +```bash +node quickstart.mjs +``` + +
+Expected output + +```console +πŸš€ Starting OpenTDF SDK Quickstart... +πŸ“‘ Connecting to platform: https://platform.opentdf.local:8443 +πŸ” Setting up client credentials authentication... +βœ… Authentication provider created +πŸ”§ Initializing SDK client... +βœ… 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 JavaScript SDK! This data is encrypted. + +πŸŽ‰ Quickstart complete! +``` + +
+ +--- + +## Step 5: Add ABAC Features {#step-5-javascript} + +:::tip JavaScript-Specific Examples +The following examples are specific to the **JavaScript/TypeScript 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: + +```typescript +// Get the existing department attribute +const attrFqn = 'https://opentdf.io/attr/department'; +const getAttrResponse = await client.attributes.getAttribute({ + fqn: attrFqn, +}); + +console.log(`βœ… Found existing attribute: ${getAttrResponse.attribute.name}`); + +// Check if "marketing" value already exists +const targetValue = 'marketing'; +const valueExists = getAttrResponse.attribute.values.some( + (value: any) => value.value === targetValue +); + +if (!valueExists) { + // Add the "marketing" value to the existing attribute + await client.attributes.createAttributeValue({ + attributeId: getAttrResponse.attribute.id, + value: targetValue, + }); + console.log(`βœ… Added '${targetValue}' value to department attribute`); +} else { + console.log(`βœ… Attribute 'department' already has '${targetValue}' value`); +} + +console.log(`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. +::: + +### Add Attributes for Access Control + +Now that you've created the attribute, update your `encrypt` call to include the attribute for access control: + +```typescript +const encryptedStream = await client.encrypt({ + offline: false, + source: dataBuffer, + attributes: ["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. + +```typescript +// Get the attribute value ID for "marketing" +const attributeValue = getAttrResponse.attribute.values.find( + (value: any) => value.value === targetValue +); +const attributeValueId = attributeValue.id; + +// Create subject condition set that matches your identity +const scsResponse = await client.subjectMapping.createSubjectConditionSet({ + subjectConditionSet: { + subjectSets: [{ + conditionGroups: [{ + booleanOperator: 'CONDITION_BOOLEAN_TYPE_ENUM_AND', + conditions: [{ + subjectExternalSelectorValue: '.clientId', + operator: 'SUBJECT_MAPPING_OPERATOR_ENUM_IN', + subjectExternalValues: ['opentdf'], + }], + }], + }], + }, +}); + +// Create the subject mapping to grant yourself the entitlement +await client.subjectMapping.createSubjectMapping({ + attributeValueId: attributeValueId, + actions: [{ name: 'read' }], + existingSubjectConditionSetId: scsResponse.subjectConditionSet.id, +}); + +console.log('βœ… 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 + +```typescript +import * as fs from 'fs'; + +// After encryption +fs.writeFileSync('encrypted.tdf', encryptedBytes); + +// Later, decrypt from file +const fileData = fs.readFileSync('encrypted.tdf'); +const decryptedStream = await client.decrypt({ + source: new Uint8Array(fileData), +}); +``` + +## Handle Large Files with Streaming + +```typescript +import * as fs from 'fs'; +import { Readable } from 'stream'; + +// Encrypt a large file +const fileStream = fs.createReadStream('large-file.pdf'); +const encryptedStream = await client.encrypt({ + offline: false, + source: fileStream, +}); + +// Save encrypted stream to file +const writeStream = fs.createWriteStream('large-file.pdf.tdf'); +Readable.fromWeb(encryptedStream).pipe(writeStream); +``` + +## Step 6: Putting it Together {#step-6-javascript} + +Here's a complete TypeScript code example that brings all the pieces together: + +
+Create Attribute, Encrypt, Grant Access, and Decrypt + +```typescript title="complete-example.ts" +import { AuthProviders, OpenTDF } from '@opentdf/client'; +import * as fs from 'fs'; + +async function main() { + try { + const platformEndpoint = 'http://localhost:8080'; + const oidcEndpoint = 'http://localhost:8888/realms/opentdf'; + + // Create authentication provider + const authProvider = await AuthProviders.clientSecretAuthProvider({ + clientId: 'opentdf', + clientSecret: 'secret', + oidcOrigin: oidcEndpoint, + exchange: 'client', + }); + + // Create SDK client + const client = new OpenTDF({ + authProvider: authProvider, + platformUrl: platformEndpoint, + }); + + // 1. Create namespace (or use existing) + let nsResponse; + try { + nsResponse = await client.namespaces.createNamespace({ + name: 'opentdf.io', + }); + console.log(`βœ… Created namespace: ${nsResponse.namespace.id}`); + } catch (error: any) { + if (error.message && error.message.includes('already_exists')) { + // Namespace already exists, fetch it + const listResponse = await client.namespaces.listNamespaces(); + const existingNs = listResponse.namespaces.find( + (ns: any) => ns.name === 'opentdf.io' + ); + nsResponse = { namespace: existingNs }; + console.log(`βœ… Using existing namespace: ${existingNs.id}`); + } else { + throw error; + } + } + + // 2. Create attribute with marketing value (or use existing) + let attrResponse; + try { + attrResponse = await client.attributes.createAttribute({ + namespaceId: nsResponse.namespace.id, + name: 'department', + rule: 'ANY_OF', + values: ['marketing'], + }); + console.log(`βœ… Created attribute: ${attrResponse.attribute.name}`); + } catch (error: any) { + if (error.message && error.message.includes('already_exists')) { + // Attribute already exists, fetch it + const listResponse = await client.attributes.listAttributes(); + const existingAttr = listResponse.attributes.find( + (attr: any) => attr.name === 'department' && attr.namespace.id === nsResponse.namespace.id + ); + attrResponse = { attribute: existingAttr }; + console.log(`βœ… Using existing attribute: ${existingAttr.name}`); + } else { + throw error; + } + } + + // 3. Encrypt data with the marketing attribute + const plaintext = 'Sensitive marketing campaign data'; + const dataBuffer = new TextEncoder().encode(plaintext); + + const encryptedStream = await client.encrypt({ + offline: false, + source: dataBuffer, + attributes: ['https://opentdf.io/attr/department/value/marketing'], + }); + + const encryptedBuffer = await new Response(encryptedStream).arrayBuffer(); + const encryptedBytes = new Uint8Array(encryptedBuffer); + console.log('βœ… Data encrypted with marketing attribute'); + + // 4. Save TDF to file + fs.writeFileSync('encrypted.tdf', encryptedBytes); + console.log('βœ… TDF saved to encrypted.tdf'); + + // 5. Grant yourself access to the marketing attribute + const attributeValueId = attrResponse.attribute.values[0].id; + + const scsResponse = await client.subjectMapping.createSubjectConditionSet({ + subjectConditionSet: { + subjectSets: [{ + conditionGroups: [{ + booleanOperator: 'CONDITION_BOOLEAN_TYPE_ENUM_AND', + conditions: [{ + subjectExternalSelectorValue: '.clientId', + operator: 'SUBJECT_MAPPING_OPERATOR_ENUM_IN', + subjectExternalValues: ['opentdf'], + }], + }], + }], + }, + }); + + try { + await client.subjectMapping.createSubjectMapping({ + attributeValueId: attributeValueId, + actions: [{ name: 'read' }], + existingSubjectConditionSetId: scsResponse.subjectConditionSet.id, + }); + console.log('βœ… Granted yourself access to department/marketing'); + } catch (error: any) { + if (error.message && error.message.includes('already_exists')) { + console.log('βœ… Subject mapping already exists for department/marketing'); + } else { + throw error; + } + } + + // 6. Load TDF from file + const tdfData = fs.readFileSync('encrypted.tdf'); + console.log('βœ… TDF loaded from encrypted.tdf'); + + // 7. Decrypt the data + const decryptedStream = await client.decrypt({ + source: new Uint8Array(tdfData), + }); + + const decryptedText = await new Response(decryptedStream).text(); + console.log('βœ… Data successfully decrypted'); + console.log(`πŸ“€ Decrypted content: ${decryptedText}`); + } catch (error) { + console.error('❌ Error:', error); + process.exit(1); + } +} + +main(); +``` + +
+ +## 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 0000000..95afbec --- /dev/null +++ b/docs/sdks/troubleshooting.mdx @@ -0,0 +1,231 @@ +--- +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. + +## 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/sidebars.js b/sidebars.js index 3327580..9bea9d0 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; From 7493f88cd88614f262e3c80ab7648e55c72e518e Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Wed, 4 Feb 2026 10:48:22 -0800 Subject: [PATCH 07/18] updates to JavaScript Quickstart Signed-off-by: Mary Dickson --- docs/sdks/quickstart/javascript.mdx | 211 ++++++++++++++++++++-------- 1 file changed, 156 insertions(+), 55 deletions(-) diff --git a/docs/sdks/quickstart/javascript.mdx b/docs/sdks/quickstart/javascript.mdx index fcff61a..56fc65a 100644 --- a/docs/sdks/quickstart/javascript.mdx +++ b/docs/sdks/quickstart/javascript.mdx @@ -16,7 +16,45 @@ This guide covers the **JavaScript/TypeScript SDK** implementation. For other la - npm or yarn - Your OpenTDF platform running locally (from Getting Started guide) -## Step 1: Create a New Project {#step-1-javascript} +:::warning Browser Authentication Requirements +The JavaScript SDK is browser-only and cannot use client credentials (client secrets) for security reasons. Instead, you'll need to obtain a refresh token from your OIDC provider. This quickstart shows how to get a token from Keycloak for testing purposes. + +For production applications, your web app would obtain tokens through a proper OAuth/OIDC authentication flow. +::: + +## Step 1: Get a Refresh Token {#step-1-javascript} + +Before you can use the SDK, you need to obtain a refresh token from Keycloak: + +```bash +curl -k -X POST 'https://keycloak.opentdf.local:9443/auth/realms/opentdf/protocol/openid-connect/token' \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + -d 'grant_type=password' \ + -d 'client_id=opentdf-public' \ + -d 'username=sample-user' \ + -d 'password=testuser123' \ + -d 'scope=openid' +``` + +This will return a JSON response. Copy the `refresh_token` value (you'll need it in Step 4). + +
+Example response + +```json +{ + "access_token": "eyJhbGc...", + "expires_in": 300, + "refresh_expires_in": 1800, + "refresh_token": "eyJhbGc...", // Copy this value + "token_type": "Bearer", + "scope": "openid email profile" +} +``` + +
+ +## Step 2: Create a New Project {#step-2-javascript} Create a new directory and initialize a Node.js project: @@ -26,7 +64,7 @@ cd opentdf-quickstart npm init -y ``` -## Step 2: Install the SDK {#step-2-javascript} +## Step 3: Install the SDK {#step-3-javascript} ```bash npm install @opentdf/client @@ -37,39 +75,42 @@ Expected output: > added 45 packages, and audited 46 packages in 3s > ``` -## Step 3: Create Your Application {#step-3-javascript} +## Step 4: Create Your Application {#step-4-javascript} -### TypeScript Implementation Code +
+TypeScript Implementation Code For **TypeScript** projects, create `quickstart.ts`: ```typescript title="quickstart.ts" -import { AuthProviders, OpenTDF } from '@opentdf/client'; +import { AuthProviders, TDF3Client } from '@opentdf/client'; async function main() { try { console.log("πŸš€ Starting OpenTDF SDK Quickstart..."); const platformEndpoint = "https://platform.opentdf.local:8443"; - const oidcOrigin = "https://keycloak.opentdf.local:9443/realms/opentdf"; - const clientId = "opentdf"; - const clientSecret = "secret"; + const oidcOrigin = "https://keycloak.opentdf.local:9443/auth/realms/opentdf"; + const clientId = "opentdf-public"; + + // TODO: Paste your refresh token from Step 1 here + const refreshToken = "YOUR_REFRESH_TOKEN_HERE"; console.log("πŸ“‘ Connecting to platform: " + platformEndpoint); - // Create authentication provider with client credentials - console.log("πŸ” Setting up client credentials authentication..."); - const authProvider = await AuthProviders.clientSecretAuthProvider({ + // Create authentication provider with refresh token + console.log("πŸ” Setting up refresh token authentication..."); + const authProvider = await AuthProviders.refreshAuthProvider({ clientId, - clientSecret, + refreshToken, oidcOrigin, - exchange: 'client', + exchange: 'refresh', }); console.log("βœ… Authentication provider created"); // Create OpenTDF client console.log("πŸ”§ Initializing SDK client..."); - const client = new OpenTDF({ + const client = new TDF3Client({ authProvider: authProvider, kasEndpoint: platformEndpoint, }); @@ -78,16 +119,23 @@ async function main() { // Encrypt data console.log("\nπŸ“ Encrypting sensitive data..."); const sensitiveData = "Hello from the OpenTDF JavaScript SDK! This data is encrypted."; - const dataBuffer = new TextEncoder().encode(sensitiveData); console.log("πŸ”’ Creating TDF..."); + // Create a ReadableStream from the data + const source = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode(sensitiveData)); + controller.close(); + }, + }); + const encryptedStream = await client.encrypt({ offline: false, - source: dataBuffer, + source: source, }); - // Convert stream to buffer - const encryptedBuffer = await new Response(encryptedStream).arrayBuffer(); + // Convert encrypted stream to buffer for decryption + const encryptedBuffer = await new Response(encryptedStream as any).arrayBuffer(); const encryptedBytes = new Uint8Array(encryptedBuffer); console.log("βœ… Data successfully encrypted"); @@ -96,57 +144,60 @@ async function main() { // Decrypt data console.log("\nπŸ”“ Decrypting TDF..."); const decryptedStream = await client.decrypt({ - source: encryptedBytes, + source: { type: 'buffer', location: encryptedBytes }, }); - const decryptedText = await new Response(decryptedStream).text(); + const decryptedText = await decryptedStream.toString(); console.log("βœ… Data successfully decrypted"); console.log("πŸ“€ Decrypted content:\n\n" + decryptedText + "\n"); console.log("πŸŽ‰ Quickstart complete!"); - process.exit(0); } catch (error) { console.error("❌ Error occurred:", error); - process.exit(1); } } main(); ``` -### JavaScript Implementation Code +
+ +
+JavaScript Implementation Code For **JavaScript** projects, create `quickstart.mjs`: ```javascript title="quickstart.mjs" -import { AuthProviders, OpenTDF } from '@opentdf/client'; +import { AuthProviders, TDF3Client } from '@opentdf/client'; async function main() { try { console.log("πŸš€ Starting OpenTDF SDK Quickstart..."); const platformEndpoint = "https://platform.opentdf.local:8443"; - const oidcOrigin = "https://keycloak.opentdf.local:9443/realms/opentdf"; - const clientId = "opentdf"; - const clientSecret = "secret"; + const oidcOrigin = "https://keycloak.opentdf.local:9443/auth/realms/opentdf"; + const clientId = "opentdf-public"; + + // TODO: Paste your refresh token from Step 1 here + const refreshToken = "YOUR_REFRESH_TOKEN_HERE"; console.log("πŸ“‘ Connecting to platform: " + platformEndpoint); - // Create authentication provider with client credentials - console.log("πŸ” Setting up client credentials authentication..."); - const authProvider = await AuthProviders.clientSecretAuthProvider({ + // Create authentication provider with refresh token + console.log("πŸ” Setting up refresh token authentication..."); + const authProvider = await AuthProviders.refreshAuthProvider({ clientId, - clientSecret, + refreshToken, oidcOrigin, - exchange: 'client', + exchange: 'refresh', }); console.log("βœ… Authentication provider created"); // Create OpenTDF client console.log("πŸ”§ Initializing SDK client..."); - const client = new OpenTDF({ + const client = new TDF3Client({ authProvider: authProvider, kasEndpoint: platformEndpoint, }); @@ -155,15 +206,22 @@ async function main() { // Encrypt data console.log("\nπŸ“ Encrypting sensitive data..."); const sensitiveData = "Hello from the OpenTDF JavaScript SDK! This data is encrypted."; - const dataBuffer = new TextEncoder().encode(sensitiveData); console.log("πŸ”’ Creating TDF..."); + // Create a ReadableStream from the data + const source = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode(sensitiveData)); + controller.close(); + }, + }); + const encryptedStream = await client.encrypt({ offline: false, - source: dataBuffer, + source: source, }); - // Convert stream to buffer + // Convert encrypted stream to buffer for decryption const encryptedBuffer = await new Response(encryptedStream).arrayBuffer(); const encryptedBytes = new Uint8Array(encryptedBuffer); @@ -173,45 +231,73 @@ async function main() { // Decrypt data console.log("\nπŸ”“ Decrypting TDF..."); const decryptedStream = await client.decrypt({ - source: encryptedBytes, + source: { type: 'buffer', location: encryptedBytes }, }); - const decryptedText = await new Response(decryptedStream).text(); + const decryptedText = await decryptedStream.toString(); console.log("βœ… Data successfully decrypted"); console.log("πŸ“€ Decrypted content:\n\n" + decryptedText + "\n"); console.log("πŸŽ‰ Quickstart complete!"); - process.exit(0); } catch (error) { console.error("❌ Error occurred:", error); - process.exit(1); } } main(); ``` -## Step 4: Run Your Application {#step-4-javascript} +
+ +## Step 5: Run Your Application {#step-5-javascript} + +The JavaScript SDK is designed for browser environments. To run your quickstart code, you'll need to set up a simple web application. + +### Create an HTML File + +Create `index.html` in your project directory: + +```html title="index.html" + + + + + + OpenTDF Quickstart + + +

OpenTDF JavaScript SDK Quickstart

+

Check the browser console for output (F12 or Right-click β†’ Inspect β†’ Console)

+ + + +``` + +### Install and Run Vite Dev Server + +Install Vite as a development dependency: -For TypeScript: ```bash -npx tsx quickstart.ts +npm install --save-dev vite ``` -For JavaScript: +Start the development server: + ```bash -node quickstart.mjs +npx vite ``` +Open your browser to `http://localhost:5173` (or the URL shown in the terminal). Open the browser console (F12 or Right-click β†’ Inspect β†’ Console) to see the output. +
-Expected output +Expected browser console output ```console πŸš€ Starting OpenTDF SDK Quickstart... πŸ“‘ Connecting to platform: https://platform.opentdf.local:8443 -πŸ” Setting up client credentials authentication... +πŸ” Setting up refresh token authentication... βœ… Authentication provider created πŸ”§ Initializing SDK client... βœ… SDK client initialized successfully @@ -234,7 +320,7 @@ Hello from the OpenTDF JavaScript SDK! This data is encrypted. --- -## Step 5: Add ABAC Features {#step-5-javascript} +## Step 6: Add ABAC Features {#step-6-javascript} :::tip JavaScript-Specific Examples The following examples are specific to the **JavaScript/TypeScript SDK**. @@ -350,6 +436,10 @@ In production applications, you'll often need to persist encrypted TDFs to disk - **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 +:::note Browser vs Server-Side +The example below shows Node.js file system operations. In a browser environment, you would trigger a download using the browser's download API or save data to IndexedDB. For server-side applications using Node.js, you'll need to handle the SDK's browser dependencies appropriately. +::: + ```typescript import * as fs from 'fs'; @@ -365,6 +455,10 @@ const decryptedStream = await client.decrypt({ ## Handle Large Files with Streaming +:::note Browser vs Server-Side +The example below shows Node.js streaming with the file system. In a browser environment, you would use the File API to read files and trigger downloads for the encrypted output. For server-side applications using Node.js, you'll need to handle the SDK's browser dependencies appropriately. +::: + ```typescript import * as fs from 'fs'; import { Readable } from 'stream'; @@ -381,15 +475,19 @@ const writeStream = fs.createWriteStream('large-file.pdf.tdf'); Readable.fromWeb(encryptedStream).pipe(writeStream); ``` -## Step 6: Putting it Together {#step-6-javascript} +## Step 7: Putting it Together {#step-7-javascript} Here's a complete TypeScript code example that brings all the pieces together: +:::note +This example includes Node.js file system operations (`fs.writeFileSync`, `fs.readFileSync`). In a browser environment, you would use browser APIs for file handling instead. +::: +
Create Attribute, Encrypt, Grant Access, and Decrypt ```typescript title="complete-example.ts" -import { AuthProviders, OpenTDF } from '@opentdf/client'; +import { AuthProviders, TDF3Client } from '@opentdf/client'; import * as fs from 'fs'; async function main() { @@ -397,18 +495,21 @@ async function main() { const platformEndpoint = 'http://localhost:8080'; const oidcEndpoint = 'http://localhost:8888/realms/opentdf'; + // TODO: Paste your refresh token here + const refreshToken = "YOUR_REFRESH_TOKEN_HERE"; + // Create authentication provider - const authProvider = await AuthProviders.clientSecretAuthProvider({ + const authProvider = await AuthProviders.refreshAuthProvider({ clientId: 'opentdf', - clientSecret: 'secret', + refreshToken, oidcOrigin: oidcEndpoint, - exchange: 'client', + exchange: 'refresh', }); // Create SDK client - const client = new OpenTDF({ + const client = new TDF3Client({ authProvider: authProvider, - platformUrl: platformEndpoint, + kasEndpoint: platformEndpoint, }); // 1. Create namespace (or use existing) From 115470f2861d308d8c833f939523bebd288309d8 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Wed, 4 Feb 2026 14:38:10 -0800 Subject: [PATCH 08/18] agent feedback Signed-off-by: Mary Dickson --- docs/getting-started/managing-platform.mdx | 6 +- docs/getting-started/quickstart.mdx | 293 +++++++++++++++++++-- docs/sdks/troubleshooting.mdx | 4 + 3 files changed, 283 insertions(+), 20 deletions(-) diff --git a/docs/getting-started/managing-platform.mdx b/docs/getting-started/managing-platform.mdx index 2174c40..8149646 100644 --- a/docs/getting-started/managing-platform.mdx +++ b/docs/getting-started/managing-platform.mdx @@ -73,7 +73,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 diff --git a/docs/getting-started/quickstart.mdx b/docs/getting-started/quickstart.mdx index 23d69f1..0d4009e 100644 --- a/docs/getting-started/quickstart.mdx +++ b/docs/getting-started/quickstart.mdx @@ -145,7 +145,41 @@ We'll run a local instance using Docker. This includes the [Platform](https://gi Not for production use. ::: -#### Step 1: Run Pre-flight Check (Optional) +#### Step 1: Check for Port Conflicts (Recommended) + +OpenTDF uses these ports by default: +- **8443**: Platform API +- **9443**: Keycloak authentication +- **2019**: Caddy admin +- **5432**: PostgreSQL + +Port 9443 commonly conflicts with other services (Rancher, steve, Kubernetes dashboards). Before installing, check if these ports are available: + +```shell +# Check for port conflicts +lsof -i :9443 # Most common conflict +lsof -i :8443 +lsof -i :5432 +``` + +If ports are in use, configure OpenTDF to use different ports before installation: + +```shell +# Create config directory +mkdir -p ~/.opentdf/platform + +# Set custom ports +cat > ~/.opentdf/platform/.env <<'EOF' +PLATFORM_PORT=8443 +KEYCLOAK_PORT=9444 +CADDY_ADMIN_PORT=2019 +POSTGRES_PORT=5432 +EOF +``` + +Change `KEYCLOAK_PORT` to an available port if 9443 is in use. + +#### Step 2: Run Pre-flight Check (Optional) Verify your system meets the requirements: @@ -155,7 +189,7 @@ 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 ``` @@ -201,9 +235,9 @@ Ready to install OpenTDF: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ``` -
+>
-#### Step 2: Install OpenTDF +#### Step 3: Install OpenTDF Run the automated installer: @@ -213,7 +247,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 +296,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 @@ -283,7 +321,7 @@ Password: [you'll be prompted for your password again] /Users/username/.opentdf/bin/otdfctl --version 4. Create an otdfctl profile: - /Users/username/.opentdf/bin/otdfctl profile create platform-otdf-local https://platform.opentdf.local:8443 + /Users/username/.opentdf/bin/otdfctl profile create platform https://platform.opentdf.local:8443 🌐 Access points: Platform: https://platform.opentdf.local:8443 @@ -319,7 +357,7 @@ You'll be prompted for your password twice: 1. To add entries to /etc/hosts 2. To import the SSL certificate -#### Step 3: Add otdfctl to Your PATH (Optional) +#### Step 4: Add otdfctl to Your PATH (Optional) Choose one option: @@ -350,7 +388,7 @@ Expected output: > otdfctl version 0.x.x (2026-XX-XXT00:00:00Z) > ``` -#### Step 4: Verify Installation +#### Step 5: Verify Installation ```shell otdfctl --version @@ -374,19 +412,19 @@ Expected output: When you visit the Keycloak URL, seeing "Resource not found" is expected and indicates Keycloak is running correctly. -#### Step 5: Create an otdfctl Profile +#### Step 6: Create an otdfctl Profile A profile stores your platform connection details and authentication credentials, making commands shorter and easier to use. 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. ```shell -otdfctl profile create platform-otdf-local https://platform.opentdf.local:8443 +otdfctl profile create platform https://platform.opentdf.local:8443 ``` Expected output: > ```console -> SUCCESS Profile platform-otdf-local created +> SUCCESS Profile platform created > ``` **About Default Profiles:** @@ -396,16 +434,30 @@ When you have only one profile (like most users following this guide), it automa If you have multiple profiles, you can set which one is the default: ```shell -otdfctl profile set-default platform-otdf-local +otdfctl profile set-default platform ``` Expected output: > ```console -> Set profile platform-otdf-local as default +> Set profile platform as default > ``` 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. +#### Step 7: Trust SSL Certificate (If Needed) + +The installer attempts to import Caddy's self-signed certificate automatically, but this may fail without proper permissions. + +**Symptoms of untrusted certificate:** +- Browser shows "Not Secure" warning when visiting platform URLs +- curl commands require `-k` flag +- SDK applications show certificate validation errors + +**Solution:** Follow the [TLS Certificate Verification](/getting-started/managing-platform#tls-certificate-verification) guide for detailed instructions on: +- Extracting the Caddy root CA certificate +- Trusting it on your OS (macOS, Linux, or Windows) +- Using the `OTDFCTL_TLS_NO_VERIFY` environment variable as a workaround + :::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. ::: @@ -420,7 +472,7 @@ otdfctl auth client-credentials opentdf secret Expected output: > ```console -> Client credentials set for profile [platform-otdf-local] +> Client credentials set for profile [platform] > ``` Credentials: @@ -770,7 +822,21 @@ otdfctl policy attributes get --id=$DEPT_ATTRIBUTE_ID ### Create Clearance Attribute -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. +Now create a clearance attribute using `HIERARCHY` rule. + +:::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) + +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 + +The order matters! Always create values from highest to lowest privilege. +::: + +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 @@ -799,7 +865,7 @@ 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. We'll create "executive" first (highest clearance), then "standard" (lower clearance): ```shell otdfctl policy attributes values create -a $CLEARANCE_ATTRIBUTE_ID --value executive @@ -912,7 +978,7 @@ For this tutorial, we'll simulate our three personas by creating condition sets { "conditions": [ { - "subject_external_selector_value": ".clientId", + "subject_external_selector_value": ".client_id", "operator": 1, "subject_external_values": [ "opentdf" @@ -998,7 +1064,7 @@ This is an authorization **decision** in action! Here's what happened: Now let's grant an **entitlement** to represent Jen's access level. This changes what attribute values we're entitled to: ```shell -otdfctl policy subject-mappings create --action read --attribute-value-id $EXECUTIVE_VALUE_ID --subject-condition-set-id $SUBJECT_CONDITION_SET_ID +otdfctl policy subject-mappings create --action STANDARD_ACTION_DECRYPT --attribute-value-id $EXECUTIVE_VALUE_ID --subject-condition-set-id $SUBJECT_CONDITION_SET_ID ``` > ```console @@ -1058,7 +1124,7 @@ Notice this document has TWO attributes - it requires BOTH finance department me To simulate Preston's access, let's grant ourselves 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 +otdfctl policy subject-mappings create --action STANDARD_ACTION_DECRYPT --attribute-value-id $FINANCE_VALUE_ID --subject-condition-set-id $SUBJECT_CONDITION_SET_ID ``` Expected output: @@ -1139,3 +1205,192 @@ In production, you would: - 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 + +## Troubleshooting + +:::note Troubleshooting Scope +This section covers issues specific to **platform installation and setup**. For SDK-specific errors when writing code, see [SDK Troubleshooting](/sdks/troubleshooting). +::: + +### "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 in Step 1, 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 +# Token shows "client_id" - your condition set must use ".client_id" not ".clientId" + +# 4. Check platform logs for details +docker logs platform-platform-1 --tail 50 +``` + +### TLS/Certificate Warnings + +**Symptom:** Browser shows "Not Secure", curl/SDK commands show certificate validation errors + +**Solution:** See [TLS Certificate Verification](/getting-started/managing-platform#tls-certificate-verification) for complete instructions on trusting the Caddy root CA certificate + +### 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 + +```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 STANDARD_ACTION_DECRYPT \ + --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 + +```shell +# Configure in ~/.opentdf/platform/.env if needed +PLATFORM_PORT=8443 +KEYCLOAK_PORT=9443 +CADDY_ADMIN_PORT=2019 +POSTGRES_PORT=5432 +``` + +### 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/sdks/troubleshooting.mdx b/docs/sdks/troubleshooting.mdx index 95afbec..4910e43 100644 --- a/docs/sdks/troubleshooting.mdx +++ b/docs/sdks/troubleshooting.mdx @@ -10,6 +10,10 @@ import TabItem from '@theme/TabItem'; 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` From 19d599248e58d3730641628a4499e7256f83d4df Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Thu, 5 Feb 2026 06:20:53 -0800 Subject: [PATCH 09/18] fix ci error Signed-off-by: Mary Dickson --- docs/getting-started/quickstart.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/getting-started/quickstart.mdx b/docs/getting-started/quickstart.mdx index 0d4009e..e006157 100644 --- a/docs/getting-started/quickstart.mdx +++ b/docs/getting-started/quickstart.mdx @@ -235,7 +235,7 @@ Ready to install OpenTDF: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ``` ->
+
#### Step 3: Install OpenTDF @@ -1232,7 +1232,7 @@ lsof -i :9443 # or whichever port is conflicting cd ~/.opentdf/platform && docker compose restart ``` -### "Invalid character '<'" Error During Authentication +### "Invalid character" Error During Authentication **Symptom:** ```console From 5b3daee67e6f89cbb092258a276947b519526e07 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Thu, 5 Feb 2026 06:47:07 -0800 Subject: [PATCH 10/18] clean up js page and overall language Signed-off-by: Mary Dickson --- README.md | 24 +- docs/getting-started/quickstart.mdx | 32 +- docs/sdks/quickstart/javascript.mdx | 625 +--------------------------- 3 files changed, 45 insertions(+), 636 deletions(-) diff --git a/README.md b/README.md index 3e929f9..a3f0437 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/docs/getting-started/quickstart.mdx b/docs/getting-started/quickstart.mdx index e006157..89ec832 100644 --- a/docs/getting-started/quickstart.mdx +++ b/docs/getting-started/quickstart.mdx @@ -32,7 +32,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 +102,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,7 +139,7 @@ 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. @@ -608,11 +608,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 @@ -717,7 +717,7 @@ You can create values when creating the definition: `--value finance --value eng ### 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 @@ -865,7 +865,7 @@ export CLEARANCE_ATTRIBUTE_ID= ### Add Clearance Values -Add clearance levels in order from highest to lowest privilege. We'll create "executive" first (highest clearance), then "standard" (lower clearance): +Add clearance levels in order from highest to lowest privilege. Create "executive" first (highest clearance), then "standard" (lower clearance): ```shell otdfctl policy attributes values create -a $CLEARANCE_ATTRIBUTE_ID --value executive @@ -949,13 +949,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 @@ -969,7 +969,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" [ @@ -1019,7 +1019,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= @@ -1041,7 +1041,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 @@ -1061,7 +1061,7 @@ 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: ```shell otdfctl policy subject-mappings create --action STANDARD_ACTION_DECRYPT --attribute-value-id $EXECUTIVE_VALUE_ID --subject-condition-set-id $SUBJECT_CONDITION_SET_ID @@ -1088,7 +1088,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 @@ -1121,7 +1121,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 STANDARD_ACTION_DECRYPT --attribute-value-id $FINANCE_VALUE_ID --subject-condition-set-id $SUBJECT_CONDITION_SET_ID @@ -1145,7 +1145,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 diff --git a/docs/sdks/quickstart/javascript.mdx b/docs/sdks/quickstart/javascript.mdx index 56fc65a..38c893e 100644 --- a/docs/sdks/quickstart/javascript.mdx +++ b/docs/sdks/quickstart/javascript.mdx @@ -10,627 +10,18 @@ unlisted: true This guide covers the **JavaScript/TypeScript SDK** implementation. For other languages or general information, see the [SDK Quickstart](/sdks/quickstart) page. ::: -## Prerequisites +:::info Coming Soon +The JavaScript/TypeScript SDK quickstart guide is currently under development. -- Node.js 18 or later -- npm or yarn -- Your OpenTDF platform running locally (from Getting Started guide) +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 -:::warning Browser Authentication Requirements -The JavaScript SDK is browser-only and cannot use client credentials (client secrets) for security reasons. Instead, you'll need to obtain a refresh token from your OIDC provider. This quickstart shows how to get a token from Keycloak for testing purposes. - -For production applications, your web app would obtain tokens through a proper OAuth/OIDC authentication flow. +Check back soon for a complete JavaScript/TypeScript quickstart guide! ::: -## Step 1: Get a Refresh Token {#step-1-javascript} - -Before you can use the SDK, you need to obtain a refresh token from Keycloak: - -```bash -curl -k -X POST 'https://keycloak.opentdf.local:9443/auth/realms/opentdf/protocol/openid-connect/token' \ - -H 'Content-Type: application/x-www-form-urlencoded' \ - -d 'grant_type=password' \ - -d 'client_id=opentdf-public' \ - -d 'username=sample-user' \ - -d 'password=testuser123' \ - -d 'scope=openid' -``` - -This will return a JSON response. Copy the `refresh_token` value (you'll need it in Step 4). - -
-Example response - -```json -{ - "access_token": "eyJhbGc...", - "expires_in": 300, - "refresh_expires_in": 1800, - "refresh_token": "eyJhbGc...", // Copy this value - "token_type": "Bearer", - "scope": "openid email profile" -} -``` - -
- -## Step 2: Create a New Project {#step-2-javascript} - -Create a new directory and initialize a Node.js project: - -```bash -mkdir opentdf-quickstart -cd opentdf-quickstart -npm init -y -``` - -## Step 3: Install the SDK {#step-3-javascript} - -```bash -npm install @opentdf/client -``` - -Expected output: -> ```console -> added 45 packages, and audited 46 packages in 3s -> ``` - -## Step 4: Create Your Application {#step-4-javascript} - -
-TypeScript Implementation Code - -For **TypeScript** projects, create `quickstart.ts`: - -```typescript title="quickstart.ts" -import { AuthProviders, TDF3Client } from '@opentdf/client'; - -async function main() { - try { - console.log("πŸš€ Starting OpenTDF SDK Quickstart..."); - - const platformEndpoint = "https://platform.opentdf.local:8443"; - const oidcOrigin = "https://keycloak.opentdf.local:9443/auth/realms/opentdf"; - const clientId = "opentdf-public"; - - // TODO: Paste your refresh token from Step 1 here - const refreshToken = "YOUR_REFRESH_TOKEN_HERE"; - - console.log("πŸ“‘ Connecting to platform: " + platformEndpoint); - - // Create authentication provider with refresh token - console.log("πŸ” Setting up refresh token authentication..."); - const authProvider = await AuthProviders.refreshAuthProvider({ - clientId, - refreshToken, - oidcOrigin, - exchange: 'refresh', - }); - console.log("βœ… Authentication provider created"); - - // Create OpenTDF client - console.log("πŸ”§ Initializing SDK client..."); - const client = new TDF3Client({ - authProvider: authProvider, - kasEndpoint: platformEndpoint, - }); - console.log("βœ… SDK client initialized successfully"); - - // Encrypt data - console.log("\nπŸ“ Encrypting sensitive data..."); - const sensitiveData = "Hello from the OpenTDF JavaScript SDK! This data is encrypted."; - - console.log("πŸ”’ Creating TDF..."); - // Create a ReadableStream from the data - const source = new ReadableStream({ - start(controller) { - controller.enqueue(new TextEncoder().encode(sensitiveData)); - controller.close(); - }, - }); - - const encryptedStream = await client.encrypt({ - offline: false, - source: source, - }); - - // Convert encrypted stream to buffer for decryption - const encryptedBuffer = await new Response(encryptedStream as any).arrayBuffer(); - const encryptedBytes = new Uint8Array(encryptedBuffer); - - console.log("βœ… Data successfully encrypted"); - console.log("πŸ“Š Encrypted TDF size: " + encryptedBytes.length + " bytes"); - - // Decrypt data - console.log("\nπŸ”“ Decrypting TDF..."); - const decryptedStream = await client.decrypt({ - source: { type: 'buffer', location: encryptedBytes }, - }); - - const decryptedText = await decryptedStream.toString(); - - console.log("βœ… Data successfully decrypted"); - console.log("πŸ“€ Decrypted content:\n\n" + decryptedText + "\n"); - - console.log("πŸŽ‰ Quickstart complete!"); - - } catch (error) { - console.error("❌ Error occurred:", error); - } -} - -main(); -``` - -
- -
-JavaScript Implementation Code - -For **JavaScript** projects, create `quickstart.mjs`: - -```javascript title="quickstart.mjs" -import { AuthProviders, TDF3Client } from '@opentdf/client'; - -async function main() { - try { - console.log("πŸš€ Starting OpenTDF SDK Quickstart..."); - - const platformEndpoint = "https://platform.opentdf.local:8443"; - const oidcOrigin = "https://keycloak.opentdf.local:9443/auth/realms/opentdf"; - const clientId = "opentdf-public"; - - // TODO: Paste your refresh token from Step 1 here - const refreshToken = "YOUR_REFRESH_TOKEN_HERE"; - - console.log("πŸ“‘ Connecting to platform: " + platformEndpoint); - - // Create authentication provider with refresh token - console.log("πŸ” Setting up refresh token authentication..."); - const authProvider = await AuthProviders.refreshAuthProvider({ - clientId, - refreshToken, - oidcOrigin, - exchange: 'refresh', - }); - console.log("βœ… Authentication provider created"); - - // Create OpenTDF client - console.log("πŸ”§ Initializing SDK client..."); - const client = new TDF3Client({ - authProvider: authProvider, - kasEndpoint: platformEndpoint, - }); - console.log("βœ… SDK client initialized successfully"); - - // Encrypt data - console.log("\nπŸ“ Encrypting sensitive data..."); - const sensitiveData = "Hello from the OpenTDF JavaScript SDK! This data is encrypted."; - - console.log("πŸ”’ Creating TDF..."); - // Create a ReadableStream from the data - const source = new ReadableStream({ - start(controller) { - controller.enqueue(new TextEncoder().encode(sensitiveData)); - controller.close(); - }, - }); - - const encryptedStream = await client.encrypt({ - offline: false, - source: source, - }); - - // Convert encrypted stream to buffer for decryption - const encryptedBuffer = await new Response(encryptedStream).arrayBuffer(); - const encryptedBytes = new Uint8Array(encryptedBuffer); - - console.log("βœ… Data successfully encrypted"); - console.log("πŸ“Š Encrypted TDF size: " + encryptedBytes.length + " bytes"); - - // Decrypt data - console.log("\nπŸ”“ Decrypting TDF..."); - const decryptedStream = await client.decrypt({ - source: { type: 'buffer', location: encryptedBytes }, - }); - - const decryptedText = await decryptedStream.toString(); - - console.log("βœ… Data successfully decrypted"); - console.log("πŸ“€ Decrypted content:\n\n" + decryptedText + "\n"); - - console.log("πŸŽ‰ Quickstart complete!"); - - } catch (error) { - console.error("❌ Error occurred:", error); - } -} - -main(); -``` - -
- -## Step 5: Run Your Application {#step-5-javascript} - -The JavaScript SDK is designed for browser environments. To run your quickstart code, you'll need to set up a simple web application. - -### Create an HTML File - -Create `index.html` in your project directory: - -```html title="index.html" - - - - - - OpenTDF Quickstart - - -

OpenTDF JavaScript SDK Quickstart

-

Check the browser console for output (F12 or Right-click β†’ Inspect β†’ Console)

- - - -``` - -### Install and Run Vite Dev Server - -Install Vite as a development dependency: - -```bash -npm install --save-dev vite -``` - -Start the development server: - -```bash -npx vite -``` - -Open your browser to `http://localhost:5173` (or the URL shown in the terminal). Open the browser console (F12 or Right-click β†’ Inspect β†’ Console) to see the output. - -
-Expected browser console output - -```console -πŸš€ Starting OpenTDF SDK Quickstart... -πŸ“‘ Connecting to platform: https://platform.opentdf.local:8443 -πŸ” Setting up refresh token authentication... -βœ… Authentication provider created -πŸ”§ Initializing SDK client... -βœ… 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 JavaScript SDK! This data is encrypted. - -πŸŽ‰ Quickstart complete! -``` - -
- ---- - -## Step 6: Add ABAC Features {#step-6-javascript} - -:::tip JavaScript-Specific Examples -The following examples are specific to the **JavaScript/TypeScript 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: - -```typescript -// Get the existing department attribute -const attrFqn = 'https://opentdf.io/attr/department'; -const getAttrResponse = await client.attributes.getAttribute({ - fqn: attrFqn, -}); - -console.log(`βœ… Found existing attribute: ${getAttrResponse.attribute.name}`); - -// Check if "marketing" value already exists -const targetValue = 'marketing'; -const valueExists = getAttrResponse.attribute.values.some( - (value: any) => value.value === targetValue -); - -if (!valueExists) { - // Add the "marketing" value to the existing attribute - await client.attributes.createAttributeValue({ - attributeId: getAttrResponse.attribute.id, - value: targetValue, - }); - console.log(`βœ… Added '${targetValue}' value to department attribute`); -} else { - console.log(`βœ… Attribute 'department' already has '${targetValue}' value`); -} - -console.log(`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. -::: - -### Add Attributes for Access Control - -Now that you've created the attribute, update your `encrypt` call to include the attribute for access control: - -```typescript -const encryptedStream = await client.encrypt({ - offline: false, - source: dataBuffer, - attributes: ["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. - -```typescript -// Get the attribute value ID for "marketing" -const attributeValue = getAttrResponse.attribute.values.find( - (value: any) => value.value === targetValue -); -const attributeValueId = attributeValue.id; - -// Create subject condition set that matches your identity -const scsResponse = await client.subjectMapping.createSubjectConditionSet({ - subjectConditionSet: { - subjectSets: [{ - conditionGroups: [{ - booleanOperator: 'CONDITION_BOOLEAN_TYPE_ENUM_AND', - conditions: [{ - subjectExternalSelectorValue: '.clientId', - operator: 'SUBJECT_MAPPING_OPERATOR_ENUM_IN', - subjectExternalValues: ['opentdf'], - }], - }], - }], - }, -}); - -// Create the subject mapping to grant yourself the entitlement -await client.subjectMapping.createSubjectMapping({ - attributeValueId: attributeValueId, - actions: [{ name: 'read' }], - existingSubjectConditionSetId: scsResponse.subjectConditionSet.id, -}); - -console.log('βœ… 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 - -:::note Browser vs Server-Side -The example below shows Node.js file system operations. In a browser environment, you would trigger a download using the browser's download API or save data to IndexedDB. For server-side applications using Node.js, you'll need to handle the SDK's browser dependencies appropriately. -::: - -```typescript -import * as fs from 'fs'; - -// After encryption -fs.writeFileSync('encrypted.tdf', encryptedBytes); - -// Later, decrypt from file -const fileData = fs.readFileSync('encrypted.tdf'); -const decryptedStream = await client.decrypt({ - source: new Uint8Array(fileData), -}); -``` - -## Handle Large Files with Streaming - -:::note Browser vs Server-Side -The example below shows Node.js streaming with the file system. In a browser environment, you would use the File API to read files and trigger downloads for the encrypted output. For server-side applications using Node.js, you'll need to handle the SDK's browser dependencies appropriately. -::: - -```typescript -import * as fs from 'fs'; -import { Readable } from 'stream'; - -// Encrypt a large file -const fileStream = fs.createReadStream('large-file.pdf'); -const encryptedStream = await client.encrypt({ - offline: false, - source: fileStream, -}); - -// Save encrypted stream to file -const writeStream = fs.createWriteStream('large-file.pdf.tdf'); -Readable.fromWeb(encryptedStream).pipe(writeStream); -``` - -## Step 7: Putting it Together {#step-7-javascript} - -Here's a complete TypeScript code example that brings all the pieces together: - -:::note -This example includes Node.js file system operations (`fs.writeFileSync`, `fs.readFileSync`). In a browser environment, you would use browser APIs for file handling instead. -::: - -
-Create Attribute, Encrypt, Grant Access, and Decrypt - -```typescript title="complete-example.ts" -import { AuthProviders, TDF3Client } from '@opentdf/client'; -import * as fs from 'fs'; - -async function main() { - try { - const platformEndpoint = 'http://localhost:8080'; - const oidcEndpoint = 'http://localhost:8888/realms/opentdf'; - - // TODO: Paste your refresh token here - const refreshToken = "YOUR_REFRESH_TOKEN_HERE"; - - // Create authentication provider - const authProvider = await AuthProviders.refreshAuthProvider({ - clientId: 'opentdf', - refreshToken, - oidcOrigin: oidcEndpoint, - exchange: 'refresh', - }); - - // Create SDK client - const client = new TDF3Client({ - authProvider: authProvider, - kasEndpoint: platformEndpoint, - }); - - // 1. Create namespace (or use existing) - let nsResponse; - try { - nsResponse = await client.namespaces.createNamespace({ - name: 'opentdf.io', - }); - console.log(`βœ… Created namespace: ${nsResponse.namespace.id}`); - } catch (error: any) { - if (error.message && error.message.includes('already_exists')) { - // Namespace already exists, fetch it - const listResponse = await client.namespaces.listNamespaces(); - const existingNs = listResponse.namespaces.find( - (ns: any) => ns.name === 'opentdf.io' - ); - nsResponse = { namespace: existingNs }; - console.log(`βœ… Using existing namespace: ${existingNs.id}`); - } else { - throw error; - } - } - - // 2. Create attribute with marketing value (or use existing) - let attrResponse; - try { - attrResponse = await client.attributes.createAttribute({ - namespaceId: nsResponse.namespace.id, - name: 'department', - rule: 'ANY_OF', - values: ['marketing'], - }); - console.log(`βœ… Created attribute: ${attrResponse.attribute.name}`); - } catch (error: any) { - if (error.message && error.message.includes('already_exists')) { - // Attribute already exists, fetch it - const listResponse = await client.attributes.listAttributes(); - const existingAttr = listResponse.attributes.find( - (attr: any) => attr.name === 'department' && attr.namespace.id === nsResponse.namespace.id - ); - attrResponse = { attribute: existingAttr }; - console.log(`βœ… Using existing attribute: ${existingAttr.name}`); - } else { - throw error; - } - } - - // 3. Encrypt data with the marketing attribute - const plaintext = 'Sensitive marketing campaign data'; - const dataBuffer = new TextEncoder().encode(plaintext); - - const encryptedStream = await client.encrypt({ - offline: false, - source: dataBuffer, - attributes: ['https://opentdf.io/attr/department/value/marketing'], - }); - - const encryptedBuffer = await new Response(encryptedStream).arrayBuffer(); - const encryptedBytes = new Uint8Array(encryptedBuffer); - console.log('βœ… Data encrypted with marketing attribute'); - - // 4. Save TDF to file - fs.writeFileSync('encrypted.tdf', encryptedBytes); - console.log('βœ… TDF saved to encrypted.tdf'); - - // 5. Grant yourself access to the marketing attribute - const attributeValueId = attrResponse.attribute.values[0].id; - - const scsResponse = await client.subjectMapping.createSubjectConditionSet({ - subjectConditionSet: { - subjectSets: [{ - conditionGroups: [{ - booleanOperator: 'CONDITION_BOOLEAN_TYPE_ENUM_AND', - conditions: [{ - subjectExternalSelectorValue: '.clientId', - operator: 'SUBJECT_MAPPING_OPERATOR_ENUM_IN', - subjectExternalValues: ['opentdf'], - }], - }], - }], - }, - }); - - try { - await client.subjectMapping.createSubjectMapping({ - attributeValueId: attributeValueId, - actions: [{ name: 'read' }], - existingSubjectConditionSetId: scsResponse.subjectConditionSet.id, - }); - console.log('βœ… Granted yourself access to department/marketing'); - } catch (error: any) { - if (error.message && error.message.includes('already_exists')) { - console.log('βœ… Subject mapping already exists for department/marketing'); - } else { - throw error; - } - } - - // 6. Load TDF from file - const tdfData = fs.readFileSync('encrypted.tdf'); - console.log('βœ… TDF loaded from encrypted.tdf'); - - // 7. Decrypt the data - const decryptedStream = await client.decrypt({ - source: new Uint8Array(tdfData), - }); - - const decryptedText = await new Response(decryptedStream).text(); - console.log('βœ… Data successfully decrypted'); - console.log(`πŸ“€ Decrypted content: ${decryptedText}`); - } catch (error) { - console.error('❌ Error:', error); - process.exit(1); - } -} - -main(); -``` - -
- ## Troubleshooting Having issues? See the **[SDK Troubleshooting](/sdks/troubleshooting)** guide for solutions to common problems. From 8e849b8b561114ffbbdb0ea1a96ac624232097bd Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Thu, 5 Feb 2026 07:33:16 -0800 Subject: [PATCH 11/18] address agent code review feedback Signed-off-by: Mary Dickson --- docs/sdks/quickstart/go.mdx | 1 + docs/sdks/quickstart/java.mdx | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/sdks/quickstart/go.mdx b/docs/sdks/quickstart/go.mdx index 6987c9e..2f2508c 100644 --- a/docs/sdks/quickstart/go.mdx +++ b/docs/sdks/quickstart/go.mdx @@ -245,6 +245,7 @@ To decrypt the TDF you just created, you need to grant yourself the `marketing` ```go import ( + "github.com/opentdf/platform/protocol/go/policy" "github.com/opentdf/platform/protocol/go/policy/subjectmapping" ) diff --git a/docs/sdks/quickstart/java.mdx b/docs/sdks/quickstart/java.mdx index 2b5d93e..926e16d 100644 --- a/docs/sdks/quickstart/java.mdx +++ b/docs/sdks/quickstart/java.mdx @@ -183,7 +183,10 @@ public class QuickstartApp { System.exit(1); } - // Force JVM exit (SDK background threads may not shut down immediately) + // 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); } } From 82847efb5f1f8ef03830201fd2d5c267bd77e64b Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Thu, 5 Feb 2026 13:44:45 -0800 Subject: [PATCH 12/18] go back to .clientId and read to get quickstart guide working Signed-off-by: Mary Dickson --- docs/getting-started/quickstart.mdx | 28 +++++++++++++++++++--------- docs/sdks/quickstart/go.mdx | 18 ++++++++++++++++++ docs/sdks/quickstart/java.mdx | 24 ++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/docs/getting-started/quickstart.mdx b/docs/getting-started/quickstart.mdx index 89ec832..7e02022 100644 --- a/docs/getting-started/quickstart.mdx +++ b/docs/getting-started/quickstart.mdx @@ -419,7 +419,7 @@ A profile stores your platform connection details and authentication credentials 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. ```shell -otdfctl profile create platform https://platform.opentdf.local:8443 +otdfctl profile create --tls-no-verify platform https://platform.opentdf.local:8443 ``` Expected output: @@ -444,11 +444,11 @@ Expected output: 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. -#### Step 7: Trust SSL Certificate (If Needed) +#### Step 7: Trust SSL Certificate (Optional) -The installer attempts to import Caddy's self-signed certificate automatically, but this may fail without proper permissions. +The profile was created with `--tls-no-verify` to skip certificate validation for the CLI. This is sufficient for quickstart testing, but for production use or if you're using browsers/SDKs, you may want to trust the certificate properly. -**Symptoms of untrusted certificate:** +**Symptoms indicating you should trust the certificate:** - Browser shows "Not Secure" warning when visiting platform URLs - curl commands require `-k` flag - SDK applications show certificate validation errors @@ -456,7 +456,7 @@ The installer attempts to import Caddy's self-signed certificate automatically, **Solution:** Follow the [TLS Certificate Verification](/getting-started/managing-platform#tls-certificate-verification) guide for detailed instructions on: - Extracting the Caddy root CA certificate - Trusting it on your OS (macOS, Linux, or Windows) -- Using the `OTDFCTL_TLS_NO_VERIFY` environment variable as a workaround +- When to use `--tls-no-verify` vs properly trusting certificates :::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. @@ -483,6 +483,16 @@ 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. +:::tip Troubleshooting TLS Certificate Errors +If you see a TLS certificate verification error during authentication (e.g., "x509: certificate is not trusted"), your profile may have been created without the `--tls-no-verify` flag. To fix this: + +1. Set another profile as default: `otdfctl profile set-default ` +2. Delete the problematic profile: `otdfctl profile delete platform` +3. Recreate with TLS verification disabled: `otdfctl profile create --tls-no-verify platform https://platform.opentdf.local:8443` +4. Set it as default: `otdfctl profile set-default platform` +5. Retry authentication: `otdfctl auth client-credentials opentdf secret` +::: + ## Encrypt and Decrypt with the CLI Let's start with a simple encryption example using some plain text: @@ -978,7 +988,7 @@ For this tutorial, you'll simulate the three personas by creating condition sets { "conditions": [ { - "subject_external_selector_value": ".client_id", + "subject_external_selector_value": ".clientId", "operator": 1, "subject_external_values": [ "opentdf" @@ -1064,7 +1074,7 @@ This is an authorization **decision** in action! Here's what happened: Now grant an **entitlement** to represent Jen's access level. This changes what attribute values you're entitled to: ```shell -otdfctl policy subject-mappings create --action STANDARD_ACTION_DECRYPT --attribute-value-id $EXECUTIVE_VALUE_ID --subject-condition-set-id $SUBJECT_CONDITION_SET_ID +otdfctl policy subject-mappings create --action read --attribute-value-id $EXECUTIVE_VALUE_ID --subject-condition-set-id $SUBJECT_CONDITION_SET_ID ``` > ```console @@ -1124,7 +1134,7 @@ Notice this document has TWO attributes - it requires BOTH finance department me To simulate Preston's access, grant yourself an additional **entitlement** for the finance department: ```shell -otdfctl policy subject-mappings create --action STANDARD_ACTION_DECRYPT --attribute-value-id $FINANCE_VALUE_ID --subject-condition-set-id $SUBJECT_CONDITION_SET_ID +otdfctl policy subject-mappings create --action read --attribute-value-id $FINANCE_VALUE_ID --subject-condition-set-id $SUBJECT_CONDITION_SET_ID ``` Expected output: @@ -1257,7 +1267,7 @@ otdfctl policy subject-mappings list otdfctl auth print-access-token # 3. Verify field name in condition set -# Token shows "client_id" - your condition set must use ".client_id" not ".clientId" +# 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 diff --git a/docs/sdks/quickstart/go.mdx b/docs/sdks/quickstart/go.mdx index 2f2508c..0d1b226 100644 --- a/docs/sdks/quickstart/go.mdx +++ b/docs/sdks/quickstart/go.mdx @@ -15,6 +15,24 @@ This guide covers the **Go SDK** implementation. For other languages or general - 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: diff --git a/docs/sdks/quickstart/java.mdx b/docs/sdks/quickstart/java.mdx index 926e16d..c259ead 100644 --- a/docs/sdks/quickstart/java.mdx +++ b/docs/sdks/quickstart/java.mdx @@ -16,6 +16,24 @@ This guide covers the **Java SDK** implementation. For other languages or genera - 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: @@ -69,6 +87,12 @@ Create `pom.xml`: sdk 0.12.0 + + + org.slf4j + slf4j-simple + 2.0.9 + ``` From 6bbd321080b182343b11f074179c4e22a7ea9fe2 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Thu, 5 Feb 2026 16:28:41 -0800 Subject: [PATCH 13/18] more guide improvements and bugfix Signed-off-by: Mary Dickson --- docs/getting-started/quickstart.mdx | 81 ++++++++++++++++--- docs/sdks/quickstart/go.mdx | 121 ++++++++++++++++++++++++---- docs/sdks/quickstart/index.mdx | 1 - docs/sdks/quickstart/java.mdx | 62 ++++++++++---- 4 files changed, 226 insertions(+), 39 deletions(-) diff --git a/docs/getting-started/quickstart.mdx b/docs/getting-started/quickstart.mdx index 7e02022..4cb8865 100644 --- a/docs/getting-started/quickstart.mdx +++ b/docs/getting-started/quickstart.mdx @@ -2,7 +2,6 @@ title: Quickstart slug: /quickstart sidebar_label: Quickstart -unlisted: true --- import dockerComposeContent from '!!raw-loader!./docker-compose.yaml'; @@ -321,7 +320,7 @@ If the certificate import fails during installation, you'll need to import it ma /Users/username/.opentdf/bin/otdfctl --version 4. Create an otdfctl profile: - /Users/username/.opentdf/bin/otdfctl profile create platform https://platform.opentdf.local:8443 + /Users/username/.opentdf/bin/otdfctl profile create platform-otdf-local https://platform.opentdf.local:8443 🌐 Access points: Platform: https://platform.opentdf.local:8443 @@ -419,12 +418,12 @@ A profile stores your platform connection details and authentication credentials 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. ```shell -otdfctl profile create --tls-no-verify platform https://platform.opentdf.local:8443 +otdfctl profile create --tls-no-verify platform-otdf-local https://platform.opentdf.local:8443 ``` Expected output: > ```console -> SUCCESS Profile platform created +> SUCCESS Profile platform-otdf-local created > ``` **About Default Profiles:** @@ -434,12 +433,12 @@ When you have only one profile (like most users following this guide), it automa If you have multiple profiles, you can set which one is the default: ```shell -otdfctl profile set-default platform +otdfctl profile set-default platform-otdf-local ``` Expected output: > ```console -> Set profile platform as default +> Set profile platform-otdf-local as default > ``` 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. @@ -472,7 +471,7 @@ otdfctl auth client-credentials opentdf secret Expected output: > ```console -> Client credentials set for profile [platform] +> Client credentials set for profile [platform-otdf-local] > ``` Credentials: @@ -487,9 +486,9 @@ These are test credentials provisioned automatically during installation. If you see a TLS certificate verification error during authentication (e.g., "x509: certificate is not trusted"), your profile may have been created without the `--tls-no-verify` flag. To fix this: 1. Set another profile as default: `otdfctl profile set-default ` -2. Delete the problematic profile: `otdfctl profile delete platform` -3. Recreate with TLS verification disabled: `otdfctl profile create --tls-no-verify platform https://platform.opentdf.local:8443` -4. Set it as default: `otdfctl profile set-default platform` +2. Delete the problematic profile: `otdfctl profile delete platform-otdf-local` +3. Recreate with TLS verification disabled: `otdfctl profile create --tls-no-verify platform-otdf-local https://platform.opentdf.local:8443` +4. Set it as default: `otdfctl profile set-default platform-otdf-local` 5. Retry authentication: `otdfctl auth client-credentials opentdf secret` ::: @@ -725,6 +724,62 @@ 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, create an attribute for departments. We use `ANY_OF` since a person typically belongs to one department: @@ -1073,6 +1128,12 @@ This is an authorization **decision** in action! Here's what happened: 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 ``` diff --git a/docs/sdks/quickstart/go.mdx b/docs/sdks/quickstart/go.mdx index 0d1b226..3586078 100644 --- a/docs/sdks/quickstart/go.mdx +++ b/docs/sdks/quickstart/go.mdx @@ -1,7 +1,6 @@ --- sidebar_position: 1 title: Go -unlisted: true --- # Go SDK Quickstart @@ -83,7 +82,10 @@ func main() { client, err := sdk.New( platformEndpoint, sdk.WithClientCredentials("opentdf", "secret", nil), - sdk.WithInsecurePlaintextConn(), // Only for local development! + // 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 { @@ -102,6 +104,8 @@ func main() { _, 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, @@ -184,25 +188,75 @@ For additional policy management examples including managing attributes, namespa ### 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: +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" ) -// Get the existing department attribute -attrFqn := "https://opentdf.io/attr/department" -getAttrResp, err := client.Attributes.GetAttribute(context.Background(), - &attributes.GetAttributeRequest{ - Fqn: attrFqn, +// First, ensure the namespace exists +nsResp, err := client.Namespaces.CreateNamespace(context.Background(), + &namespaces.CreateNamespaceRequest{ + Name: "opentdf.io", }) if err != nil { - log.Fatalf("Failed to get attribute: %v", err) + 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()) } -log.Printf("βœ… Found existing attribute: %s", getAttrResp.GetAttribute().GetName()) +// 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" @@ -230,6 +284,20 @@ if !valueExists { } 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 @@ -245,6 +313,8 @@ _, 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, @@ -261,6 +331,12 @@ Only users with the `department/marketing` entitlement will be able to decrypt t 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" @@ -360,14 +436,16 @@ 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: Putting it Together {#step-6-go} +## Step 6: Complete Reference Implementation {#step-6-go} -Here's a complete Go code example that brings all the pieces above together: +For reference, here's a complete example showing all the pieces together:
Create Attribute, Encrypt, Grant Access, Decrypt, Save File @@ -396,7 +474,10 @@ func main() { client, err := sdk.New( platformEndpoint, sdk.WithClientCredentials("opentdf", "secret", nil), - sdk.WithInsecurePlaintextConn(), + // 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) @@ -462,6 +543,8 @@ func main() { 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, @@ -481,7 +564,17 @@ func main() { log.Printf("βœ… TDF saved to encrypted.tdf") // 5. Grant yourself access to the marketing attribute - attributeValueId := attrResp.GetAttribute().GetValues()[0].GetId() + // 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{ diff --git a/docs/sdks/quickstart/index.mdx b/docs/sdks/quickstart/index.mdx index 0957496..8b00bc0 100644 --- a/docs/sdks/quickstart/index.mdx +++ b/docs/sdks/quickstart/index.mdx @@ -1,7 +1,6 @@ --- sidebar_position: 2 title: SDK Quickstart -unlisted: true --- import Tabs from '@theme/Tabs'; diff --git a/docs/sdks/quickstart/java.mdx b/docs/sdks/quickstart/java.mdx index c259ead..a1a3413 100644 --- a/docs/sdks/quickstart/java.mdx +++ b/docs/sdks/quickstart/java.mdx @@ -1,7 +1,6 @@ --- sidebar_position: 2 title: Java -unlisted: true --- # Java SDK Quickstart @@ -93,6 +92,12 @@ Create `pom.xml`: slf4j-simple 2.0.9 + + + io.github.hakky54 + sslcontext-kickstart + 9.2.1 + ``` @@ -141,7 +146,10 @@ public class QuickstartApp { System.out.println("πŸ“‘ Connecting to platform: " + platformEndpoint); - // Create SSL factory that trusts all certificates (DEV ONLY - DO NOT USE IN PRODUCTION) + // 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() @@ -161,6 +169,8 @@ public class QuickstartApp { 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"; @@ -470,6 +480,12 @@ Only users with the `department/marketing` entitlement will be able to decrypt t 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; @@ -578,9 +594,9 @@ try (FileChannel inputChannel = FileChannel.open( } ``` -## Step 6: Putting it Together {#step-6-java} +## Step 6: Complete Reference Implementation {#step-6-java} -Here's a complete Java code example that brings all the pieces together: +For reference, here's a complete example showing all the pieces together:
Create Attribute, Encrypt, Grant Access, and Decrypt @@ -623,18 +639,23 @@ public class QuickstartApp { String clientSecret = "secret"; String platformEndpoint = "https://platform.opentdf.local:8443"; - // Create SSL factory that trusts all certificates (DEV ONLY - DO NOT USE IN PRODUCTION) + // 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) - try (SDK sdk = new SDKBuilder() - .platformEndpoint(platformEndpoint) + 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"; @@ -740,7 +761,17 @@ public class QuickstartApp { System.out.println("βœ… TDF saved to encrypted.tdf"); // 5. Grant yourself access to the marketing attribute - String attributeValueId = attrResponse.getAttribute().getValues(0).getId(); + // 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") @@ -791,17 +822,20 @@ public class QuickstartApp { } } - // 6. Load TDF from file - byte[] tdfData = Files.readAllBytes(Path.of("encrypted.tdf")); + // 6. Load TDF from file and decrypt System.out.println("βœ… TDF loaded from encrypted.tdf"); - // 7. Decrypt the data - var decryptedStream = new ByteArrayOutputStream(); - var reader = sdk.loadTDF(new ByteArrayInputStream(tdfData)); - reader.readPayload(decryptedStream); + 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) From 6b918e164a046a39160ea1840c0ade386abceac6 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Fri, 6 Feb 2026 07:50:20 -0800 Subject: [PATCH 14/18] move some content out of main quickstart guide Signed-off-by: Mary Dickson --- docs/getting-started/managing-platform.mdx | 177 ++++++++++++++++++ docs/getting-started/quickstart.mdx | 199 ++------------------- docs/sdks/quickstart/index.mdx | 4 +- 3 files changed, 198 insertions(+), 182 deletions(-) diff --git a/docs/getting-started/managing-platform.mdx b/docs/getting-started/managing-platform.mdx index 8149646..3aefe54 100644 --- a/docs/getting-started/managing-platform.mdx +++ b/docs/getting-started/managing-platform.mdx @@ -106,3 +106,180 @@ 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 + +```shell +# Configure in ~/.opentdf/platform/.env if needed +PLATFORM_PORT=8443 +KEYCLOAK_PORT=9443 +CADDY_ADMIN_PORT=2019 +POSTGRES_PORT=5432 +``` + +### 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 4cb8865..2e72e40 100644 --- a/docs/getting-started/quickstart.mdx +++ b/docs/getting-started/quickstart.mdx @@ -1277,191 +1277,30 @@ In production, you would: - Understand [policy management](/components/policy) for access control - Build [attribute hierarchies](/components/authorization) for complex organizational structures -## Troubleshooting +## Need Help? -:::note Troubleshooting Scope -This section covers issues specific to **platform installation and setup**. For SDK-specific errors when writing code, see [SDK Troubleshooting](/sdks/troubleshooting). -::: - -### "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 in Step 1, then restart the platform - -### "PermissionDenied" After Creating Subject Mapping +**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 -**Symptom:** Subject mapping created but decryption still fails with `PermissionDenied` +For SDK-specific errors when writing code, see [SDK Troubleshooting](/sdks/troubleshooting). -**Investigation steps:** +## Next 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 -``` - -### TLS/Certificate Warnings - -**Symptom:** Browser shows "Not Secure", curl/SDK commands show certificate validation errors - -**Solution:** See [TLS Certificate Verification](/getting-started/managing-platform#tls-certificate-verification) for complete instructions on trusting the Caddy root CA certificate - -### 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 - -```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 STANDARD_ACTION_DECRYPT \ - --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 - -```shell -# Configure in ~/.opentdf/platform/.env if needed -PLATFORM_PORT=8443 -KEYCLOAK_PORT=9443 -CADDY_ADMIN_PORT=2019 -POSTGRES_PORT=5432 -``` +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. -### Access Points +**Continue your journey:** -- Platform API: https://platform.opentdf.local:8443 -- Platform Health: https://platform.opentdf.local:8443/healthz -- Keycloak: https://keycloak.opentdf.local:9443 +πŸ“š **[SDK Quickstart Guide](/sdks/quickstart)** - Learn how to encrypt and decrypt data in your Go, Java, or JavaScript applications -### Default Test Credentials +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 -- Client ID: `opentdf` -- Client Secret: `secret` -- Username: `user1` -- Password: `testuser123` +**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/sdks/quickstart/index.mdx b/docs/sdks/quickstart/index.mdx index 8b00bc0..a9a114a 100644 --- a/docs/sdks/quickstart/index.mdx +++ b/docs/sdks/quickstart/index.mdx @@ -107,8 +107,8 @@ The SDKs create **TDF (Trusted Data Format)** files, which contain: :::warning Development Only - Language-Specific Settings The SDK examples use different settings for local development with self-signed certificates: -**Go SDK**: `.WithInsecurePlaintextConn()` -**Java SDK**: `.useInsecurePlaintextConnection(true)` +**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.** From f6ffb0984ac71453c11b3e16704ae98b53b08889 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Fri, 6 Feb 2026 08:12:16 -0800 Subject: [PATCH 15/18] streamline quickstart text Signed-off-by: Mary Dickson --- docs/getting-started/managing-platform.mdx | 1 - docs/getting-started/quickstart.mdx | 372 ++++----------------- static/quickstart/check.sh | 3 +- 3 files changed, 70 insertions(+), 306 deletions(-) diff --git a/docs/getting-started/managing-platform.mdx b/docs/getting-started/managing-platform.mdx index 3aefe54..2fc73ae 100644 --- a/docs/getting-started/managing-platform.mdx +++ b/docs/getting-started/managing-platform.mdx @@ -1,6 +1,5 @@ --- sidebar_position: 3 -unlisted: true --- # Managing the Platform diff --git a/docs/getting-started/quickstart.mdx b/docs/getting-started/quickstart.mdx index 2e72e40..86aa4b4 100644 --- a/docs/getting-started/quickstart.mdx +++ b/docs/getting-started/quickstart.mdx @@ -2,6 +2,7 @@ title: Quickstart slug: /quickstart sidebar_label: Quickstart +pagination_next: sdks/quickstart/index --- import dockerComposeContent from '!!raw-loader!./docker-compose.yaml'; @@ -144,43 +145,9 @@ You'll run a local instance using Docker. This includes the [Platform](https://g Not for production use. ::: -#### Step 1: Check for Port Conflicts (Recommended) +#### Step 1: Check Prerequisites -OpenTDF uses these ports by default: -- **8443**: Platform API -- **9443**: Keycloak authentication -- **2019**: Caddy admin -- **5432**: PostgreSQL - -Port 9443 commonly conflicts with other services (Rancher, steve, Kubernetes dashboards). Before installing, check if these ports are available: - -```shell -# Check for port conflicts -lsof -i :9443 # Most common conflict -lsof -i :8443 -lsof -i :5432 -``` - -If ports are in use, configure OpenTDF to use different ports before installation: - -```shell -# Create config directory -mkdir -p ~/.opentdf/platform - -# Set custom ports -cat > ~/.opentdf/platform/.env <<'EOF' -PLATFORM_PORT=8443 -KEYCLOAK_PORT=9444 -CADDY_ADMIN_PORT=2019 -POSTGRES_PORT=5432 -EOF -``` - -Change `KEYCLOAK_PORT` to an available port if 9443 is in use. - -#### Step 2: Run Pre-flight Check (Optional) - -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 @@ -206,7 +173,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 @@ -215,12 +182,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 +βœ“ Port 5432 is available Checking permissions... βœ“ sudo access available (will prompt for password) @@ -236,7 +204,19 @@ Ready to install OpenTDF:
-#### Step 3: Install OpenTDF +The script checks: +- Docker is installed and running +- Docker Compose is available +- Required ports are available (8443, 9443, 2019, 5432) +- 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: @@ -344,152 +324,76 @@ If the certificate import fails during installation, you'll need to import it ma
-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 - -You'll be prompted for your password twice: -1. To add entries to /etc/hosts -2. To import the SSL certificate +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 -#### Step 4: Add otdfctl to Your PATH (Optional) +You'll be prompted for your password twice: once for /etc/hosts and once for SSL certificate import. -Choose one option: +**Add otdfctl to your PATH** (optional but recommended): -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 5: 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) -> ``` +Expected: version output and `{"status":"SERVING"}` from the healthz check. -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/ +#### Step 3: Create Profile & Authenticate -When you visit the platform healthz endpoint, you should see: - -Expected output: -```json -{"status":"SERVING"} -``` - -When you visit the Keycloak URL, seeing "Resource not found" is expected and indicates Keycloak is running correctly. - -#### Step 6: Create an otdfctl Profile - -A profile stores your platform connection details and authentication credentials, making commands shorter and easier to use. - -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. +Create a profile to store your connection details, then authenticate: ```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 + +# Authenticate with test credentials +otdfctl auth client-credentials opentdf secret ``` Expected output: > ```console > SUCCESS Profile platform-otdf-local created +> Client credentials set for profile [platform-otdf-local] > ``` -**About Default Profiles:** - -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. +**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. -If you have multiple profiles, you can set which one is the default: +Your credentials are saved in the profile and persist across commandsβ€”no need to re-authenticate unless you change profiles. -```shell -otdfctl profile set-default platform-otdf-local -``` - -Expected output: -> ```console -> Set profile platform-otdf-local as default -> ``` +#### Step 4: Trust SSL Certificate (Optional) -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. +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. -#### Step 7: Trust SSL Certificate (Optional) +**Trust the certificate if you see:** +- Browser "Not Secure" warnings +- curl commands requiring `-k` flag +- SDK certificate validation errors -The profile was created with `--tls-no-verify` to skip certificate validation for the CLI. This is sufficient for quickstart testing, but for production use or if you're using browsers/SDKs, you may want to trust the certificate properly. - -**Symptoms indicating you should trust the certificate:** -- Browser shows "Not Secure" warning when visiting platform URLs -- curl commands require `-k` flag -- SDK applications show certificate validation errors - -**Solution:** Follow the [TLS Certificate Verification](/getting-started/managing-platform#tls-certificate-verification) guide for detailed instructions on: -- Extracting the Caddy root CA certificate -- Trusting it on your OS (macOS, Linux, or Windows) -- When to use `--tls-no-verify` vs properly trusting certificates +**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. -::: - -## 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. - -:::tip Troubleshooting TLS Certificate Errors -If you see a TLS certificate verification error during authentication (e.g., "x509: certificate is not trusted"), your profile may have been created without the `--tls-no-verify` flag. To fix this: - -1. Set another profile as default: `otdfctl profile set-default ` -2. Delete the problematic profile: `otdfctl profile delete platform-otdf-local` -3. Recreate with TLS verification disabled: `otdfctl profile create --tls-no-verify platform-otdf-local https://platform.opentdf.local:8443` -4. Set it as default: `otdfctl profile set-default platform-otdf-local` -5. Retry authentication: `otdfctl auth client-credentials opentdf secret` +Need to start, stop, or restart the platform later? See the [Managing the Platform](/getting-started/managing-platform) guide. ::: ## Encrypt and Decrypt with the CLI @@ -811,79 +715,21 @@ 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 -``` - -> ```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: +export 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: - -```shell -export ENGINEERING_VALUE_ID= -``` - -### Review Your Department Attributes - -Now that you've created the department attribute with its values, let's verify it: - -```shell -otdfctl policy attributes get --id=$DEPT_ATTRIBUTE_ID -``` - -> ```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 -> ``` +:::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` +::: ### Create Clearance Attribute @@ -905,27 +751,7 @@ In the next few steps you will add an executive clearance value and a standard c ```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 @@ -933,78 +759,16 @@ export CLEARANCE_ATTRIBUTE_ID= 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 -> ``` +export EXECUTIVE_VALUE_ID= -Save the executive clearance 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 diff --git a/static/quickstart/check.sh b/static/quickstart/check.sh index 01b44aa..545ce43 100644 --- a/static/quickstart/check.sh +++ b/static/quickstart/check.sh @@ -151,9 +151,10 @@ check_port() { fi } -check_port 8080 || true check_port 8443 || true check_port 9443 || true +check_port 2019 || true +check_port 5432 || true echo "" # Check for sudo access (needed for /etc/hosts) From 40ba4c206d4a3b7a6b96dc715a6532a60082088b Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Fri, 6 Feb 2026 09:02:44 -0800 Subject: [PATCH 16/18] update links and site nav Signed-off-by: Mary Dickson --- docs/getting-started/index.mdx | 4 ++ docs/getting-started/managing-platform.mdx | 2 +- docs/getting-started/quickstart.mdx | 3 +- docs/introduction.mdx | 5 +- docs/sdks/_category_.json | 4 +- docs/sdks/index.mdx | 55 ++++++++++++++++++++++ docs/sdks/overview.mdx | 1 + docs/sdks/quickstart/index.mdx | 2 +- docs/sdks/quickstart/javascript.mdx | 1 - 9 files changed, 69 insertions(+), 8 deletions(-) create mode 100644 docs/sdks/index.mdx diff --git a/docs/getting-started/index.mdx b/docs/getting-started/index.mdx index d8bb51a..e08fdf6 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 2fc73ae..907d6d6 100644 --- a/docs/getting-started/managing-platform.mdx +++ b/docs/getting-started/managing-platform.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 3 +sidebar_position: 2 --- # Managing the Platform diff --git a/docs/getting-started/quickstart.mdx b/docs/getting-started/quickstart.mdx index 86aa4b4..b7b4e9c 100644 --- a/docs/getting-started/quickstart.mdx +++ b/docs/getting-started/quickstart.mdx @@ -2,6 +2,7 @@ title: Quickstart slug: /quickstart sidebar_label: Quickstart +sidebar_position: 1 pagination_next: sdks/quickstart/index --- @@ -1036,7 +1037,7 @@ 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 diff --git a/docs/introduction.mdx b/docs/introduction.mdx index d42adf0..4e1e4fa 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 8da194e..b1c39d0 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 0000000..d34d539 --- /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 ffb88ae..464639f 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/index.mdx b/docs/sdks/quickstart/index.mdx index a9a114a..d44add9 100644 --- a/docs/sdks/quickstart/index.mdx +++ b/docs/sdks/quickstart/index.mdx @@ -76,7 +76,7 @@ OpenTDF provides native SDKs in three languages. Choose your language to get sta diff --git a/docs/sdks/quickstart/javascript.mdx b/docs/sdks/quickstart/javascript.mdx index 38c893e..c7b3992 100644 --- a/docs/sdks/quickstart/javascript.mdx +++ b/docs/sdks/quickstart/javascript.mdx @@ -1,7 +1,6 @@ --- sidebar_position: 3 title: JavaScript/TypeScript -unlisted: true --- # JavaScript/TypeScript SDK Quickstart From adecf86c9d73511dc0d1a8f4e13fd705f071f491 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Fri, 6 Feb 2026 09:07:09 -0800 Subject: [PATCH 17/18] fix check script ports Signed-off-by: Mary Dickson --- tests/quickstart.bats | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/quickstart.bats b/tests/quickstart.bats index bf4e907..8e6996e 100644 --- a/tests/quickstart.bats +++ b/tests/quickstart.bats @@ -82,14 +82,17 @@ setup() { # Port checking tests @test "check.sh port validation works" { - run bash -c "grep -q 'check_port.*8080' $SCRIPT_DIR/check.sh" - [ "$status" -eq 0 ] - run bash -c "grep -q 'check_port.*8443' $SCRIPT_DIR/check.sh" [ "$status" -eq 0 ] 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 ] + + run bash -c "grep -q 'check_port.*5432' $SCRIPT_DIR/check.sh" + [ "$status" -eq 0 ] } # Script structure tests From 15ff0cc65faac7c17ed3e9dd3dd77abdd374353a Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Fri, 6 Feb 2026 15:20:06 -0800 Subject: [PATCH 18/18] clarify port requirements and address CR comments Signed-off-by: Mary Dickson --- docs/getting-started/managing-platform.mdx | 13 ++- docs/getting-started/quickstart.mdx | 101 ++++++++++++++++++++- docusaurus.config.ts | 2 +- static/quickstart/check.sh | 2 +- tests/quickstart.bats | 6 +- 5 files changed, 113 insertions(+), 11 deletions(-) diff --git a/docs/getting-started/managing-platform.mdx b/docs/getting-started/managing-platform.mdx index 907d6d6..0c76873 100644 --- a/docs/getting-started/managing-platform.mdx +++ b/docs/getting-started/managing-platform.mdx @@ -262,12 +262,21 @@ 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 -# Configure in ~/.opentdf/platform/.env if needed +PLATFORM_HTTP_PORT=8080 PLATFORM_PORT=8443 KEYCLOAK_PORT=9443 CADDY_ADMIN_PORT=2019 -POSTGRES_PORT=5432 ``` ### Access Points diff --git a/docs/getting-started/quickstart.mdx b/docs/getting-started/quickstart.mdx index b7b4e9c..9fa5aee 100644 --- a/docs/getting-started/quickstart.mdx +++ b/docs/getting-started/quickstart.mdx @@ -142,8 +142,16 @@ The OpenTDF Platform is the core service that enforces attribute-based access co 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: Check Prerequisites @@ -161,6 +169,10 @@ 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 @@ -186,10 +198,10 @@ Checking disk space... βœ“ Disk space: 50GB available Checking port availability... +βœ“ Port 8080 is available βœ“ Port 8443 is available βœ“ Port 9443 is available βœ“ Port 2019 is available -βœ“ Port 5432 is available Checking permissions... βœ“ sudo access available (will prompt for password) @@ -208,7 +220,7 @@ Ready to install OpenTDF: The script checks: - Docker is installed and running - Docker Compose is available -- Required ports are available (8443, 9443, 2019, 5432) +- Required ports are available (8080, 8443, 9443, 2019) - Sufficient disk space (10GB+) and RAM (4GB+) - curl is installed - sudo access for /etc/hosts modification @@ -372,6 +384,17 @@ otdfctl profile create --tls-no-verify platform-otdf-local https://platform.open otdfctl auth client-credentials opentdf secret ``` +:::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 --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 @@ -425,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: diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 5cad766..cb8d5e4 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", }, ], }, diff --git a/static/quickstart/check.sh b/static/quickstart/check.sh index 545ce43..ac3764c 100644 --- a/static/quickstart/check.sh +++ b/static/quickstart/check.sh @@ -151,10 +151,10 @@ check_port() { fi } +check_port 8080 || true check_port 8443 || true check_port 9443 || true check_port 2019 || true -check_port 5432 || true echo "" # Check for sudo access (needed for /etc/hosts) diff --git a/tests/quickstart.bats b/tests/quickstart.bats index 8e6996e..64b34d6 100644 --- a/tests/quickstart.bats +++ b/tests/quickstart.bats @@ -82,6 +82,9 @@ setup() { # Port checking tests @test "check.sh port validation works" { + run bash -c "grep -q 'check_port.*8080' $SCRIPT_DIR/check.sh" + [ "$status" -eq 0 ] + run bash -c "grep -q 'check_port.*8443' $SCRIPT_DIR/check.sh" [ "$status" -eq 0 ] @@ -90,9 +93,6 @@ setup() { run bash -c "grep -q 'check_port.*2019' $SCRIPT_DIR/check.sh" [ "$status" -eq 0 ] - - run bash -c "grep -q 'check_port.*5432' $SCRIPT_DIR/check.sh" - [ "$status" -eq 0 ] } # Script structure tests