secretstore

package
v0.2.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 29, 2025 License: MIT Imports: 4 Imported by: 0

Documentation

Overview

Package secretstore provides interfaces and types for secret storage systems in dsops.

This package defines the modern secret store abstraction that replaces the older provider interface with a cleaner, more focused API specifically designed for secret storage operations. It represents the storage layer in dsops's architecture, handling where secrets are stored and how they are retrieved.

Architecture Overview

The secretstore package sits at the foundation of dsops's layered architecture, providing the storage abstraction upon which all other operations are built:

┌─────────────────────────────────────────────────────────────┐
│                    Application Layer                        │
│               (CLI, Config, Templates)                      │
└─────────────────────────┬───────────────────────────────────┘
                          │
┌─────────────────────────▼───────────────────────────────────┐
│                  Resolution Engine                          │
│               (internal/resolve/)                           │
└─────────────────────────┬───────────────────────────────────┘
                          │
┌─────────────────────────▼───────────────────────────────────┐
│                Secret Store Interface                       │
│                 (pkg/secretstore/)             ◄────────────┤
└─────────────────────────┬───────────────────────────────────┘
                          │
┌─────────────────────────▼───────────────────────────────────┐
│              Secret Store Implementations                   │
│              (internal/secretstores/)                       │
│                                                             │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐          │
│  │     AWS     │  │    Vault    │  │ 1Password   │  ...     │
│  │   Secrets   │  │   Store     │  │   Store     │          │
│  └─────────────┘  └─────────────┘  └─────────────┘          │
└─────────────────────────────────────────────────────────────┘

Design Philosophy

## Separation of Concerns

The secretstore package embodies dsops's core architectural principle of separating secret storage from secret consumption:

  • **Secret Stores**: Systems that store and retrieve secret values

  • AWS Secrets Manager, HashiCorp Vault, 1Password, Azure Key Vault

  • Focus: Storage, versioning, access control, retrieval

  • This package's domain

  • **Services**: Systems that consume secrets for operations

  • PostgreSQL, Stripe API, GitHub, Kubernetes clusters

  • Focus: Service configuration, rotation, lifecycle management

  • Handled by pkg/service and pkg/rotation

This separation enables:

  • Clear architectural boundaries
  • Independent evolution of storage and consumption layers
  • Flexible mixing and matching of stores and services
  • Simplified testing and maintenance

## Modern Reference System

The package introduces a modern URI-based reference system that replaces the legacy provider+key approach:

Legacy:  provider: "aws", key: "database/password"
Modern:  store://aws-prod/database/password#password?version=current

Benefits of the new system:

  • **Hierarchical addressing**: Natural path-based organization
  • **Field extraction**: Direct access to specific JSON fields
  • **Version selection**: Native support for versioned secrets
  • **Extensible options**: Store-specific parameters via query strings
  • **URL-like familiarity**: Intuitive for developers

## Capability-Driven Design

Rather than assuming all secret stores have identical capabilities, the package uses capability negotiation to adapt behavior:

  • **Feature detection**: Stores expose what they can do
  • **Graceful degradation**: Operations adapt to store limitations
  • **Future-proof**: New capabilities can be added without breaking changes
  • **User transparency**: Clear feedback about what operations are possible

Core Interface: SecretStore

The SecretStore interface defines five essential operations:

  1. **Name()**: Unique identifier for the store instance
  2. **Resolve()**: Retrieve secret values with metadata
  3. **Describe()**: Get secret metadata without retrieving values
  4. **Capabilities()**: Expose supported features and limitations
  5. **Validate()**: Verify configuration and connectivity

This minimal but complete interface enables:

  • Consistent behavior across all secret stores
  • Efficient metadata-only operations
  • Capability-aware application logic
  • Robust error handling and validation

Reference System Deep Dive

## SecretRef Structure

The SecretRef type provides structured access to URI components:

type SecretRef struct {
    Store   string            // Store instance name
    Path    string            // Path within the store
    Field   string            // Optional field extraction
    Version string            // Optional version selection
    Options map[string]string // Store-specific parameters
}

## URI Format

The complete URI format supports rich addressing:

store://store-name/path/to/secret#field?version=v&option=value

Examples across different stores:

# AWS Secrets Manager
store://aws-prod/database/credentials#password?version=AWSCURRENT

# HashiCorp Vault KV v2
store://vault/secret/data/app#api_key?version=2

# 1Password
store://onepassword/Production/Database#password?vault=Private

# Azure Key Vault
store://azure/app-secrets#connection-string?version=latest

## Parsing and Validation

The package provides robust parsing with comprehensive validation:

  • URI format validation
  • Required component checking
  • Query parameter parsing
  • Error reporting with actionable messages

Metadata and Capabilities

## SecretMetadata

The SecretMetadata type enables metadata-only operations:

  • Existence checking without value retrieval
  • Size and version information
  • Permission and tag data
  • Performance optimization for validation

## SecretStoreCapabilities

Comprehensive capability reporting enables adaptive behavior:

  • **Feature flags**: Versioning, metadata, binary support, watching
  • **Authentication**: Required methods and supported mechanisms
  • **Rotation support**: Version management and constraints
  • **Performance characteristics**: Batch operation support, caching

This information drives:

  • UI feature enablement/disabling
  • Configuration validation
  • Operation routing and optimization
  • User experience adaptation

Error Handling Strategy

The package defines a comprehensive error taxonomy:

## NotFoundError

Indicates missing secrets with precise location information:

  • Distinguishes from authentication failures
  • Enables retry logic and fallback strategies
  • Provides actionable error messages

## AuthError

Covers authentication and authorization failures:

  • Invalid credentials
  • Expired tokens
  • Insufficient permissions
  • Network authentication issues

## ValidationError

Handles malformed requests and configuration issues:

  • Invalid URI format
  • Missing required parameters
  • Constraint violations
  • Configuration errors

This structured approach enables:

  • Appropriate error handling strategies
  • Clear user feedback
  • Automated retry and recovery logic
  • Comprehensive logging and monitoring

