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
1 change: 1 addition & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,7 @@ Strict mode applies to **both** direct dot-access (`{{ .steps.auth.field }}`) an
|------|-------------|--------|
| `secrets.vault` | HashiCorp Vault integration | secrets |
| `secrets.aws` | AWS Secrets Manager integration | secrets |
| `secrets.keychain` | OS credential store (macOS Keychain, Linux Secret Service, Windows Credential Manager); requires libsecret/gnome-keyring/KWallet on Linux | secrets |

### Event Sourcing & Messaging Services
| Type | Description | Plugin |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ workflows:
| **Storage/Persistence** | 7 | database.workflow, persistence.store, storage.s3, storage.gcs, storage.local, storage.sqlite, static.fileserver |
| **Observability** | 4 | metrics.collector, health.checker, observability.otel, log.collector |
| **Auth** | 2 | auth.jwt, auth.user-store |
| **Other** | 6 | data.transformer, webhook.sender, dynamic.component, secrets.vault, secrets.aws, workflow.registry |
| **Other** | 7 | data.transformer, webhook.sender, dynamic.component, secrets.vault, secrets.aws, secrets.keychain, workflow.registry |
| **Triggers** | 5 | http, schedule, event, eventbus, mock |

### Security
Expand Down
9 changes: 8 additions & 1 deletion cmd/wfctl/infra_secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,14 @@ func resolveSecretsProvider(cfg *SecretsConfig) (secrets.Provider, error) {
prefix, _ := c["prefix"].(string)
return secrets.NewEnvProvider(prefix), nil

case "keychain":
service, _ := c["service"].(string)
if service == "" {
return nil, fmt.Errorf("secrets.keychain: 'service' is required")
}
return secrets.NewKeychainProvider(service)

default:
return nil, fmt.Errorf("unknown secrets provider %q (supported: github, vault, aws, env)", cfg.Provider)
return nil, fmt.Errorf("unknown secrets provider %q (supported: github, vault, aws, env, keychain)", cfg.Provider)
}
}
25 changes: 25 additions & 0 deletions cmd/wfctl/infra_secrets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,5 +155,30 @@ func TestParseSecretsConfig_MissingFile(t *testing.T) {
}
}

func TestResolveSecretsProvider_KeychainProvider(t *testing.T) {
cfg := &SecretsConfig{
Provider: "keychain",
Config: map[string]any{"service": "test-workflow-app"},
}
p, err := resolveSecretsProvider(cfg)
if err != nil {
t.Fatalf("resolveSecretsProvider keychain: %v", err)
}
if p.Name() != "keychain" {
t.Errorf("provider name = %q, want %q", p.Name(), "keychain")
}
}

func TestResolveSecretsProvider_KeychainMissingService(t *testing.T) {
cfg := &SecretsConfig{
Provider: "keychain",
Config: map[string]any{},
}
_, err := resolveSecretsProvider(cfg)
if err == nil {
t.Error("expected error when 'service' is missing")
}
}

// Ensure GitHubSecretsProvider satisfies secrets.Provider interface.
var _ secrets.Provider = (*secrets.GitHubSecretsProvider)(nil)
6 changes: 6 additions & 0 deletions cmd/wfctl/type_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,12 @@ func KnownModuleTypes() map[string]ModuleTypeInfo {
Stateful: false,
ConfigKeys: []string{"region", "accessKeyId", "secretAccessKey"},
},
"secrets.keychain": {
Type: "secrets.keychain",
Plugin: "secrets",
Stateful: false,
ConfigKeys: []string{"service"},
},

