Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 143 additions & 33 deletions sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -16,47 +18,155 @@ 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 := "opentdf" // 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` | `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) |

**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"

// 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,
}
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
Expand Down
161 changes: 161 additions & 0 deletions sdk/readme_test.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading