| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | package secretresolver |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "fmt" |
| 8 | "regexp" |
| 9 | "strings" |
| 10 | ) |
| 11 | |
| 12 | type StoreRefResolver func(ctx context.Context, ref, original string) (string, error) |
| 13 | |
| 14 | // reSecretRef matches ${...} references in strings. |
| 15 | var reSecretRef = regexp.MustCompile(`\$\{([^}]+)\}`) |
| 16 | |
| 17 | // Resolve walks the config map and resolves all secret references in string values. |
| 18 | func (r *Resolver) Resolve(cfg map[string]any) error { |
| 19 | return r.ResolveWithStoreResolver(context.Background(), cfg, nil) |
| 20 | } |
| 21 | |
| 22 | func (r *Resolver) ResolveWithStoreResolver(ctx context.Context, cfg map[string]any, storeResolver StoreRefResolver) error { |
| 23 | if r == nil { |
| 24 | return fmt.Errorf("secret resolver is nil") |
| 25 | } |
| 26 | if ctx == nil { |
| 27 | ctx = context.Background() |
| 28 | } |
| 29 | r.ensureDefaults() |
| 30 | return r.resolveMap(ctx, cfg, storeResolver) |
| 31 | } |
| 32 | |
| 33 | func (r *Resolver) resolveMap(ctx context.Context, m map[string]any, storeResolver StoreRefResolver) error { |
| 34 | for k, v := range m { |
| 35 | // skip internal keys (__key__) |
| 36 | if isInternalKey(k) { |
| 37 | continue |
| 38 | } |
| 39 | resolved, err := r.resolveValue(ctx, v, storeResolver) |
| 40 | if err != nil { |
| 41 | return err |
| 42 | } |
| 43 | m[k] = resolved |
| 44 | } |
| 45 | return nil |
| 46 | } |
| 47 | |
| 48 | func (r *Resolver) resolveMapAny(ctx context.Context, m map[any]any, storeResolver StoreRefResolver) error { |
| 49 | for k, v := range m { |
| 50 | s, ok := k.(string) |
| 51 | if ok && isInternalKey(s) { |
| 52 | continue |
| 53 | } |
| 54 | resolved, err := r.resolveValue(ctx, v, storeResolver) |
| 55 | if err != nil { |
| 56 | return err |
| 57 | } |
| 58 | m[k] = resolved |
| 59 | } |
| 60 | return nil |
| 61 | } |
| 62 | |
| 63 | func (r *Resolver) resolveSlice(ctx context.Context, s []any, storeResolver StoreRefResolver) error { |
| 64 | for i, v := range s { |
| 65 | resolved, err := r.resolveValue(ctx, v, storeResolver) |
| 66 | if err != nil { |
| 67 | return err |
| 68 | } |
| 69 | s[i] = resolved |
| 70 | } |
| 71 | return nil |
| 72 | } |
| 73 | |
| 74 | func (r *Resolver) resolveValue(ctx context.Context, v any, storeResolver StoreRefResolver) (any, error) { |
| 75 | switch val := v.(type) { |
| 76 | case string: |
| 77 | return r.resolveString(ctx, val, storeResolver) |
| 78 | case map[string]any: |
| 79 | return val, r.resolveMap(ctx, val, storeResolver) |
| 80 | case map[any]any: |
| 81 | return val, r.resolveMapAny(ctx, val, storeResolver) |
| 82 | case []any: |
| 83 | return val, r.resolveSlice(ctx, val, storeResolver) |
| 84 | default: |
| 85 | return v, nil |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | func (r *Resolver) resolveString(ctx context.Context, s string, storeResolver StoreRefResolver) (string, error) { |
| 90 | if !strings.Contains(s, "${") { |
| 91 | return s, nil |
| 92 | } |
| 93 | |
| 94 | var resolveErr error |
| 95 | |
| 96 | result := reSecretRef.ReplaceAllStringFunc(s, func(match string) string { |
| 97 | if resolveErr != nil { |
| 98 | return match |
| 99 | } |
| 100 | |
| 101 | // extract inner content between ${ and } |
| 102 | inner := match[2 : len(match)-1] |
| 103 | |
| 104 | val, err := r.resolveRef(ctx, inner, match, storeResolver) |
| 105 | if err != nil { |
| 106 | resolveErr = err |
| 107 | return match |
| 108 | } |
| 109 | return val |
| 110 | }) |
| 111 | |
| 112 | if resolveErr != nil { |
| 113 | return "", resolveErr |
| 114 | } |
| 115 | return result, nil |
| 116 | } |
| 117 | |
| 118 | func (r *Resolver) resolveRef(ctx context.Context, ref, original string, storeResolver StoreRefResolver) (string, error) { |
| 119 | scheme, name, hasScheme := strings.Cut(ref, ":") |
| 120 | |
| 121 | if !hasScheme { |
| 122 | // no scheme — leave unchanged |
| 123 | return original, nil |
| 124 | } |
| 125 | |
| 126 | if scheme == "store" { |
| 127 | if storeResolver == nil { |
| 128 | return "", fmt.Errorf("resolving secret '%s': secretstore resolver is not configured", original) |
| 129 | } |
| 130 | return storeResolver(ctx, name, original) |
| 131 | } |
| 132 | |
| 133 | provider, ok := r.providers[scheme] |
| 134 | if !ok { |
| 135 | return "", fmt.Errorf("resolving secret '%s': unknown secret provider '%s'", original, scheme) |
| 136 | } |
| 137 | |
| 138 | return provider(ctx, name, original) |
| 139 | } |
| 140 | |
| 141 | func isInternalKey(k string) bool { |
| 142 | return strings.HasPrefix(k, "__") && strings.HasSuffix(k, "__") |
| 143 | } |