From 16c6bad63ec06a130dba2ea28aa1ad7c05e5a8b2 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Fri, 6 Feb 2026 15:51:10 -0800 Subject: [PATCH 1/3] replace code example with working code Signed-off-by: Mary Dickson --- sdk/README.md | 170 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 137 insertions(+), 33 deletions(-) diff --git a/sdk/README.md b/sdk/README.md index c5a4f05bdb..ee4e59e6ac 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -3,6 +3,8 @@ A Go implementation of the OpenTDF protocol, and access library for services included in the Data Security Platform. +**New to the OpenTDF SDK?** See the [OpenTDF SDK Quickstart Guide](https://opentdf.io/category/sdk) for a comprehensive introduction. + Note: if you are consuming the SDK as a submodule you may need to add replace directives as follows: ```go @@ -16,47 +18,149 @@ replace ( ) ``` -## Quick Start of the Go SDK +## Example code + +This example demonstrates how to create and read TDF (Trusted Data Format) files using the OpenTDF SDK. + +**Prerequisites:** Follow the [OpenTDF Quickstart](https://opentdf.io/quickstart) to get a local platform running, or if you already have a hosted version, replace the values with your OpenTDF platform details. + +For more code examples, see: +- [Creating TDFs](https://opentdf.io/sdks/tdf) +- [Managing policy](https://opentdf.io/sdks/policy) ```go package main -import "fmt" -import "bytes" -import "io" -import "os" -import "strings" -import "github.com/opentdf/platform/sdk" +import ( + "bytes" + "fmt" + "io" + "log" + "os" + "strings" + "github.com/opentdf/platform/sdk" +) func main() { - s, _ := sdk.New( - sdk.WithAuth(mtls.NewGRPCAuthorizer(creds) /* or OIDC or whatever */), - sdk.WithDataSecurityConfig(/* attribute schemas, kas multi-attribute mapping */), - ) - - plaintext := strings.NewReader("Hello, world!") - var ciphertext bytes.Buffer - _, err := s.CreateTDF( - ciphertext, - plaintext, - sdk.WithDataAttributes("https://example.com/attr/Classification/value/Open"), - ) - if err != nil { - panic(err) - } - - fmt.Printf("Ciphertext is %s bytes long", ciphertext.Len()) - - ct2 := make([]byte, ciphertext.Len()) - copy(ct2, ciphertext.Bytes()) - r, err := s.NewTDFReader(bytes.NewReader(ct2)) - f, err := os.Create("output.txt") - if err != nil { - panic(err) - } - io.Copy(f, r) + // Initialize SDK with platform endpoint and authentication + // Replace these values with your actual configuration: + platformEndpoint := "http://localhost:8080" // Your platform URL + clientID := "tdf-client" // Your OAuth client ID + clientSecret := "secret" // Your OAuth client secret + keycloakURL := "http://localhost:8888/auth/realms/opentdf" // Your Keycloak realm URL + + s, err := sdk.New( + platformEndpoint, + sdk.WithClientCredentials(clientID, clientSecret, []string{"email", "profile"}), + sdk.WithPlatformConfiguration(sdk.PlatformConfiguration{ + "platform_issuer": keycloakURL, + }), + sdk.WithInsecurePlaintextConn(), // Only for local development with HTTP + ) + if err != nil { + log.Fatalf("Failed to create SDK: %v", err) + } + defer s.Close() + + // Create a TDF + // This attribute is created in the quickstart guide + dataAttribute := "https://opentdf.io/attr/department/value/finance" + + plaintext := strings.NewReader("Hello, world!") + var ciphertext bytes.Buffer + _, err = s.CreateTDF( + &ciphertext, + plaintext, + sdk.WithDataAttributes(dataAttribute), + ) + if err != nil { + log.Fatalf("Failed to create TDF: %v", err) + } + + fmt.Printf("Ciphertext is %d bytes long\n", ciphertext.Len()) + + // Decrypt the TDF + // LoadTDF contacts the Key Access Service (KAS) to verify that this client + // has been granted access to the data attributes, then decrypts the TDF. + // Note: The client must have entitlements configured on the platform first. + r, err := s.LoadTDF(bytes.NewReader(ciphertext.Bytes())) + if err != nil { + log.Fatalf("Failed to load TDF: %v", err) + } + + // Write the decrypted plaintext to a file + f, err := os.Create("output.txt") + if err != nil { + log.Fatalf("Failed to create output file: %v", err) + } + defer f.Close() + + _, err = io.Copy(f, r) + if err != nil { + log.Fatalf("Failed to write decrypted content: %v", err) + } + + fmt.Println("Successfully created and decrypted TDF") +} +``` + +### Configuration Values + +Replace these placeholder values with your actual configuration: + +| Variable | Default (Quickstart) | Description | +|----------|---------------------|-------------| +| `platformEndpoint` | `http://localhost:8080` | Your OpenTDF platform URL | +| `clientID` | `tdf-client` | OAuth client ID (create in Keycloak) | +| `clientSecret` | `secret` | OAuth client secret (from Keycloak) | +| `keycloakURL` | `http://localhost:8888/auth/realms/opentdf` | Your Keycloak realm URL | +| `dataAttribute` | `https://opentdf.io/attr/department/value/finance` | Data attribute FQN (created in quickstart) | + +**Before running:** +1. Follow the [OpenTDF Quickstart](https://opentdf.io/quickstart) to start the platform +2. Create an OAuth client in Keycloak and note the credentials +3. Grant your client entitlements to the `department` attribute (see [Managing policy](https://opentdf.io/sdks/policy)) + +**Expected Output:** +``` +Ciphertext is 1234 bytes long +Successfully created and decrypted TDF +``` + +The `output.txt` file will contain the decrypted plaintext: `Hello, world!` + +### Authentication Options + +The SDK supports multiple authentication methods: + +**Client Credentials (OAuth 2.0):** +```go +sdk.WithClientCredentials("client-id", "client-secret", []string{"scope1", "scope2"}) +``` + +**TLS/mTLS Authentication:** +```go +import "crypto/tls" + +tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + MinVersion: tls.VersionTLS12, } +sdk.WithTLSCredentials(tlsConfig, []string{"audience1", "audience2"}) +``` + +**Custom OAuth2 Token Source:** +```go +import "golang.org/x/oauth2" + +tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: "your-token"}) +sdk.WithOAuthAccessTokenSource(tokenSource) +``` + +**Token Exchange:** +```go +sdk.WithTokenExchange("subject-token", []string{"audience1", "audience2"}) ``` ## Base key From 6e4a416810b237bb28da53de6dbbe75663bcfc0f Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Fri, 6 Feb 2026 16:03:47 -0800 Subject: [PATCH 2/3] add readme test for complete code blocks Signed-off-by: Mary Dickson --- sdk/README.md | 6 +- sdk/readme_test.go | 161 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 3 deletions(-) create mode 100644 sdk/readme_test.go diff --git a/sdk/README.md b/sdk/README.md index ee4e59e6ac..b6b4888041 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -46,7 +46,7 @@ func main() { // Initialize SDK with platform endpoint and authentication // Replace these values with your actual configuration: platformEndpoint := "http://localhost:8080" // Your platform URL - clientID := "tdf-client" // Your OAuth client ID + clientID := "opentdf" // Your OAuth client ID clientSecret := "secret" // Your OAuth client secret keycloakURL := "http://localhost:8888/auth/realms/opentdf" // Your Keycloak realm URL @@ -112,8 +112,8 @@ Replace these placeholder values with your actual configuration: | Variable | Default (Quickstart) | Description | |----------|---------------------|-------------| | `platformEndpoint` | `http://localhost:8080` | Your OpenTDF platform URL | -| `clientID` | `tdf-client` | OAuth client ID (create in Keycloak) | -| `clientSecret` | `secret` | OAuth client secret (from Keycloak) | +| `clientID` | `opentdf` | OAuth client ID (from quickstart) | +| `clientSecret` | `secret` | OAuth client secret (from quickstart) | | `keycloakURL` | `http://localhost:8888/auth/realms/opentdf` | Your Keycloak realm URL | | `dataAttribute` | `https://opentdf.io/attr/department/value/finance` | Data attribute FQN (created in quickstart) | diff --git a/sdk/readme_test.go b/sdk/readme_test.go new file mode 100644 index 0000000000..dcb948a77a --- /dev/null +++ b/sdk/readme_test.go @@ -0,0 +1,161 @@ +package sdk_test + +import ( + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "testing" +) + +// TestREADMECodeBlocks verifies that all Go code blocks in the README compile successfully. +// This ensures the documentation stays accurate and up-to-date with the actual API. +func TestREADMECodeBlocks(t *testing.T) { + // Read the README file + readmePath := filepath.Join("..", "sdk", "README.md") + content, err := os.ReadFile(readmePath) + if err != nil { + t.Fatalf("Failed to read README.md: %v", err) + } + + // Extract Go code blocks + codeBlocks := extractGoCodeBlocks(string(content)) + if len(codeBlocks) == 0 { + t.Fatal("No Go code blocks found in README.md") + } + + t.Logf("Found %d Go code block(s) in README.md", len(codeBlocks)) + + // Test each code block that is a complete program + testedCount := 0 + for i, code := range codeBlocks { + // Only test complete programs (those with package main) + if !strings.Contains(code, "package main") { + t.Logf("Skipping code block %d (not a complete program)", i+1) + continue + } + + testedCount++ + t.Run(formatBlockName(i, code), func(t *testing.T) { + if err := testCodeBlock(t, code); err != nil { + t.Errorf("Code block %d failed to compile:\n%v", i+1, err) + } + }) + } + + if testedCount == 0 { + t.Fatal("No complete program code blocks found in README.md") + } + t.Logf("Tested %d complete program(s)", testedCount) +} + +// extractGoCodeBlocks finds all Go code blocks in the markdown content. +func extractGoCodeBlocks(content string) []string { + // Match code blocks that start with ```go and end with ``` + re := regexp.MustCompile("(?s)```go\n(.*?)```") + matches := re.FindAllStringSubmatch(content, -1) + + blocks := make([]string, 0, len(matches)) + for _, match := range matches { + if len(match) > 1 { + blocks = append(blocks, match[1]) + } + } + return blocks +} + +// formatBlockName creates a readable test name from the code block. +func formatBlockName(index int, code string) string { + lines := strings.Split(strings.TrimSpace(code), "\n") + if len(lines) == 0 { + return "empty_block" + } + + // Try to find a meaningful identifier in the first few lines + for _, line := range lines[:min(5, len(lines))] { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "package ") { + return strings.TrimPrefix(line, "package ") + } + if strings.HasPrefix(line, "func ") { + return strings.Fields(line)[1] + } + } + + return "code_block_" + string(rune('A'+index)) +} + +// testCodeBlock attempts to compile a code block. +func testCodeBlock(t *testing.T, code string) error { + // Create a temporary directory + tmpDir, err := os.MkdirTemp("", "readme-test-*") + if err != nil { + return err + } + defer os.RemoveAll(tmpDir) + + // Write the code to main.go + mainPath := filepath.Join(tmpDir, "main.go") + if err := os.WriteFile(mainPath, []byte(code), 0644); err != nil { + return err + } + + // Initialize go module + cmd := exec.Command("go", "mod", "init", "example") + cmd.Dir = tmpDir + if output, err := cmd.CombinedOutput(); err != nil { + t.Logf("go mod init output: %s", output) + return err + } + + // Get the absolute path to the platform directory + // When running from sdk directory, we need to go up one level + platformDir, err := filepath.Abs(filepath.Join("..")) + if err != nil { + return err + } + + // Add replace directives for local modules + replacements := []string{ + "github.com/opentdf/platform/sdk=" + filepath.Join(platformDir, "sdk"), + "github.com/opentdf/platform/lib/ocrypto=" + filepath.Join(platformDir, "lib/ocrypto"), + "github.com/opentdf/platform/protocol/go=" + filepath.Join(platformDir, "protocol/go"), + } + + for _, replace := range replacements { + cmd := exec.Command("go", "mod", "edit", "-replace", replace) + cmd.Dir = tmpDir + if output, err := cmd.CombinedOutput(); err != nil { + t.Logf("go mod edit output: %s", output) + return err + } + } + + // Run go mod tidy + cmd = exec.Command("go", "mod", "tidy") + cmd.Dir = tmpDir + if output, err := cmd.CombinedOutput(); err != nil { + t.Logf("go mod tidy output: %s", output) + return err + } + + // Attempt to build + cmd = exec.Command("go", "build", "-o", "/dev/null", "main.go") + cmd.Dir = tmpDir + output, err := cmd.CombinedOutput() + if err != nil { + t.Logf("Build output:\n%s", output) + return err + } + + t.Logf("Code block compiled successfully") + return nil +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} From 639e881a2b0f61de80b02a1e1f47826009bb5525 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Fri, 6 Feb 2026 16:11:38 -0800 Subject: [PATCH 3/3] clarify cert needs Signed-off-by: Mary Dickson --- sdk/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sdk/README.md b/sdk/README.md index b6b4888041..fc3ab82c21 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -143,6 +143,12 @@ sdk.WithClientCredentials("client-id", "client-secret", []string{"scope1", "scop ```go import "crypto/tls" +// Load your client certificate and key +cert, err := tls.LoadX509KeyPair("client.crt", "client.key") +if err != nil { + log.Fatal(err) +} + tlsConfig := &tls.Config{ Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS12,