Implementation Guidelines

## Threading and Concurrency

All SecretStore implementations must be thread-safe:

  • Multiple goroutines may call methods concurrently
  • Internal state must be properly synchronized
  • Context cancellation must be respected
  • Resource cleanup must be thread-safe

## Security Requirements

Secret store implementations must follow security best practices:

  • Never log secret values (use logging.Secret wrapper)
  • Validate all inputs to prevent injection attacks
  • Use secure transport (TLS) for network operations
  • Handle credentials securely in memory
  • Support proper context cancellation

## Performance Considerations

Implementations should optimize for common patterns:

  • Describe() operations should be faster than Resolve()
  • Connection pooling and reuse where appropriate
  • Efficient batch operations when supported
  • Appropriate caching with security considerations
  • Respect context timeouts and cancellation

## Store-Specific Adaptations

Each store type has unique characteristics that implementations should handle:

### AWS Secrets Manager

  • JSON field extraction for structured secrets
  • Version labels (AWSCURRENT, AWSPENDING)
  • Cross-region replication
  • IAM-based access control

### HashiCorp Vault

  • Path-based organization with engines
  • Version numbers for KV v2
  • Token-based authentication
  • Namespace support in Enterprise

### 1Password

  • Vault/Item/Field hierarchy
  • SCIM integration for team management
  • CLI-based authentication flow
  • Item template variations

### Azure Key Vault

  • Managed identity integration
  • Certificate vs secret vs key distinction
  • Soft delete and purge protection
  • Network access restrictions

Integration Patterns

## Configuration Integration

Secret stores integrate with dsops configuration through:

  • Store definitions in dsops.yaml secretStores section
  • Factory functions registered in the registry
  • Validation during configuration loading
  • Runtime capability checking

## Resolution Integration

The resolution engine leverages secret stores through:

  • URI parsing and SecretRef creation
  • Store selection based on reference
  • Parallel resolution for batch operations
  • Error aggregation and reporting

## Rotation Integration

Rotation operations interact with secret stores for:

  • Storing newly generated secret values
  • Version management during rotation
  • Cleanup of deprecated versions
  • Audit trail storage

Testing Strategy

The package supports comprehensive testing through:

## Interface Contracts

  • Standard test suites for all implementations
  • Capability verification tests
  • Error condition testing
  • Thread safety validation

## Mock Implementations

  • In-memory stores for unit testing
  • Configurable behavior for edge cases
  • Performance and concurrency testing
  • Error injection for resilience testing

## Integration Testing

  • Real store connectivity tests
  • Authentication validation
  • Cross-store compatibility tests
  • Performance benchmarking

Future Evolution

The secretstore package is designed for extensibility:

## New Store Types

  • Plugin architecture for external stores
  • Community-contributed implementations
  • Experimental and preview stores
  • Legacy system integrations

## Enhanced Capabilities

  • Real-time change notifications
  • Advanced caching strategies
  • Batch operation optimization
  • Cross-store replication

## Protocol Evolution

  • New authentication methods
  • Enhanced metadata schemas
  • Performance optimizations
  • Security enhancements

This package provides the stable foundation for dsops's secret management capabilities, enabling secure, scalable, and reliable secret storage operations across diverse infrastructure environments.

Package secretstore provides interfaces and types for secret storage systems in dsops.

This package defines the modern secret store abstraction that replaces the older provider interface. Secret stores are systems that store and retrieve secret values, as distinct from services that consume secrets for operational purposes.

Secret Store vs Service Distinction

dsops separates two key concepts:

  • **Secret Stores**: Where secrets are stored (AWS Secrets Manager, Vault, 1Password, etc.)
  • **Services**: What uses secrets (PostgreSQL, Stripe API, GitHub, etc.)

This package focuses exclusively on secret stores - the storage layer.

Modern Reference Format

This package uses the new URI-based reference format:

store://store-name/path#field?version=v&option=value

Examples:

  • store://aws-prod/database/password
  • store://vault/secret/app#api_key?version=2
  • store://onepassword/Production/Database#password

Key Features

  • **URI-based references**: Consistent addressing across all stores
  • **Capability negotiation**: Stores expose their supported features
  • **Version support**: Built-in support for secret versioning
  • **Metadata access**: Describe secrets without retrieving values
  • **Standardized errors**: Consistent error types across stores
  • **Rotation integration**: Native support for secret rotation

Implementing a Secret Store

To implement a custom secret store:

  1. Implement the SecretStore interface
  2. Handle URI parsing for your store's format
  3. Provide appropriate capabilities
  4. Register with the secret store registry

Example:

type MySecretStore struct {
    client MyStoreClient
    name   string
}

func (s *MySecretStore) Name() string {
    return s.name
}

func (s *MySecretStore) Resolve(ctx context.Context, ref SecretRef) (SecretValue, error) {
    // Validate reference format
    if !ref.IsValid() {
        return SecretValue{}, ValidationError{
            Store:   s.name,
            Message: "invalid secret reference",
        }
    }

    // Retrieve secret from your storage system
    value, err := s.client.GetSecret(ref.Path)
    if err != nil {
        if isNotFound(err) {
            return SecretValue{}, NotFoundError{
                Store: s.name,
                Path:  ref.Path,
            }
        }
        return SecretValue{}, err
    }

    // Extract specific field if requested
    if ref.Field != "" {
        fieldValue, err := extractField(value, ref.Field)
        if err != nil {
            return SecretValue{}, err
        }
        value = fieldValue
    }

    return SecretValue{
        Value:     value,
        Version:   "1",
        UpdatedAt: time.Now(),
    }, nil
}

// ... implement other methods

Error Handling

Use the standardized error types:

  • NotFoundError: Secret doesn't exist
  • AuthError: Authentication failed
  • ValidationError: Invalid request or configuration

Security Considerations

Secret store implementations must:

  • Never log secret values (use logging.Secret wrapper)
  • Validate all inputs to prevent injection attacks
  • Use secure transport (TLS) for network operations
  • Support context cancellation for timeouts
  • Handle concurrent access safely

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type AuthError