// ai plugin
"dynamic.component": {
Expand Down
3 changes: 3 additions & 0 deletions example/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ require (
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/danieljoos/wincred v1.2.3 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/deckarep/golang-set/v2 v2.8.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
Expand Down Expand Up @@ -111,6 +112,7 @@ require (
github.com/go-openapi/swag/typeutils v0.25.5 // indirect
github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/golobby/cast v1.3.3 // indirect
github.com/google/btree v1.1.3 // indirect
Expand Down Expand Up @@ -195,6 +197,7 @@ require (
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/zalando/go-keyring v0.2.8 // indirect
github.com/zeebo/xxh3 v1.1.0 // indirect
go.etcd.io/bbolt v1.4.3 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
Expand Down
6 changes: 6 additions & 0 deletions example/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ github.com/cucumber/godog v0.15.1 h1:rb/6oHDdvVZKS66hrhpjFQFHjthFSrQBCOI1LwshNTI
github.com/cucumber/godog v0.15.1/go.mod h1:qju+SQDewOljHuq9NSM66s0xEhogx0q30flfxL4WUk8=
github.com/cucumber/messages/go/v21 v21.0.1 h1:wzA0LxwjlWQYZd32VTlAVDTkW6inOFmSM+RuOwHZiMI=
github.com/cucumber/messages/go/v21 v21.0.1/go.mod h1:zheH/2HS9JLVFukdrsPWoPdmUtmYQAQPLk7w5vWsk5s=
github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
Expand Down Expand Up @@ -297,6 +299,8 @@ github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPE
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
Expand Down Expand Up @@ -660,6 +664,8 @@ github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs=
github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ require (
github.com/tliron/glsp v0.2.2
github.com/tochemey/goakt/v4 v4.1.1
github.com/xdg-go/scram v1.2.0
github.com/zalando/go-keyring v0.2.8
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0
go.opentelemetry.io/otel v1.43.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0
Expand Down Expand Up @@ -135,6 +136,7 @@ require (
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect
github.com/cucumber/messages/go/v21 v21.0.1 // indirect
github.com/danieljoos/wincred v1.2.3 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/deckarep/golang-set/v2 v2.8.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
Expand Down Expand Up @@ -169,6 +171,7 @@ require (
github.com/go-openapi/swag/typeutils v0.25.5 // indirect
github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/golobby/cast v1.3.3 // indirect
github.com/google/btree v1.1.3 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ github.com/cucumber/godog v0.15.1/go.mod h1:qju+SQDewOljHuq9NSM66s0xEhogx0q30flf
github.com/cucumber/messages/go/v21 v21.0.1 h1:wzA0LxwjlWQYZd32VTlAVDTkW6inOFmSM+RuOwHZiMI=
github.com/cucumber/messages/go/v21 v21.0.1/go.mod h1:zheH/2HS9JLVFukdrsPWoPdmUtmYQAQPLk7w5vWsk5s=
github.com/cucumber/messages/go/v22 v22.0.0/go.mod h1:aZipXTKc0JnjCsXrJnuZpWhtay93k7Rn3Dee7iyPJjs=
github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
Expand Down Expand Up @@ -347,6 +349,8 @@ github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPE
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
Expand Down Expand Up @@ -807,6 +811,8 @@ github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs=
github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
Expand Down
84 changes: 84 additions & 0 deletions module/secrets_keychain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package module

import (
"context"
"fmt"

"github.com/GoCodeAlone/modular"
"github.com/GoCodeAlone/workflow/secrets"
)

// SecretsKeychainModule provides an OS-keychain-backed secret provider as a modular service.
// It uses the macOS Keychain, Linux Secret Service, or Windows Credential Manager
// via github.com/zalando/go-keyring.
type SecretsKeychainModule struct {
name string
service string
provider *secrets.KeychainProvider
logger modular.Logger
}

// NewSecretsKeychainModule creates a new OS keychain secrets module.
func NewSecretsKeychainModule(name string) *SecretsKeychainModule {
return &SecretsKeychainModule{
name: name,
logger: &noopLogger{},
}
}

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

func (m *SecretsKeychainModule) Init(app modular.Application) error {
m.logger = app.Logger()
return nil
}

func (m *SecretsKeychainModule) ProvidesServices() []modular.ServiceProvider {
return []modular.ServiceProvider{
{
Name: m.name,
Description: "OS Keychain Secrets Provider",
Instance: m,
},
}
}

func (m *SecretsKeychainModule) RequiresServices() []modular.ServiceDependency {
return nil
}

// SetService sets the keychain service namespace.
func (m *SecretsKeychainModule) SetService(service string) { m.service = service }

// Start initializes the keychain provider.
func (m *SecretsKeychainModule) Start(_ context.Context) error {
if m.service == "" {
return fmt.Errorf("secrets.keychain: 'service' is required")
}
provider, err := secrets.NewKeychainProvider(m.service)
if err != nil {
return err
}
m.provider = provider
m.logger.Info("Keychain secrets provider started", "service", m.service)
return nil
}

// Stop is a no-op.
func (m *SecretsKeychainModule) Stop(_ context.Context) error {
m.logger.Info("Keychain secrets provider stopped")
return nil
}

// Provider returns the underlying secrets.Provider.
func (m *SecretsKeychainModule) Provider() secrets.Provider {
return m.provider
}

// Get retrieves a secret from the OS keychain.
func (m *SecretsKeychainModule) Get(ctx context.Context, key string) (string, error) {
if m.provider == nil {
return "", fmt.Errorf("secrets.keychain: provider not initialized")
}
return m.provider.Get(ctx, key)
}
17 changes: 12 additions & 5 deletions plugins/secrets/plugin.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Package secrets provides a plugin that registers secrets management modules:
// secrets.vault (HashiCorp Vault) and secrets.aws (AWS Secrets Manager),
// as well as the step.secret_rotate pipeline step type.
// secrets.vault (HashiCorp Vault), secrets.aws (AWS Secrets Manager),
// secrets.keychain (OS credential store), and the step.secret_rotate pipeline step type.
package secrets

import (
Expand All @@ -22,15 +22,15 @@ func New() *Plugin {
BaseNativePlugin: plugin.BaseNativePlugin{
PluginName: "secrets",
PluginVersion: "1.0.0",
PluginDescription: "Secrets management modules (Vault, AWS Secrets Manager)",
PluginDescription: "Secrets management modules (Vault, AWS Secrets Manager, OS Keychain)",
},
Manifest: plugin.PluginManifest{
Name: "secrets",
Version: "1.0.0",
Author: "GoCodeAlone",
Description: "Secrets management modules (Vault, AWS Secrets Manager)",
Description: "Secrets management modules (Vault, AWS Secrets Manager, OS Keychain)",
Tier: plugin.TierCore,
ModuleTypes: []string{"secrets.vault", "secrets.aws"},
ModuleTypes: []string{"secrets.vault", "secrets.aws", "secrets.keychain"},
StepTypes: []string{"step.secret_rotate"},
Capabilities: []plugin.CapabilityDecl{
{Name: "secrets-management", Role: "provider", Priority: 50},
Expand Down Expand Up @@ -85,6 +85,13 @@ func (p *Plugin) ModuleFactories() map[string]plugin.ModuleFactory {
}
return am
},
"secrets.keychain": func(name string, config map[string]any) modular.Module {
km := module.NewSecretsKeychainModule(name)
if svc, ok := config["service"].(string); ok && svc != "" {
km.SetService(svc)
}
return km
},
}
}

Expand Down
26 changes: 21 additions & 5 deletions plugins/secrets/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ func TestModuleFactories(t *testing.T) {
p := New()
factories := p.ModuleFactories()

for _, name := range []string{"secrets.vault", "secrets.aws"} {
for _, name := range []string{"secrets.vault", "secrets.aws", "secrets.keychain"} {
if _, ok := factories[name]; !ok {
t.Errorf("missing module factory: %s", name)
}
}
if len(factories) != 2 {
t.Errorf("expected 2 module factories, got %d", len(factories))
if len(factories) != 3 {
t.Errorf("expected 3 module factories, got %d", len(factories))
}
}

Expand Down Expand Up @@ -77,6 +77,22 @@ func TestAWSModuleFactory(t *testing.T) {
}
}

func TestKeychainModuleFactory(t *testing.T) {
p := New()
factories := p.ModuleFactories()
factory := factories["secrets.keychain"]

mod := factory("my-keychain", map[string]any{
"service": "my-app",
})
if mod == nil {
t.Fatal("keychain factory returned nil")
}
if mod.Name() != "my-keychain" {
t.Errorf("expected name my-keychain, got %s", mod.Name())
}
}

func TestPluginLoads(t *testing.T) {
p := New()
loader := plugin.NewPluginLoader(capability.NewRegistry(), schema.NewModuleSchemaRegistry())
Expand All @@ -85,7 +101,7 @@ func TestPluginLoads(t *testing.T) {
}

modules := loader.ModuleFactories()
if len(modules) != 2 {
t.Fatalf("expected 2 module factories after load, got %d", len(modules))
if len(modules) != 3 {
t.Fatalf("expected 3 module factories after load, got %d", len(modules))
}
}
Loading
Loading