Skip to content
Merged
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
14 changes: 9 additions & 5 deletions pkg/secrets/op_cli_secrets_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import (
)

// The OnePasswordCLISecretsProvider is an implementation of the SecretsProvider interface
// It provides integration with the 1Password CLI for secret management
// It serves as a bridge between the application and 1Password's secure storage
// It enables retrieval and parsing of secrets from 1Password vaults
// It provides integration with the 1Password CLI for secret management with automatic shell scrubbing registration
// It serves as a bridge between the application and 1Password's secure storage with built-in security features
// It enables retrieval and parsing of secrets from 1Password vaults while automatically registering secrets for output scrubbing

// =============================================================================
// Types
Expand Down Expand Up @@ -41,7 +41,9 @@ func NewOnePasswordCLISecretsProvider(vault secretsConfigType.OnePasswordVault,
// Public Methods
// =============================================================================

// GetSecret retrieves a secret value for the specified key
// GetSecret retrieves a secret value for the specified key and automatically registers it with the shell for output scrubbing.
// It uses the 1Password CLI to fetch secrets from the configured vault and automatically registers
// the retrieved secret with the shell's scrubbing system to prevent accidental exposure in command output.
func (s *OnePasswordCLISecretsProvider) GetSecret(key string) (string, error) {
if !s.unlocked {
return "********", nil
Expand All @@ -59,7 +61,9 @@ func (s *OnePasswordCLISecretsProvider) GetSecret(key string) (string, error) {
return "", fmt.Errorf("failed to retrieve secret from 1Password: %w", err)
}

return strings.TrimSpace(string(output)), nil
value := strings.TrimSpace(string(output))
s.shell.RegisterSecret(value)
return value, nil
}

// ParseSecrets identifies and replaces ${{ op.<id>.<secret>.<field> }} patterns in the input
Expand Down
17 changes: 9 additions & 8 deletions pkg/secrets/op_sdk_secrets_provider.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// The OnePasswordSDKSecretsProvider is an implementation of the SecretsProvider interface
// It provides integration with the 1Password SDK for secret management
// It serves as a bridge between the application and 1Password's secure storage
// It enables retrieval and parsing of secrets from 1Password vaults using the official SDK
// It provides integration with the 1Password SDK for secret management with automatic shell scrubbing registration
// It serves as a bridge between the application and 1Password's secure storage with built-in security features
// It enables retrieval and parsing of secrets from 1Password vaults using the official SDK while automatically registering secrets for output scrubbing

package secrets

Expand Down Expand Up @@ -69,11 +69,11 @@ func (s *OnePasswordSDKSecretsProvider) Initialize() error {
return nil
}

// GetSecret retrieves a secret value for the specified key. It first checks if the provider is unlocked.
// If not, it returns a masked value. It then ensures the 1Password client is initialized using a
// service account token from the environment. The key is split into item and field parts, and the
// item name is sanitized. A secret reference URI is constructed and used to resolve the secret value
// from 1Password. If successful, the secret value is returned; otherwise, an error is reported.
// GetSecret retrieves a secret value for the specified key and automatically registers it with the shell for output scrubbing.
// It first checks if the provider is unlocked. If not, it returns a masked value. It then ensures the 1Password client
// is initialized using a service account token from the environment. The key is split into item and field parts, and the
// item name is sanitized. A secret reference URI is constructed and used to resolve the secret value from 1Password.
// If successful, the secret value is registered with the shell's scrubbing system and returned; otherwise, an error is reported.
func (s *OnePasswordSDKSecretsProvider) GetSecret(key string) (string, error) {
if !s.unlocked {
return "********", nil
Expand Down Expand Up @@ -120,6 +120,7 @@ func (s *OnePasswordSDKSecretsProvider) GetSecret(key string) (string, error) {
return "", fmt.Errorf("failed to resolve secret: %w", err)
}

s.shell.RegisterSecret(value)
return value, nil
}

Expand Down
160 changes: 45 additions & 115 deletions pkg/secrets/op_sdk_secrets_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,14 @@ func TestOnePasswordSDKSecretsProvider_GetSecret(t *testing.T) {
}

func TestOnePasswordSDKSecretsProvider_ParseSecrets(t *testing.T) {
t.Run("Success", func(t *testing.T) {
// setup creates and initializes a OnePasswordSDKSecretsProvider for testing
setup := func(t *testing.T) (*Mocks, *OnePasswordSDKSecretsProvider) {
t.Helper()

// Set environment variable
os.Setenv("OP_SERVICE_ACCOUNT_TOKEN", "test-token")
t.Cleanup(func() { os.Unsetenv("OP_SERVICE_ACCOUNT_TOKEN") })

// Setup mocks
mocks := setupMocks(t)

Expand All @@ -457,209 +464,132 @@ func TestOnePasswordSDKSecretsProvider_ParseSecrets(t *testing.T) {
ID: "test-id",
}

// Create the provider
// Create and initialize the provider
provider := NewOnePasswordSDKSecretsProvider(vault, mocks.Injector)
err := provider.Initialize()
if err != nil {
t.Fatalf("Failed to initialize provider: %v", err)
}

// Set environment variable
os.Setenv("OP_SERVICE_ACCOUNT_TOKEN", "test-token")
defer os.Unsetenv("OP_SERVICE_ACCOUNT_TOKEN")
return mocks, provider
}

// Set the provider to unlocked state
t.Run("Success", func(t *testing.T) {
// Given a provider with unlocked state and mock 1Password client configured
_, provider := setup(t)
provider.unlocked = true

// Set up the shims to use our mock
provider.shims.NewOnePasswordClient = func(ctx context.Context, opts ...onepassword.ClientOption) (*onepassword.Client, error) {
return &onepassword.Client{}, nil
}
provider.shims.ResolveSecret = func(client *onepassword.Client, ctx context.Context, secretRef string) (string, error) {
return "secret-value", nil
}

// Test with standard notation
// When parsing input containing a valid 1Password secret reference
input := "This is a secret: ${{ op.test-id.test-secret.password }}"
expectedOutput := "This is a secret: secret-value"

output, err := provider.ParseSecrets(input)

// Verify the result
// Then the secret reference should be replaced with the actual secret value
expectedOutput := "This is a secret: secret-value"
if err != nil {
t.Errorf("Expected no error, got %v", err)
}

if output != expectedOutput {
t.Errorf("Expected output to be '%s', got '%s'", expectedOutput, output)
}
})

t.Run("EmptyInput", func(t *testing.T) {
// Setup mocks
mocks := setupMocks(t)

// Create a test vault
vault := secretsConfigType.OnePasswordVault{
Name: "test-vault",
ID: "test-id",
}

// Create the provider
provider := NewOnePasswordSDKSecretsProvider(vault, mocks.Injector)

// Set environment variable
os.Setenv("OP_SERVICE_ACCOUNT_TOKEN", "test-token")
defer os.Unsetenv("OP_SERVICE_ACCOUNT_TOKEN")
// Given a provider with no special configuration
_, provider := setup(t)

// Test with empty input
// When parsing an empty input string
input := ""
output, err := provider.ParseSecrets(input)

// Verify the result
// Then the output should remain empty and unchanged
if err != nil {
t.Errorf("Expected no error, got %v", err)
}

if output != input {
t.Errorf("Expected output to be '%s', got '%s'", input, output)
}
})

t.Run("InvalidFormat", func(t *testing.T) {
// Setup mocks
mocks := setupMocks(t)

// Create a test vault
vault := secretsConfigType.OnePasswordVault{
Name: "test-vault",
ID: "test-id",
}
// Given a provider with no special configuration
_, provider := setup(t)

// Create the provider
provider := NewOnePasswordSDKSecretsProvider(vault, mocks.Injector)

// Set environment variable
os.Setenv("OP_SERVICE_ACCOUNT_TOKEN", "test-token")
defer os.Unsetenv("OP_SERVICE_ACCOUNT_TOKEN")

// Test with invalid format (missing field)
// When parsing input with an invalid secret reference format (missing field)
input := "This is a secret: ${{ op.test-id.test-secret }}"
expectedOutput := "This is a secret: <ERROR: invalid key path: test-id.test-secret>"

output, err := provider.ParseSecrets(input)

// Verify the result
// Then the invalid reference should be replaced with an error message
expectedOutput := "This is a secret: <ERROR: invalid key path: test-id.test-secret>"
if err != nil {
t.Errorf("Expected no error, got %v", err)
}

if output != expectedOutput {
t.Errorf("Expected output to be '%s', got '%s'", expectedOutput, output)
}
})

t.Run("MalformedJSON", func(t *testing.T) {
// Setup mocks
mocks := setupMocks(t)
// Given a provider with no special configuration
_, provider := setup(t)

// Create a test vault
vault := secretsConfigType.OnePasswordVault{
Name: "test-vault",
ID: "test-id",
}

// Create the provider
provider := NewOnePasswordSDKSecretsProvider(vault, mocks.Injector)

// Set environment variable
os.Setenv("OP_SERVICE_ACCOUNT_TOKEN", "test-token")
defer os.Unsetenv("OP_SERVICE_ACCOUNT_TOKEN")

// Test with malformed JSON (missing closing brace)
// When parsing input with malformed JSON syntax (missing closing brace)
input := "This is a secret: ${{ op.test-id.test-secret.password"
expectedOutput := "This is a secret: ${{ op.test-id.test-secret.password"

output, err := provider.ParseSecrets(input)

// Verify the result
// Then the malformed reference should be left unchanged
expectedOutput := "This is a secret: ${{ op.test-id.test-secret.password"
if err != nil {
t.Errorf("Expected no error, got %v", err)
}

if output != expectedOutput {
t.Errorf("Expected output to be '%s', got '%s'", expectedOutput, output)
}
})

t.Run("MismatchedVaultID", func(t *testing.T) {
// Setup mocks
mocks := setupMocks(t)
// Given a provider configured for vault "test-id"
_, provider := setup(t)

// Create a test vault
vault := secretsConfigType.OnePasswordVault{
Name: "test-vault",
ID: "test-id",
}

// Create the provider
provider := NewOnePasswordSDKSecretsProvider(vault, mocks.Injector)

// Set environment variable
os.Setenv("OP_SERVICE_ACCOUNT_TOKEN", "test-token")
defer os.Unsetenv("OP_SERVICE_ACCOUNT_TOKEN")

// Test with wrong vault ID
// When parsing input with a secret reference for a different vault ID
input := "This is a secret: ${{ op.wrong-id.test-secret.password }}"
expectedOutput := "This is a secret: ${{ op.wrong-id.test-secret.password }}"

output, err := provider.ParseSecrets(input)

// Verify the result
// Then the mismatched reference should be left unchanged
expectedOutput := "This is a secret: ${{ op.wrong-id.test-secret.password }}"
if err != nil {
t.Errorf("Expected no error, got %v", err)
}

if output != expectedOutput {
t.Errorf("Expected output to be '%s', got '%s'", expectedOutput, output)
}
})

t.Run("SecretNotFound", func(t *testing.T) {
// Setup mocks
mocks := setupMocks(t)

// Create a test vault
vault := secretsConfigType.OnePasswordVault{
Name: "test-vault",
ID: "test-id",
}

// Create the provider
provider := NewOnePasswordSDKSecretsProvider(vault, mocks.Injector)

// Set environment variable
os.Setenv("OP_SERVICE_ACCOUNT_TOKEN", "test-token")
defer os.Unsetenv("OP_SERVICE_ACCOUNT_TOKEN")

// Set the provider to unlocked state
// Given a provider with unlocked state and mock client that simulates secret not found
_, provider := setup(t)
provider.unlocked = true

// Set up the shims to use our mock
provider.shims.NewOnePasswordClient = func(ctx context.Context, opts ...onepassword.ClientOption) (*onepassword.Client, error) {
return &onepassword.Client{}, nil
}
provider.shims.ResolveSecret = func(client *onepassword.Client, ctx context.Context, secretRef string) (string, error) {
return "", errors.New("secret not found")
}

// Test with a secret that doesn't exist
// When parsing input with a reference to a nonexistent secret
input := "This is a secret: ${{ op.test-id.nonexistent-secret.password }}"
expectedOutput := "This is a secret: <ERROR: failed to resolve: nonexistent-secret.password: failed to resolve secret: secret not found>"

output, err := provider.ParseSecrets(input)

// Verify the result
// Then the reference should be replaced with an error message indicating the secret was not found
expectedOutput := "This is a secret: <ERROR: failed to resolve: nonexistent-secret.password: failed to resolve secret: secret not found>"
if err != nil {
t.Errorf("Expected no error, got %v", err)
}

if output != expectedOutput {
t.Errorf("Expected output to be '%s', got '%s'", expectedOutput, output)
}
Expand Down
11 changes: 7 additions & 4 deletions pkg/secrets/sops_secrets_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import (
)

// The SopsSecretsProvider is an implementation of the SecretsProvider interface
// It provides integration with Mozilla SOPS for encrypted secrets management
// It serves as a bridge between the application and SOPS-encrypted YAML files
// It enables secure storage and retrieval of secrets using SOPS encryption
// It provides integration with Mozilla SOPS for encrypted secrets management with automatic shell scrubbing registration
// It serves as a bridge between the application and SOPS-encrypted YAML files with built-in security features
// It enables secure storage and retrieval of secrets using SOPS encryption while automatically registering secrets for output scrubbing

// =============================================================================
// Constants
Expand Down Expand Up @@ -116,13 +116,16 @@ func (s *SopsSecretsProvider) LoadSecrets() error {
return nil
}

// GetSecret retrieves a secret value for the specified key
// GetSecret retrieves a secret value for the specified key and automatically registers it with the shell for output scrubbing.
// If the provider is locked, it returns a masked value. When unlocked, it returns the actual secret value
// and registers it with the shell's scrubbing system to prevent accidental exposure in command output.
func (s *SopsSecretsProvider) GetSecret(key string) (string, error) {
if !s.unlocked {
return "********", nil
}

if value, ok := s.secrets[key]; ok {
s.shell.RegisterSecret(value)
return value, nil
}

Expand Down
8 changes: 8 additions & 0 deletions pkg/shell/mock_shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type MockShell struct {
GetSessionTokenFunc func() (string, error)
CheckResetFlagsFunc func() (bool, error)
ResetFunc func()
RegisterSecretFunc func(value string)
}

// =============================================================================
Expand Down Expand Up @@ -199,5 +200,12 @@ func (s *MockShell) Reset() {
}
}

// RegisterSecret calls the custom RegisterSecretFunc if provided.
func (s *MockShell) RegisterSecret(value string) {
if s.RegisterSecretFunc != nil {
s.RegisterSecretFunc(value)
}
}

// Ensure MockShell implements the Shell interface
var _ Shell = (*MockShell)(nil)
Loading
Loading