type AuthError struct {
	// Store is the name of the secret store that failed authentication.
	Store string

	// Message provides details about the authentication failure.
	Message string
}

AuthError indicates that authentication to the secret store failed.

This error should be returned when:

  • Credentials are invalid or expired
  • Authentication method is not supported
  • Network authentication fails
  • Permission is denied for the requested operation

Example:

if !isAuthenticated() {
    return SecretValue{}, AuthError{
        Store:   s.Name(),
        Message: "API key is invalid or expired",
    }
}

func (AuthError) Error

func (e AuthError) Error() string

Error implements the error interface.

type NotFoundError

type NotFoundError struct {
	// Store is the name of the secret store where the secret was not found.
	Store string

	// Path is the secret path that could not be found.
	Path string
}

NotFoundError indicates that a requested secret does not exist in the store.

Secret stores should return this error when a SecretRef points to a non-existent secret. This is distinct from authentication failures or permission errors.

Example:

if !secretExists(ref.Path) {
    return SecretValue{}, NotFoundError{
        Store: s.Name(),
        Path:  ref.Path,
    }
}

func (NotFoundError) Error

func (e NotFoundError) Error() string

Error implements the error interface.

type RotationCapabilities

type RotationCapabilities struct {
	// SupportsRotation indicates if the store can create new versions
	// of existing secrets for rotation purposes.
	SupportsRotation bool

	// SupportsVersioning indicates if the store maintains multiple versions
	// simultaneously, allowing for zero-downtime rotation strategies.
	SupportsVersioning bool

	// MaxVersions specifies the maximum number of versions the store will keep.
	// 0 means unlimited versions are supported.
	MaxVersions int

	// MinRotationTime specifies the minimum time that must pass between
	// rotation attempts for the same secret.
	MinRotationTime time.Duration

	// Constraints contains store-specific rotation constraints and requirements.
	// Common keys include:
	//   - "max_length": Maximum secret value length
	//   - "min_length": Minimum secret value length
	//   - "charset": Allowed character set
	//   - "complexity": Password complexity requirements
	//   - "format": Required format (e.g., "json", "pem")
	Constraints map[string]string
}

RotationCapabilities describes rotation features supported by the store.

This structure defines what rotation operations a secret store can perform, including version management, timing constraints, and format requirements. Used by the rotation engine to plan and validate rotation operations.

Example:

rotationCaps := &RotationCapabilities{
    SupportsRotation:   true,
    SupportsVersioning: true,
    MaxVersions:        5,           // Keep 5 versions
    MinRotationTime:    time.Hour,   // Minimum 1 hour between rotations
    Constraints: map[string]string{
        "max_length":    "4096",
        "min_length":    "8",
        "charset":       "alphanumeric",
        "complexity":    "high",
    },
}

type SecretMetadata

type SecretMetadata struct {
	// Exists indicates whether the secret exists in the store.
	// If false, other fields may be empty or meaningless.
	Exists bool

	// Version identifies the current version of the secret.
	// Empty if versioning not supported or secret doesn't exist.
	Version string

	// UpdatedAt indicates when the secret was last modified.
	// Zero time if not supported or secret doesn't exist.
	UpdatedAt time.Time

	// Size is the approximate size of the secret value in bytes.
	// May be 0 if not supported, not available, or secret doesn't exist.
	Size int

	// Type describes the kind of secret (password, certificate, api_key, etc.).
	// Store-specific classification. May be empty.
	Type string

	// Permissions lists the operations the current credentials can perform
	// on this secret. Common values: "read", "write", "delete", "list".
	// Empty slice if not supported.
	Permissions []string

	// Tags contains store-specific metadata and labels.
	// Common keys include environment, owner, purpose, etc.
	// Empty map if not supported.
	Tags map[string]string
}

SecretMetadata describes a secret without exposing its value.

Used by the Describe method to provide information about a secret's existence, properties, and attributes without retrieving the actual secret value. Useful for validation, auditing, and planning operations.

Example:

meta := SecretMetadata{
    Exists:      true,
    Version:     "AWSCURRENT",
    UpdatedAt:   time.Now(),
    Size:        256, // bytes
    Type:        "password",
    Permissions: []string{"read", "list"},
    Tags: map[string]string{
        "environment": "production",
        "team":        "platform",
        "criticality": "high",
    },
}

type SecretRef

type SecretRef struct {
	// Store is the name of the secret store that contains this secret.
	// Must match a configured store name in dsops.yaml.
	Store string

	// Path identifies the secret within the store's namespace.
	// Format varies by store type:
	//   - AWS: Secret name or ARN
	//   - Vault: Full path like "secret/data/app"
	//   - 1Password: "Vault/Item" format
	//   - Azure: Secret name
	Path string

	// Field specifies a particular field within a structured secret.
	// Used when secrets contain multiple values (JSON objects, key-value pairs).
	// Optional - if empty, the entire secret value is returned.
	Field string

	// Version specifies a particular version of the secret.
	// Format is store-specific:
	//   - AWS: "AWSCURRENT", "AWSPENDING", or version UUID
	//   - Vault: Integer version number as string
	//   - Others: Store-defined versioning scheme
	// Optional - if empty, returns the current/latest version.
	Version string

	// Options contains additional store-specific parameters.
	// Common options include:
	//   - "vault": Vault name for 1Password
	//   - "namespace": Namespace for Vault Enterprise
	//   - "region": AWS region override
	Options map[string]string
}

SecretRef identifies a secret within a secret store using the modern URI-based format.

This structure represents a parsed store:// URI and provides a standardized way to reference secrets across different storage systems. It replaces the older Provider+Key format with a more flexible and extensible approach.

Different stores use different addressing patterns:

  • AWS Secrets Manager: Path is the secret name, Field for JSON extraction
  • HashiCorp Vault: Path is the full secret path, Field is the key name
  • 1Password: Path is Vault/Item format, Field is the field name
  • Azure Key Vault: Path is the secret name, Version for specific versions

Examples:

// AWS Secrets Manager with JSON field extraction
SecretRef{
    Store:   "aws-prod",
    Path:    "database/credentials",
    Field:   "password",
    Version: "AWSCURRENT",
}

// HashiCorp Vault KV store
SecretRef{
    Store:   "vault",
    Path:    "secret/data/app",
    Field:   "api_key",
    Version: "2",
}

// 1Password item
SecretRef{
    Store:   "onepassword",
    Path:    "Production/Database",
    Field:   "password",
    Options: map[string]string{"vault": "Private"},
}

func ParseSecretRef

func ParseSecretRef(uri string) (SecretRef, error)

ParseSecretRef parses a store:// URI into a SecretRef.

This function converts URI-based secret references into structured SecretRef objects. It supports the full dsops store:// URI format with optional field extraction, version selection, and custom options.

URI Format:

store://store-name/path#field?version=v&option=value

Components:

  • scheme: Must be "store://"
  • store-name: Name of the secret store (required)
  • path: Path to the secret within the store (required)
  • field: Optional field name for extraction (after #)
  • version: Optional version specifier (in query params)
  • options: Additional store-specific parameters (in query params)

Examples:

// Basic secret reference
ref, err := ParseSecretRef("store://aws-prod/database/password")

// With field extraction
ref, err := ParseSecretRef("store://vault/secret/app#api_key")

// With version and options
ref, err := ParseSecretRef("store://aws-prod/cert?version=AWSCURRENT&region=us-west-2")

// Complex example
ref, err := ParseSecretRef("store://onepassword/Production/Database#password?vault=Private")

Returns ValidationError for malformed URIs or missing required components.

Example

Example demonstrates URI parsing and reference creation

package main

import (
	"fmt"

	"github.com/systmms/dsops/pkg/secretstore"
)

func main() {
	// Parse various URI formats
	examples := []string{
		"store://aws-prod/database/password",
		"store://vault/secret/app#api_key?version=2",
		"store://onepassword/Production/Database#password?vault=Private",
		"store://azure/app-secrets?version=latest&region=eastus",
	}

	for _, uri := range examples {
		ref, err := secretstore.ParseSecretRef(uri)
		if err != nil {
			fmt.Printf("Failed to parse %s: %v\n", uri, err)
			continue
		}

		fmt.Printf("URI: %s\n", uri)
		fmt.Printf("  Store: %s\n", ref.Store)
		fmt.Printf("  Path: %s\n", ref.Path)
		if ref.Field != "" {
			fmt.Printf("  Field: %s\n", ref.Field)
		}
		if ref.Version != "" {
			fmt.Printf("  Version: %s\n", ref.Version)
		}
		if len(ref.Options) > 0 {
			fmt.Printf("  Options: %v\n", ref.Options)
		}
		fmt.Printf("  Valid: %t\n", ref.IsValid())
		fmt.Println()
	}

}
Output:
URI: store://aws-prod/database/password
  Store: aws-prod
  Path: database/password
  Valid: true

URI: store://vault/secret/app#api_key?version=2
  Store: vault
  Path: secret/app
  Field: api_key
  Version: 2
  Valid: true

URI: store://onepassword/Production/Database#password?vault=Private
  Store: onepassword
  Path: Production/Database
  Field: password
  Options: map[vault:Private]
  Valid: true

URI: store://azure/app-secrets?version=latest&region=eastus
  Store: azure
  Path: app-secrets
  Version: latest
  Options: map[region:eastus]
  Valid: true

func (SecretRef) IsValid

func (ref SecretRef) IsValid() bool

IsValid checks if a SecretRef has all required fields.

A valid SecretRef must have both Store and Path fields populated. Field, Version, and Options are optional and don't affect validity.

This method is used for validation before performing secret operations to ensure the reference can be processed correctly.

Example:

ref := SecretRef{
    Store: "aws-prod",
    Path:  "database/password",
    // Field, Version, Options are optional
}

if !ref.IsValid() {
    return ValidationError{
        Message: "invalid secret reference",
    }
}

Returns true if both Store and Path are non-empty, false otherwise.

func (SecretRef) String

func (ref SecretRef) String() string

String converts a SecretRef back to URI format.

This method reconstructs a store:// URI from a SecretRef structure, including all optional components like field extraction, version selection, and custom options.

The output format matches what ParseSecretRef expects:

store://store-name/path#field?version=v&option=value

Examples:

ref := SecretRef{
    Store:   "aws-prod",
    Path:    "database/password",
    Field:   "password",
    Version: "AWSCURRENT",
    Options: map[string]string{"region": "us-west-2"},
}

uri := ref.String()
// Result: "store://aws-prod/database/password#password?version=AWSCURRENT&region=us-west-2"

Returns empty string if the SecretRef is invalid (missing Store or Path).

Example

Example demonstrates SecretRef round-trip conversion

package main

import (
	"fmt"
	"log"

	"github.com/systmms/dsops/pkg/secretstore"
)

func main() {
	// Create a complex SecretRef
	ref := secretstore.SecretRef{
		Store:   "production-vault",
		Path:    "services/api/credentials",
		Field:   "api_key",
		Version: "latest",
		Options: map[string]string{
			"namespace": "production",
			"region":    "us-east-1",
		},
	}

	// Convert to URI string
	uri := ref.String()
	fmt.Printf("Original ref: %s\n", ref.String())
	fmt.Printf("URI: %s\n", uri)

	// Parse back from URI
	parsed, err := secretstore.ParseSecretRef(uri)
	if err != nil {
		log.Fatalf("Failed to parse URI: %v", err)
	}

	fmt.Printf("Parsed ref: %s\n", parsed.String())
	fmt.Printf("Round-trip successful: %t\n",
		ref.Store == parsed.Store &&
			ref.Path == parsed.Path &&
			ref.Field == parsed.Field &&
			ref.Version == parsed.Version)

}
Output:
Original ref: store://production-vault/services/api/credentials#api_key?namespace=production&region=us-east-1&version=latest
URI: store://production-vault/services/api/credentials#api_key?namespace=production&region=us-east-1&version=latest
Parsed ref: store://production-vault/services/api/credentials#api_key?namespace=production&region=us-east-1&version=latest
Round-trip successful: true

type SecretStore

type SecretStore interface {
	// Name returns the secret store's unique identifier.
	//
	// This should be a stable identifier that matches the store name used in
	// configuration and URI references. Examples: "aws-prod", "vault-dev", "onepassword".
	//
	// The name is used for:
	//   - Error messages and logging
	//   - Store registration and lookup
	//   - Configuration validation
	Name() string

	// Resolve retrieves a secret value from the store.
	//
	// This is the primary method for accessing secret values. It takes a SecretRef
	// that specifies the exact secret to retrieve, including optional field
	// extraction and version selection.
	//
	// The method should:
	//   - Support context cancellation and timeouts
	//   - Return NotFoundError for missing secrets
	//   - Return AuthError for authentication failures
	//   - Extract specific fields if ref.Field is specified
	//   - Handle version selection if ref.Version is specified
	//   - Never log the secret value
	//
	// Example:
	//
	//	ref := SecretRef{
	//	    Store:   "aws-secrets",
	//	    Path:    "prod/database/credentials",
	//	    Field:   "password",
	//	    Version: "AWSCURRENT",
	//	}
	//
	//	secret, err := store.Resolve(ctx, ref)
	//	if err != nil {
	//	    var notFound NotFoundError
	//	    if errors.As(err, &notFound) {
	//	        // Handle missing secret
	//	    }
	//	    return err
	//	}
	Resolve(ctx context.Context, ref SecretRef) (SecretValue, error)

	// Describe returns metadata about a secret without retrieving its value.
	//
	// This method provides information about a secret's existence, properties,
	// and attributes without exposing the actual secret value. It's useful for:
	//   - Validation and planning operations
	//   - Checking secret existence before retrieval
	//   - Gathering metadata for audit and reporting
	//
	// Unlike Resolve, this method should:
	//   - Be faster since no secret value is retrieved
	//   - Return metadata with Exists=false for missing secrets
	//   - Not return NotFoundError (use Exists field instead)
	//   - Include available version and size information
	//
	// Example:
	//
	//	meta, err := store.Describe(ctx, ref)
	//	if err != nil {
	//	    return err
	//	}
	//	if !meta.Exists {
	//	    fmt.Println("Secret does not exist")
	//	} else {
	//	    fmt.Printf("Secret size: %d bytes, version: %s\n", meta.Size, meta.Version)
	//	}
	Describe(ctx context.Context, ref SecretRef) (SecretMetadata, error)

	// Capabilities returns the secret store's supported features and limitations.
	//
	// This method exposes what functionality the secret store supports, allowing
	// dsops to adapt its behavior accordingly. The capabilities are used for:
	//   - Feature availability checks
	//   - Configuration validation
	//   - UI/CLI feature enablement
	//   - Rotation planning
	//
	// Example:
	//
	//	caps := store.Capabilities()
	//	if !caps.SupportsVersioning {
	//	    fmt.Println("Warning: Store doesn't support versioning")
	//	}
	//	if caps.Rotation != nil && caps.Rotation.SupportsRotation {
	//	    fmt.Println("Store supports secret rotation")
	//	}
	Capabilities() SecretStoreCapabilities

	// Validate checks if the secret store is properly configured and authenticated.
	//
	// This method verifies that the store can successfully connect to its
	// backend system and has appropriate permissions. It should be called
	// before performing any secret operations.
	//
	// The method should:
	//   - Test connectivity to the backend system
	//   - Verify authentication credentials
	//   - Check minimum required permissions
	//   - Support context cancellation
	//   - Return AuthError for authentication failures
	//   - Return descriptive errors for configuration issues
	//
	// Example:
	//
	//	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	//	defer cancel()
	//
	//	if err := store.Validate(ctx); err != nil {
	//	    var authErr AuthError
	//	    if errors.As(err, &authErr) {
	//	        fmt.Printf("Authentication failed: %v\n", authErr)
	//	    } else {
	//	        fmt.Printf("Validation failed: %v\n", err)
	//	    }
	//	    return err
	//	}
	Validate(ctx context.Context) error
}

SecretStore defines the interface for systems that store and retrieve secrets.

This interface replaces the storage functionality from the original Provider interface, providing a cleaner separation between secret stores (storage) and services (consumers).

All secret store implementations must be thread-safe as multiple goroutines may call these methods concurrently.

Example usage:

store := &MySecretStore{}
if err := store.Validate(ctx); err != nil {
    return fmt.Errorf("store validation failed: %w", err)
}

ref := SecretRef{
    Store: "my-store",
    Path:  "app/database/password",
}

secret, err := store.Resolve(ctx, ref)
if err != nil {
    return fmt.Errorf("failed to resolve secret: %w", err)
}
Example (Basic)

Example demonstrates basic secret store usage with URI references

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/systmms/dsops/pkg/secretstore"
)

