Files
lazysecrets/internal/provider/fake/fake.go
T
kebandClaude Opus 5.5 777c8cb532
CI / Checks (pull_request) Successful in 42s
PR title / Conventional title (pull_request) Successful in 2s
feat: share env files in --demo
Providers whose containers are files give their contents through
provider.Contents, which sharing reads instead of the disk. The demo
repos build their env files from their keys, and the second demo
account has a key to share with (kv-shared-prod / env-share), so S → f
and opening the link work in --demo; the demo repos aren't on disk,
so opening asks where to save.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 13:11:59 +02:00

413 lines
15 KiB
Go

// Package fake is an in-memory provider used by tests and --demo mode.
package fake
import (
"cmp"
"context"
"errors"
"fmt"
"strings"
"time"
"git.kebcloud.no/keb/lazysecrets/internal/provider"
)
// Provider serves a fixed tree of sources, containers and objects,
// optionally sleeping before every call to simulate network latency.
type Provider struct {
kind provider.Kind
id, label string
delay time.Duration
sources []provider.Source
containers map[string][]provider.Container // by source ID
objects map[listKey][]provider.Object
errs map[listKey]error
values map[string]string // by object ID
versions map[string][]provider.Object // by object ID
details map[string][]provider.Attr // by container ID
}
type listKey struct {
container string
typ provider.ObjectType
}
var _ provider.Provider = (*Provider)(nil)
func newProvider(kind provider.Kind, delay time.Duration) *Provider {
return &Provider{
kind: kind,
delay: delay,
containers: map[string][]provider.Container{},
objects: map[listKey][]provider.Object{},
errs: map[listKey]error{},
values: map[string]string{},
versions: map[string][]provider.Object{},
}
}
func (p *Provider) Kind() provider.Kind { return p.kind }
func (p *Provider) ID() string {
if p.id != "" {
return p.id
}
return string(p.kind)
}
func (p *Provider) Label() string {
if p.label != "" {
return p.label
}
return string(p.kind)
}
// As renames the provider, to simulate several accounts of one kind.
func (p *Provider) As(id, label string) *Provider {
p.id, p.label = id, label
return p
}
func (p *Provider) Sources(ctx context.Context) ([]provider.Source, error) {
if err := p.wait(ctx); err != nil {
return nil, err
}
return p.sources, nil
}
func (p *Provider) Containers(ctx context.Context, src provider.Source) ([]provider.Container, error) {
if err := p.wait(ctx); err != nil {
return nil, err
}
return p.containers[src.ID], nil
}
func (p *Provider) Objects(ctx context.Context, c provider.Container, t provider.ObjectType) ([]provider.Object, error) {
if err := p.wait(ctx); err != nil {
return nil, err
}
k := listKey{p.containerID(c), t}
if err := p.errs[k]; err != nil {
return nil, err
}
return p.objects[k], nil
}
// Describe returns o unchanged; the fixtures already carry their details.
func (p *Provider) Describe(ctx context.Context, o provider.Object) (provider.Object, error) {
return o, ctx.Err()
}
func (p *Provider) Versions(ctx context.Context, o provider.Object) ([]provider.Object, error) {
if err := p.wait(ctx); err != nil {
return nil, err
}
if p.kind != provider.KindAzure { // only Key Vault keeps versions
return nil, provider.ErrUnsupported
}
vs := p.versions[o.ID]
if len(vs) == 0 { // every object has at least its current version
return []provider.Object{o}, nil
}
out := make([]provider.Object, len(vs))
for i, v := range vs {
v.Container, v.Type, v.ID, v.Name, v.Reference, v.URL = o.Container, o.Type, o.ID, o.Name, o.Reference, o.URL
out[i] = v
}
return out, nil
}
func (p *Provider) Value(ctx context.Context, o provider.Object) (provider.SecretValue, error) {
if err := p.wait(ctx); err != nil {
return provider.SecretValue{}, err
}
if o.ID == "" { // built from a link, like the real providers read it
o.ID = p.containerID(o.Container) + "/" + strings.ToLower(string(cmp.Or(o.Type, provider.TypeSecret))) + "/" + o.Name
}
v, ok := p.values[o.ID+"@"+o.Version]
if !ok {
v, ok = p.values[o.ID]
}
if !ok {
return provider.SecretValue{}, fmt.Errorf("%s %q not found", strings.ToLower(string(o.Type)), o.Name)
}
return provider.NewSecretValue([]byte(v)), nil
}
// Locate finds the source holding the vault (by host) or the workspace.
func (p *Provider) Locate(ctx context.Context, host string) (string, bool, error) {
if err := p.wait(ctx); err != nil {
return "", false, err
}
for _, src := range p.sources {
if p.kind == provider.KindDatabricks && src.Detail == host {
return src.ID, true, nil
}
for _, c := range p.containers[src.ID] {
if c.Detail == "https://"+host {
return src.ID, true, nil
}
}
}
return "", false, nil
}
func (p *Provider) wait(ctx context.Context) error {
if p.delay <= 0 {
return ctx.Err()
}
t := time.NewTimer(p.delay)
defer t.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-t.C:
return nil
}
}
const portal = "https://portal.azure.com/#resource"
// containerID finds a container by ID, or by vault URL like the real Azure
// provider, which only needs the URL (vaults opened from links).
func (p *Provider) containerID(c provider.Container) string {
for _, cs := range p.containers {
for _, x := range cs {
if x.ID == c.ID {
return x.ID
}
}
}
// Not one listed here (built from a link): by vault URL, or a scope
// by name in its workspace.
for _, cs := range p.containers {
for _, x := range cs {
if c.Detail != "" && strings.TrimSuffix(x.Detail, "/") == strings.TrimSuffix(c.Detail, "/") {
return x.ID
}
if c.Detail == "" && c.Name == x.Name && c.Source.ID != "" && c.Source.ID == x.Source.ID {
return x.ID
}
}
}
return c.ID
}
func (p *Provider) addSource(id, name, detail string) provider.Source {
s := provider.Source{Kind: p.kind, ID: id, Name: name, Detail: detail}
if p.kind == provider.KindAzure {
s.URL = portal + "/subscriptions/" + id + "/overview"
} else {
s.URL = "https://" + detail
}
p.sources = append(p.sources, s)
return s
}
func (p *Provider) addContainer(src provider.Source, name, detail string, types ...provider.ObjectType) provider.Container {
c := provider.Container{Source: src, ID: src.ID + "/" + name, Name: name, Detail: detail, Types: types}
vault := name
if p.kind == provider.KindDatabricks {
vault, _ = strings.CutPrefix(detail, "Azure Key Vault-backed: ")
}
if p.kind == provider.KindAzure || (p.kind == provider.KindDatabricks && vault != detail) {
c.URL = portal + "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.KeyVault/vaults/" + vault + "/overview"
}
p.containers[src.ID] = append(p.containers[src.ID], c)
return c
}
func (p *Provider) add(c provider.Container, t provider.ObjectType, o provider.Object, value string) provider.Object {
o.Container = c
o.Type = t
o.ID = c.ID + "/" + strings.ToLower(string(t)) + "/" + o.Name
if base, ok := strings.CutSuffix(c.URL, "/overview"); ok {
o.URL = base + map[provider.ObjectType]string{
provider.TypeSecret: "/secrets", provider.TypeKey: "/keys", provider.TypeCertificate: "/certificates",
}[t]
}
k := listKey{c.ID, t}
p.objects[k] = append(p.objects[k], o)
p.values[o.ID] = value
return o
}
func (p *Provider) fail(c provider.Container, err error, types ...provider.ObjectType) {
for _, t := range types {
p.errs[listKey{c.ID, t}] = err
}
}
var (
base = time.Date(2026, 9, 1, 9, 30, 0, 0, time.UTC)
yes = true
no = false
)
// ErrForbidden mimics a data-plane permission error from a backend.
var ErrForbidden = errors.New("403 Forbidden: caller lacks permission to list secrets")
// ErrForbiddenKeys mimics a vault where the caller can read secrets but not keys.
var ErrForbiddenKeys = errors.New("403 Forbidden: caller lacks permission to list keys")
const (
demoPublicKey = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAdemo\n-----END PUBLIC KEY-----"
demoCert = "-----BEGIN CERTIFICATE-----\nMIIBszCCAVmgAwIBAgIUDEMO\nDEMODEMODEMO\n-----END CERTIFICATE-----"
)
var allTypes = []provider.ObjectType{provider.TypeSecret, provider.TypeKey, provider.TypeCertificate}
// Azure returns a fake Azure provider with two subscriptions.
func Azure(delay time.Duration) *Provider {
p := newProvider(provider.KindAzure, delay)
secretRef := func(vault, name string) string {
return fmt.Sprintf("@Microsoft.KeyVault(SecretUri=https://%s.vault.azure.net/secrets/%s)", vault, name)
}
objectURL := func(vault, kind, name string) string {
return fmt.Sprintf("https://%s.vault.azure.net/%s/%s", vault, kind, name)
}
prod := p.addSource("sub-prod", "platform-prod", "tenant: example.onmicrosoft.com")
kv := p.addContainer(prod, "kv-app-prod", "https://kv-app-prod.vault.azure.net", allTypes...)
dbPassword := p.add(kv, provider.TypeSecret, provider.Object{
Name: "db-password", Version: "3f9c2a", Enabled: &yes, Updated: base,
Expires: base.AddDate(1, 0, 0), ContentType: "text/plain",
Tags: map[string]string{"owner": "data", "env": "prod"}, Reference: secretRef("kv-app-prod", "db-password"),
}, "correct-horse-battery-staple")
p.versions[dbPassword.ID] = []provider.Object{
{Version: "3f9c2a", Enabled: &yes, Updated: base, Expires: base.AddDate(1, 0, 0), ContentType: "text/plain"},
{Version: "91d3e0", Enabled: &yes, Updated: base.AddDate(0, -6, 0), Expires: base.AddDate(0, -1, 0)},
{Version: "0a77c4", Enabled: &no, Updated: base.AddDate(-1, 0, 0)},
}
p.values[dbPassword.ID+"@91d3e0"] = "previous-password"
p.values[dbPassword.ID+"@0a77c4"] = "original-password"
p.add(kv, provider.TypeSecret, provider.Object{
Name: "api-key", Version: "a17b04", Enabled: &yes, Updated: base.AddDate(0, -2, 0),
Reference: secretRef("kv-app-prod", "api-key"),
}, "sk_live_51HxDEMO0000000000000000")
p.add(kv, provider.TypeSecret, provider.Object{
Name: "legacy-token", Version: "0c11de", Enabled: &no, Updated: base.AddDate(-1, 0, 0),
Reference: secretRef("kv-app-prod", "legacy-token"),
}, "deprecated")
p.add(kv, provider.TypeKey, provider.Object{
Name: "signing-key", Version: "5e21b7", Enabled: &yes, Updated: base.AddDate(0, -3, 0),
Attrs: []provider.Attr{
{Label: "key type", Value: "RSA 2048"},
{Label: "ops", Value: "sign, verify"},
},
Reference: objectURL("kv-app-prod", "keys", "signing-key"),
}, demoPublicKey)
p.add(kv, provider.TypeKey, provider.Object{
Name: "data-encryption", Version: "c40f19", Enabled: &yes, Updated: base.AddDate(0, -5, 0),
Attrs: []provider.Attr{
{Label: "key type", Value: "EC P-256"},
{Label: "ops", Value: "wrapKey, unwrapKey"},
},
Reference: objectURL("kv-app-prod", "keys", "data-encryption"),
}, demoPublicKey)
p.add(kv, provider.TypeCertificate, provider.Object{
Name: "app-tls", Version: "77e0aa", Enabled: &yes, Updated: base.AddDate(0, -1, 0),
Expires: base.AddDate(0, 11, 0), ContentType: "application/x-pem-file",
Attrs: []provider.Attr{
{Label: "subject", Value: "CN=app.example.com"},
{Label: "issuer", Value: "CN=Example Issuing CA"},
{Label: "thumbprint", Value: "3B:1F:9A:0C:44:E2:7D:15"},
{Label: "SANs", Value: "app.example.com, www.app.example.com"},
{Label: "valid from", Value: "2026-08-01 09:30"},
},
Reference: objectURL("kv-app-prod", "certificates", "app-tls"),
}, demoCert)
p.add(kv, provider.TypeCertificate, provider.Object{
Name: "old-tls", Version: "19ab3c", Enabled: &yes, Updated: base.AddDate(-1, 0, 0),
Expires: base.AddDate(0, -6, 0), ContentType: "application/x-pkcs12",
Attrs: []provider.Attr{
{Label: "subject", Value: "CN=old.example.com"},
{Label: "issuer", Value: "CN=Example Issuing CA"},
{Label: "thumbprint", Value: "A0:77:12:DE:9B:03:C6:58"},
},
Reference: objectURL("kv-app-prod", "certificates", "old-tls"),
}, demoCert)
shared := p.addContainer(prod, "kv-shared-prod", "https://kv-shared-prod.vault.azure.net", allTypes...)
// A key to share env files with (S → f): random, as share.keyRules wants.
p.add(shared, provider.TypeSecret, provider.Object{
Name: "env-share", Enabled: &yes, Updated: base, Reference: secretRef("kv-shared-prod", "env-share"),
}, "q8Zt3VwL1mXc7Rk2Pj9Ys4Hn6Bf0Ga5Ud+Te/OiWlE=")
p.add(shared, provider.TypeSecret, provider.Object{
Name: "smtp-password", Enabled: &yes, Updated: base, Reference: secretRef("kv-shared-prod", "smtp-password"),
}, "smtp-demo-password")
p.fail(shared, ErrForbiddenKeys, provider.TypeKey)
locked := p.addContainer(prod, "kv-restricted", "https://kv-restricted.vault.azure.net", allTypes...)
p.fail(locked, ErrForbidden, allTypes...)
dev := p.addSource("sub-dev", "platform-dev", "tenant: example.onmicrosoft.com")
kvDev := p.addContainer(dev, "kv-app-dev", "https://kv-app-dev.vault.azure.net", allTypes...)
p.add(kvDev, provider.TypeSecret, provider.Object{
Name: "db-password", Enabled: &yes, Updated: base, Reference: secretRef("kv-app-dev", "db-password"),
}, "dev-password")
return p
}
// AzureOps returns a second fake Azure account. It shares platform-prod
// with Azure but sees a different vault there, and has a subscription of
// its own, like an admin account next to a personal one.
func AzureOps(delay time.Duration) *Provider {
p := newProvider(provider.KindAzure, delay)
secretRef := func(vault, name string) string {
return fmt.Sprintf("@Microsoft.KeyVault(SecretUri=https://%s.vault.azure.net/secrets/%s)", vault, name)
}
prod := p.addSource("sub-prod", "platform-prod", "tenant: example.onmicrosoft.com")
breakGlass := p.addContainer(prod, "kv-breakglass", "https://kv-breakglass.vault.azure.net", allTypes...)
p.add(breakGlass, provider.TypeSecret, provider.Object{
Name: "emergency-admin", Enabled: &yes, Updated: base.AddDate(0, -4, 0),
Tags: map[string]string{"owner": "ops"}, Reference: secretRef("kv-breakglass", "emergency-admin"),
}, "break-glass-demo-password")
shared := p.addContainer(prod, "kv-shared-prod", "https://kv-shared-prod.vault.azure.net", allTypes...)
p.add(shared, provider.TypeSecret, provider.Object{
Name: "smtp-password", Enabled: &yes, Updated: base, Reference: secretRef("kv-shared-prod", "smtp-password"),
}, "smtp-demo-password")
// Unlike the first account, ops may read keys here.
p.add(shared, provider.TypeKey, provider.Object{
Name: "backup-encryption", Version: "e9a102", Enabled: &yes, Updated: base.AddDate(0, -2, 0),
Attrs: []provider.Attr{{Label: "key type", Value: "RSA-HSM 3072"}, {Label: "ops", Value: "wrapKey, unwrapKey"}},
Reference: "https://kv-shared-prod.vault.azure.net/keys/backup-encryption",
}, demoPublicKey)
sandbox := p.addSource("sub-ops", "ops-sandbox", "tenant: example.onmicrosoft.com")
kv := p.addContainer(sandbox, "kv-ops-tools", "https://kv-ops-tools.vault.azure.net", allTypes...)
p.add(kv, provider.TypeSecret, provider.Object{
Name: "pagerduty-token", Enabled: &yes, Updated: base.AddDate(0, -1, 0), Reference: secretRef("kv-ops-tools", "pagerduty-token"),
}, "pd-demo-token")
return p
}
// Databricks returns a fake Databricks provider with two workspace profiles,
// including an Azure Key Vault-backed scope. Scopes hold secrets only.
func Databricks(delay time.Duration) *Provider {
p := newProvider(provider.KindDatabricks, delay)
secret := func(c provider.Container, key, value string) {
p.add(c, provider.TypeSecret, provider.Object{
Name: key, Updated: base, Reference: fmt.Sprintf("{{secrets/%s/%s}}", c.Name, key),
}, value)
}
prod := p.addSource("dbx-prod", "prod", "adb-1111111111111111.11.azuredatabricks.net")
app := p.addContainer(prod, "app", "Databricks-backed")
secret(app, "storage-key", "storage-demo-key")
secret(app, "sp-client-secret", "sp-demo-secret")
akv := p.addContainer(prod, "kv-app-prod", "Azure Key Vault-backed: kv-app-prod")
secret(akv, "db-password", "correct-horse-battery-staple")
dev := p.addSource("dbx-dev", "dev", "adb-2222222222222222.2.azuredatabricks.net")
devApp := p.addContainer(dev, "app", "Databricks-backed")
secret(devApp, "storage-key", "dev-storage-key")
return p
}