func main() {
	// Create a mock secret store for demonstration
	fixedTime := time.Date(2025, 11, 15, 0, 0, 0, 0, time.UTC)
	store := &MockSecretStore{
		name: "example-store",
		secrets: map[string]secretstore.SecretValue{
			"database/credentials": {
				Value:     `{"username":"dbuser","password":"secret123"}`,
				Version:   "v1.0",
				UpdatedAt: fixedTime,
				Metadata: map[string]string{
					"environment": "production",
					"created_by":  "platform-team",
				},
			},
		},
	}

	ctx := context.Background()

	// Validate store is properly configured
	if err := store.Validate(ctx); err != nil {
		log.Fatalf("Store validation failed: %v", err)
	}

	// Create a secret reference using the new format
	ref := secretstore.SecretRef{
		Store: "example-store",
		Path:  "database/credentials",
		Field: "password", // Extract specific field from JSON
	}

	// Resolve the secret
	secret, err := store.Resolve(ctx, ref)
	if err != nil {
		log.Fatalf("Failed to resolve secret: %v", err)
	}

	fmt.Printf("Store name: %s\n", store.Name())
	fmt.Printf("Secret version: %s\n", secret.Version)
	fmt.Printf("Environment: %s\n", secret.Metadata["environment"])
	fmt.Printf("Field extracted: %s\n", secret.Value) // Would be "secret123" after JSON extraction

}

// MockSecretStore implements SecretStore interface for examples and testing
type MockSecretStore struct {
	name         string
	secrets      map[string]secretstore.SecretValue
	metadata     map[string]secretstore.SecretMetadata
	capabilities secretstore.SecretStoreCapabilities
	validateErr  error
}

func (m *MockSecretStore) Name() string {
	return m.name
}

func (m *MockSecretStore) Resolve(ctx context.Context, ref secretstore.SecretRef) (secretstore.SecretValue, error) {
	if !ref.IsValid() {
		return secretstore.SecretValue{}, secretstore.ValidationError{
			Store:   m.name,
			Message: "invalid secret reference",
		}
	}

	secret, exists := m.secrets[ref.Path]
	if !exists {
		return secretstore.SecretValue{}, secretstore.NotFoundError{
			Store: m.name,
			Path:  ref.Path,
		}
	}

	if ref.Version != "" && ref.Version != secret.Version {
		return secretstore.SecretValue{}, secretstore.NotFoundError{
			Store: m.name,
			Path:  ref.Path + " (version " + ref.Version + ")",
		}
	}

	_ = ref.Field

	return secret, nil
}

func (m *MockSecretStore) Describe(ctx context.Context, ref secretstore.SecretRef) (secretstore.SecretMetadata, error) {
	if !ref.IsValid() {
		return secretstore.SecretMetadata{}, secretstore.ValidationError{
			Store:   m.name,
			Message: "invalid secret reference",
		}
	}

	if m.metadata != nil {
		if meta, exists := m.metadata[ref.Path]; exists {
			return meta, nil
		}
	}

	_, exists := m.secrets[ref.Path]
	return secretstore.SecretMetadata{Exists: exists}, nil
}

func (m *MockSecretStore) Capabilities() secretstore.SecretStoreCapabilities {
	return m.capabilities
}

func (m *MockSecretStore) Validate(ctx context.Context) error {
	return m.validateErr
}
Output:
Store name: example-store
Secret version: v1.0
Environment: production
Field extracted: {"username":"dbuser","password":"secret123"}
Example (Capabilities)

Example demonstrates capability-driven feature detection

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/systmms/dsops/pkg/secretstore"
)

func main() {
	store := &MockSecretStore{
		name: "advanced-store",
		capabilities: secretstore.SecretStoreCapabilities{
			SupportsVersioning: true,
			SupportsMetadata:   true,
			SupportsWatching:   false,
			SupportsBinary:     true,
			RequiresAuth:       true,
			AuthMethods:        []string{"iam", "api_key"},
			Rotation: &secretstore.RotationCapabilities{
				SupportsRotation:   true,
				SupportsVersioning: true,
				MaxVersions:        10,
				MinRotationTime:    time.Hour,
				Constraints: map[string]string{
					"max_length": "4096",
					"complexity": "high",
				},
			},
		},
	}

	caps := store.Capabilities()

	fmt.Printf("Store: %s\n", store.Name())
	fmt.Printf("Supports versioning: %t\n", caps.SupportsVersioning)
	fmt.Printf("Supports metadata: %t\n", caps.SupportsMetadata)
	fmt.Printf("Supports binary: %t\n", caps.SupportsBinary)
	fmt.Printf("Requires auth: %t\n", caps.RequiresAuth)
	fmt.Printf("Auth methods: %v\n", caps.AuthMethods)

	// Check rotation capabilities
	if caps.Rotation != nil {
		fmt.Printf("Supports rotation: %t\n", caps.Rotation.SupportsRotation)
		fmt.Printf("Max versions: %d\n", caps.Rotation.MaxVersions)
		fmt.Printf("Min rotation time: %s\n", caps.Rotation.MinRotationTime)

		if constraint, ok := caps.Rotation.Constraints["max_length"]; ok {
			fmt.Printf("Max secret length: %s\n", constraint)
		}
	}

	// Use capabilities to adapt application behavior
	if caps.SupportsVersioning {
		fmt.Println("Version-specific operations available")
	}

	if !caps.SupportsWatching {
		fmt.Println("Real-time notifications not supported - using polling")
	}

}

// MockSecretStore implements SecretStore interface for examples and testing
type MockSecretStore struct {
	name         string
	secrets      map[string]secretstore.SecretValue
	metadata     map[string]secretstore.SecretMetadata
	capabilities secretstore.SecretStoreCapabilities
	validateErr  error
}

func (m *MockSecretStore) Name() string {
	return m.name
}

func (m *MockSecretStore) Resolve(ctx context.Context, ref secretstore.SecretRef) (secretstore.SecretValue, error) {
	if !ref.IsValid() {
		return secretstore.SecretValue{}, secretstore.ValidationError{
			Store:   m.name,
			Message: "invalid secret reference",
		}
	}

	secret, exists := m.secrets[ref.Path]
	if !exists {
		return secretstore.SecretValue{}, secretstore.NotFoundError{
			Store: m.name,
			Path:  ref.Path,
		}
	}

	if ref.Version != "" && ref.Version != secret.Version {
		return secretstore.SecretValue{}, secretstore.NotFoundError{
			Store: m.name,
			Path:  ref.Path + " (version " + ref.Version + ")",
		}
	}

	_ = ref.Field

	return secret, nil
}

func (m *MockSecretStore) Describe(ctx context.Context, ref secretstore.SecretRef) (secretstore.SecretMetadata, error) {
	if !ref.IsValid() {
		return secretstore.SecretMetadata{}, secretstore.ValidationError{
			Store:   m.name,
			Message: "invalid secret reference",
		}
	}

	if m.metadata != nil {
		if meta, exists := m.metadata[ref.Path]; exists {
			return meta, nil
		}
	}

	_, exists := m.secrets[ref.Path]
	return secretstore.SecretMetadata{Exists: exists}, nil
}

func (m *MockSecretStore) Capabilities() secretstore.SecretStoreCapabilities {
	return m.capabilities
}

func (m *MockSecretStore) Validate(ctx context.Context) error {
	return m.validateErr
}
Output:
Store: advanced-store
Supports versioning: true
Supports metadata: true
Supports binary: true
Requires auth: true
Auth methods: [iam api_key]
Supports rotation: true
Max versions: 10
Min rotation time: 1h0m0s
Max secret length: 4096
Version-specific operations available
Real-time notifications not supported - using polling
Example (Describe)

Example demonstrates metadata-only operations for efficiency

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/systmms/dsops/pkg/secretstore"
)

func main() {
	store := &MockSecretStore{
		name: "metadata-store",
		metadata: map[string]secretstore.SecretMetadata{
			"app/certificate": {
				Exists:      true,
				Version:     "v3.2",
				UpdatedAt:   time.Date(2024, 3, 15, 14, 30, 0, 0, time.UTC),
				Size:        2048,
				Type:        "certificate",
				Permissions: []string{"read", "rotate"},
				Tags: map[string]string{
					"environment": "production",
					"team":        "security",
					"expires":     "2024-12-31",
				},
			},
		},
	}

	ctx := context.Background()
	ref := secretstore.SecretRef{
		Store: "metadata-store",
		Path:  "app/certificate",
	}

	// Get metadata without retrieving the secret value
	meta, err := store.Describe(ctx, ref)
	if err != nil {
		log.Fatalf("Failed to describe secret: %v", err)
	}

	if meta.Exists {
		fmt.Printf("Secret: %s\n", ref.Path)
		fmt.Printf("Version: %s\n", meta.Version)
		fmt.Printf("Size: %d bytes\n", meta.Size)
		fmt.Printf("Type: %s\n", meta.Type)
		fmt.Printf("Updated: %s\n", meta.UpdatedAt.Format("2006-01-02 15:04"))
		fmt.Printf("Permissions: %v\n", meta.Permissions)
		fmt.Printf("Team: %s\n", meta.Tags["team"])
		fmt.Printf("Expires: %s\n", meta.Tags["expires"])

		// Use metadata to make decisions without retrieving the secret
		if meta.Type == "certificate" {
			fmt.Println("Certificate detected - checking expiration...")
		}

		if contains(meta.Permissions, "rotate") {
			fmt.Println("Secret supports rotation")
		}
	} else {
		fmt.Printf("Secret %s does not exist\n", ref.Path)
	}

}

// MockSecretStore implements SecretStore interface for examples and testing
type MockSecretStore struct {
	name         string
	secrets      map[string]secretstore.SecretValue
	metadata     map[string]secretstore.SecretMetadata
	capabilities secretstore.SecretStoreCapabilities
	validateErr  error
}

func (m *MockSecretStore) Name() string {
	return m.name
}

func (m *MockSecretStore) Resolve(ctx context.Context, ref secretstore.SecretRef) (secretstore.SecretValue, error) {
	if !ref.IsValid() {
		return secretstore.SecretValue{}, secretstore.ValidationError{
			Store:   m.name,
			Message: "invalid secret reference",
		}
	}

	secret, exists := m.secrets[ref.Path]
	if !exists {
		return secretstore.SecretValue{}, secretstore.NotFoundError{
			Store: m.name,
			Path:  ref.Path,
		}
	}

	if ref.Version != "" && ref.Version != secret.Version {
		return secretstore.SecretValue{}, secretstore.NotFoundError{
			Store: m.name,
			Path:  ref.Path + " (version " + ref.Version + ")",
		}
	}

	_ = ref.Field

	return secret, nil
}

func (m *MockSecretStore) Describe(ctx context.Context, ref secretstore.SecretRef) (secretstore.SecretMetadata, error) {
	if !ref.IsValid() {
		return secretstore.SecretMetadata{}, secretstore.ValidationError{
			Store:   m.name,
			Message: "invalid secret reference",
		}
	}

	if m.metadata != nil {
		if meta, exists := m.metadata[ref.Path]; exists {
			return meta, nil
		}
	}

	_, exists := m.secrets[ref.Path]
	return secretstore.SecretMetadata{Exists: exists}, nil
}

func (m *MockSecretStore) Capabilities() secretstore.SecretStoreCapabilities {
	return m.capabilities
}

func (m *MockSecretStore) Validate(ctx context.Context) error {
	return m.validateErr
}

// Helper function used in examples
func contains(slice []string, item string) bool {
	for _, s := range slice {
		if s == item {
			return true
		}
	}
	return false
}
Output:
Secret: app/certificate
Version: v3.2
Size: 2048 bytes
Type: certificate
Updated: 2024-03-15 14:30
Permissions: [read rotate]
Team: security
Expires: 2024-12-31
Certificate detected - checking expiration...
Secret supports rotation
Example (ErrorHandling)

Example demonstrates error handling with structured error types

package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/systmms/dsops/pkg/secretstore"
)

func main() {
	store := &MockSecretStore{
		name:    "example-store",
		secrets: make(map[string]secretstore.SecretValue),
	}

	ctx := context.Background()
	ref := secretstore.SecretRef{
		Store: "example-store",
		Path:  "nonexistent/secret",
	}

	_, err := store.Resolve(ctx, ref)
	if err != nil {
		// Handle different error types appropriately
		var notFoundErr secretstore.NotFoundError
		var authErr secretstore.AuthError
		var validationErr secretstore.ValidationError

		switch {
		case errors.As(err, &notFoundErr):
			fmt.Printf("Secret not found: %s in store %s\n",
				notFoundErr.Path, notFoundErr.Store)
			// Could implement fallback logic here

		case errors.As(err, &authErr):
			fmt.Printf("Authentication failed for store %s: %s\n",
				authErr.Store, authErr.Message)
			// Could trigger re-authentication here

		case errors.As(err, &validationErr):
			fmt.Printf("Validation error: %s\n", validationErr.Message)
			// Could provide user guidance here

		default:
			fmt.Printf("Unexpected error: %v\n", err)
		}
	}

}

// MockSecretStore implements SecretStore interface for examples and testing
type MockSecretStore struct {
	name         string
	secrets      map[string]secretstore.SecretValue
	metadata     map[string]secretstore.SecretMetadata
	capabilities secretstore.SecretStoreCapabilities
	validateErr  error
}

func (m *MockSecretStore) Name() string {
	return m.name
}

func (m *MockSecretStore) Resolve(ctx context.Context, ref secretstore.SecretRef) (secretstore.SecretValue, error) {
	if !ref.IsValid() {
		return secretstore.SecretValue{}, secretstore.ValidationError{
			Store:   m.name,
			Message: "invalid secret reference",
		}
	}

	secret, exists := m.secrets[ref.Path]
	if !exists {
		return secretstore.SecretValue{}, secretstore.NotFoundError{
			Store: m.name,
			Path:  ref.Path,
		}
	}

	if ref.Version != "" && ref.Version != secret.Version {
		return secretstore.SecretValue{}, secretstore.NotFoundError{
			Store: m.name,
			Path:  ref.Path + " (version " + ref.Version + ")",
		}
	}

	_ = ref.Field

	return secret, nil
}

func (m *MockSecretStore) Describe(ctx context.Context, ref secretstore.SecretRef) (secretstore.SecretMetadata, error) {
	if !ref.IsValid() {
		return secretstore.SecretMetadata{}, secretstore.ValidationError{
			Store:   m.name,
			Message: "invalid secret reference",
		}
	}

	if m.metadata != nil {
		if meta, exists := m.metadata[ref.Path]; exists {
			return meta, nil
		}
	}

	_, exists := m.secrets[ref.Path]
	return secretstore.SecretMetadata{Exists: exists}, nil
}

func (m *MockSecretStore) Capabilities() secretstore.SecretStoreCapabilities {
	return m.capabilities
}

func (m *MockSecretStore) Validate(ctx context.Context) error {
	return m.validateErr
}
Output:
Secret not found: nonexistent/secret in store example-store

type SecretStoreCapabilities

type SecretStoreCapabilities struct {
	// SupportsVersioning indicates if the store maintains multiple versions
	// of secrets and can retrieve specific versions.
	SupportsVersioning bool

	// SupportsMetadata indicates if the store supports additional metadata
	// like tags, descriptions, and custom attributes beyond the secret value.
	SupportsMetadata bool

	// SupportsWatching indicates if the store can notify about secret changes
	// in real-time or support long-polling for updates.
	SupportsWatching bool

	// SupportsBinary indicates if the store can store and retrieve binary data
	// (certificates, keys, images) or only text-based secrets.
	SupportsBinary bool

	// RequiresAuth indicates if the store requires authentication to access secrets.
	// If false, the store may work without credentials (e.g., literal store).
	RequiresAuth bool

	// AuthMethods lists the authentication methods supported by this store.
	// Common values include:
	//   - "api_key": API key/token authentication
	//   - "basic": Username/password authentication
	//   - "oauth2": OAuth2 flow
	//   - "iam": Cloud IAM roles
	//   - "certificate": Client certificate authentication
	//   - "cli": CLI-based authentication (like aws configure)
	AuthMethods []string

	// Rotation contains rotation-related capabilities for stores that
	// support creating and managing multiple versions for rotation.
	// Nil if the store doesn't support rotation operations.
	Rotation *RotationCapabilities
}

SecretStoreCapabilities describes what features and operations a secret store supports.

This structure allows dsops to adapt its behavior based on store capabilities, enable/disable features appropriately, and provide accurate user feedback about what operations are possible.

Example:

caps := SecretStoreCapabilities{
    SupportsVersioning: true,
    SupportsMetadata:   true,
    SupportsWatching:   false,
    SupportsBinary:     true,
    RequiresAuth:       true,
    AuthMethods:        []string{"iam", "api_key"},
    Rotation: &RotationCapabilities{
        SupportsRotation:   true,
        SupportsVersioning: true,
        MaxVersions:        10,
        MinRotationTime:    time.Hour,
    },
}

type SecretValue

type SecretValue struct {
	// Value is the actual secret data as a string.
	// For binary data, this should be base64 encoded.
	// Implementations must never log this field.
	Value string

	// Version identifies the specific version of this secret.
	// Format is store-specific. May be empty if versioning not supported.
	Version string

	// UpdatedAt indicates when this secret was last modified.
	// May be zero time if the store doesn't support timestamps.
	UpdatedAt time.Time

	// Metadata contains store-specific information about the secret.
	// Common keys include:
	//   - "created_by": Who created the secret
	//   - "environment": Environment tag (prod, staging, dev)
	//   - "owner": Team or individual responsible
	//   - "rotation_id": Rotation tracking identifier
	//   - "content_type": MIME type for binary data
	Metadata map[string]string
}

SecretValue represents a retrieved secret with its metadata.

This structure contains the actual secret value along with version information and timestamps. The Value field contains the raw secret data as a string. For binary data, this should be base64 encoded by the store implementation.

Example:

secret := SecretValue{
    Value:     "super-secret-password",
    Version:   "v1.2.3",
    UpdatedAt: time.Now(),
    Metadata: map[string]string{
        "created_by":   "rotation-service",
        "environment": "production",
        "owner":       "platform-team",
    },
}

type ValidationError

type ValidationError struct {
	// Store is the name of the secret store where validation failed.
	// May be empty for general validation errors.
	Store string

	// Message provides details about what validation failed.
	Message string
}

ValidationError indicates that a request or configuration is invalid.

This error should be returned when:

  • SecretRef format is invalid
  • Required parameters are missing
  • Configuration values are out of range
  • URI parsing fails

Example:

if !ref.IsValid() {
    return SecretValue{}, ValidationError{
        Store:   s.Name(),
        Message: "invalid secret reference format",
    }
}

func (ValidationError) Error

func (e ValidationError) Error() string

Error implements the error interface.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL