From 05d507e7bd4c4eb1c330a650b3630213929644ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl-Einar=20Bergstr=C3=B8m?= Date: Thu, 15 Oct 2026 08:54:22 +0200 Subject: [PATCH] refactor: one place for Key Vault collections and paging in the providers - ObjectType.Collection names a type's segment in Key Vault URLs and links (secrets, keys, certificates), instead of four copies in ref, azure and fake. - Azure lists through one pager helper instead of eight hand-written page loops; Value is VersionedValue without the version, rather than the two calling each other. - Databricks' Key Vault fallback check lives in fallbackVault. - The repo provider reads .lazysecrets.yaml in one place, builds plain and sops objects in one loop, and parses a file's sops metadata from the bytes Objects already read. --- internal/provider/azure/azure.go | 216 +++++++++------------ internal/provider/azure/convert.go | 24 +-- internal/provider/azure/details.go | 18 +- internal/provider/databricks/databricks.go | 25 +-- internal/provider/fake/fake.go | 14 +- internal/provider/provider.go | 14 ++ internal/provider/repo/repo.go | 84 ++++---- internal/ref/ref.go | 20 +- internal/share/rules.go | 6 +- 9 files changed, 209 insertions(+), 212 deletions(-) diff --git a/internal/provider/azure/azure.go b/internal/provider/azure/azure.go index 69b8144..0fb31b9 100644 --- a/internal/provider/azure/azure.go +++ b/internal/provider/azure/azure.go @@ -16,6 +16,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph" @@ -99,13 +100,7 @@ func (p *Provider) Label() string { return cmp.Or(p.label, string(provider.KindA // Sources lists the subscriptions the signed-in identity can see, skipping // disabled and deleted ones. func (p *Provider) Sources(ctx context.Context) ([]provider.Source, error) { - var out []provider.Source - pager := p.subs.NewListPager(nil) - for pager.More() { - page, err := pager.NextPage(ctx) - if err != nil { - return nil, cleanError(err) - } + out, err := all(ctx, p.subs.NewListPager(nil), func(page armsubscriptions.ClientListResponse) (srcs []provider.Source) { for _, s := range page.Value { switch deref(s.State) { case armsubscriptions.SubscriptionStateDisabled, armsubscriptions.SubscriptionStateDeleted: @@ -118,7 +113,7 @@ func (p *Provider) Sources(ctx context.Context) ([]provider.Source, error) { p.mu.Lock() p.tenants[id] = tenant p.mu.Unlock() - out = append(out, provider.Source{ + srcs = append(srcs, provider.Source{ Kind: provider.KindAzure, ID: id, Name: deref(s.DisplayName), @@ -126,6 +121,10 @@ func (p *Provider) Sources(ctx context.Context) ([]provider.Source, error) { URL: PortalResourceURL(tenant, "/subscriptions/"+id, "overview"), }) } + return srcs + }) + if err != nil { + return nil, err } slices.SortFunc(out, func(a, b provider.Source) int { return cmp.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name)) }) if len(p.opts.Vaults) > 0 { @@ -230,22 +229,29 @@ func (p *Provider) armVaults(ctx context.Context, src provider.Source) ([]provid if err != nil { return nil, err } - var out []provider.Container - pager := client.NewListBySubscriptionPager(nil) + return all(ctx, client.NewListBySubscriptionPager(nil), func(page armkeyvault.VaultsClientListBySubscriptionResponse) (vaults []provider.Container) { + for _, v := range page.Value { + if v.Properties == nil || v.Properties.VaultURI == nil { + continue + } + vaults = append(vaults, provider.Container{ + Source: src, ID: deref(v.ID), Name: deref(v.Name), + Detail: *v.Properties.VaultURI, Types: objectTypes, Attrs: armInfo(v).attrs(), + }) + } + return vaults + }) +} + +// all lists every page, items picking out what to keep of each. +func all[P, T any](ctx context.Context, pager *runtime.Pager[P], items func(P) []T) ([]T, error) { + var out []T for pager.More() { page, err := pager.NextPage(ctx) if err != nil { return nil, cleanError(err) } - for _, v := range page.Value { - if v.Properties == nil || v.Properties.VaultURI == nil { - continue - } - out = append(out, provider.Container{ - Source: src, ID: deref(v.ID), Name: deref(v.Name), - Detail: *v.Properties.VaultURI, Types: objectTypes, Attrs: armInfo(v).attrs(), - }) - } + out = append(out, items(page)...) } return out, nil } @@ -288,51 +294,40 @@ func (p *Provider) Objects(ctx context.Context, c provider.Container, t provider var out []provider.Object switch t { case provider.TypeSecret: - pager := vc.secrets.NewListSecretPropertiesPager(nil) - for pager.More() { - page, err := pager.NextPage(ctx) - if err != nil { - return nil, cleanError(err) - } + tenant := p.tenant(c.Source.ID) + out, err = all(ctx, vc.secrets.NewListSecretPropertiesPager(nil), func(page azsecrets.ListSecretPropertiesResponse) (objs []provider.Object) { for _, s := range page.Value { // Certificate-backing secrets appear under Certificates. - if s.ID == nil || deref(s.Managed) { - continue + if s.ID != nil && !deref(s.Managed) { + objs = append(objs, secretObject(c, s, tenant)) } - out = append(out, secretObject(c, s, p.tenant(c.Source.ID))) } - } + return objs + }) case provider.TypeKey: - pager := vc.keys.NewListKeyPropertiesPager(nil) - for pager.More() { - page, err := pager.NextPage(ctx) - if err != nil { - return nil, cleanError(err) - } + out, err = all(ctx, vc.keys.NewListKeyPropertiesPager(nil), func(page azkeys.ListKeyPropertiesResponse) (objs []provider.Object) { for _, k := range page.Value { - if k.KID == nil || deref(k.Managed) { - continue + if k.KID != nil && !deref(k.Managed) { + objs = append(objs, keyObject(c, k)) } - out = append(out, keyObject(c, k)) } - } + return objs + }) case provider.TypeCertificate: - pager := vc.certs.NewListCertificatePropertiesPager(nil) - for pager.More() { - page, err := pager.NextPage(ctx) - if err != nil { - return nil, cleanError(err) - } + out, err = all(ctx, vc.certs.NewListCertificatePropertiesPager(nil), func(page azcertificates.ListCertificatePropertiesResponse) (objs []provider.Object) { for _, cp := range page.Value { - if cp.ID == nil { - continue + if cp.ID != nil { + objs = append(objs, certObject(c, cp)) } - out = append(out, certObject(c, cp)) } - } + return objs + }) default: return nil, provider.ErrUnsupported } + if err != nil { + return nil, err + } slices.SortFunc(out, func(a, b provider.Object) int { return cmp.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name)) }) return out, nil } @@ -353,7 +348,7 @@ func (p *Provider) Describe(ctx context.Context, o provider.Object) (provider.Ob if resp.Key != nil { if resp.Key.KID != nil { o.Version = resp.Key.KID.Version() - o.URL = portalAssetURL(p.tenant(o.Container.Source.ID), "Key", string(*resp.Key.KID)) + o.URL = portalAssetURL(p.tenant(o.Container.Source.ID), o.Type, string(*resp.Key.KID)) } o.Attrs = keyAttrs(resp.Key) } @@ -364,7 +359,7 @@ func (p *Provider) Describe(ctx context.Context, o provider.Object) (provider.Ob } if resp.ID != nil { o.Version = resp.ID.Version() - o.URL = portalAssetURL(p.tenant(o.Container.Source.ID), "Certificate", string(*resp.ID)) + o.URL = portalAssetURL(p.tenant(o.Container.Source.ID), o.Type, string(*resp.ID)) } if pol := resp.Policy; pol != nil && pol.SecretProperties != nil { o.ContentType = deref(pol.SecretProperties.ContentType) @@ -381,56 +376,46 @@ func (p *Provider) Versions(ctx context.Context, o provider.Object) ([]provider. if err != nil { return nil, err } + tenant := p.tenant(o.Container.Source.ID) var out []provider.Object switch o.Type { case provider.TypeSecret: - pager := vc.secrets.NewListSecretPropertiesVersionsPager(o.Name, nil) - for pager.More() { - page, err := pager.NextPage(ctx) - if err != nil { - return nil, cleanError(err) - } + out, err = all(ctx, vc.secrets.NewListSecretPropertiesVersionsPager(o.Name, nil), func(page azsecrets.ListSecretPropertiesVersionsResponse) (objs []provider.Object) { for _, s := range page.Value { if s.ID != nil { - out = append(out, secretObject(o.Container, s, p.tenant(o.Container.Source.ID))) + objs = append(objs, secretObject(o.Container, s, tenant)) } } - } + return objs + }) case provider.TypeKey: - pager := vc.keys.NewListKeyPropertiesVersionsPager(o.Name, nil) - for pager.More() { - page, err := pager.NextPage(ctx) - if err != nil { - return nil, cleanError(err) - } + out, err = all(ctx, vc.keys.NewListKeyPropertiesVersionsPager(o.Name, nil), func(page azkeys.ListKeyPropertiesVersionsResponse) (objs []provider.Object) { for _, k := range page.Value { if k.KID != nil { - out = append(out, keyObject(o.Container, k)) + objs = append(objs, keyObject(o.Container, k)) } } - } + return objs + }) case provider.TypeCertificate: - pager := vc.certs.NewListCertificatePropertiesVersionsPager(o.Name, nil) - for pager.More() { - page, err := pager.NextPage(ctx) - if err != nil { - return nil, cleanError(err) - } + out, err = all(ctx, vc.certs.NewListCertificatePropertiesVersionsPager(o.Name, nil), func(page azcertificates.ListCertificatePropertiesVersionsResponse) (objs []provider.Object) { for _, cp := range page.Value { if cp.ID != nil { - out = append(out, certObject(o.Container, cp)) + objs = append(objs, certObject(o.Container, cp)) } } - } + return objs + }) default: return nil, provider.ErrUnsupported } - tenant := p.tenant(o.Container.Source.ID) + if err != nil { + return nil, err + } for i := range out { out[i].Reference = versionedReference(out[i]) if o.Type != provider.TypeSecret { // secretObject's is the version's already - kind := map[provider.ObjectType]string{provider.TypeKey: "Key", provider.TypeCertificate: "Certificate"}[o.Type] - out[i].URL = portalAssetURL(tenant, kind, out[i].ID+"/"+out[i].Version) // the version's page + out[i].URL = portalAssetURL(tenant, o.Type, out[i].ID+"/"+out[i].Version) // the version's page } } slices.SortFunc(out, func(a, b provider.Object) int { return b.Updated.Compare(a.Updated) }) @@ -452,56 +437,49 @@ func versionedReference(o provider.Object) string { // Value returns a secret's value, a key's public key or a certificate's // public certificate, as of o.Version (latest if empty). func (p *Provider) Value(ctx context.Context, o provider.Object) (provider.SecretValue, error) { - vc, err := p.clients(o.Container.Detail) - if err != nil { - return provider.SecretValue{}, err - } - switch o.Type { - case provider.TypeSecret: - v, _, err := p.VersionedValue(ctx, o) - return v, err - case provider.TypeKey: - resp, err := vc.keys.GetKey(ctx, o.Name, o.Version, nil) - if err != nil { - return provider.SecretValue{}, cleanError(err) - } - pemKey, err := publicKeyPEM(resp.Key) - if err != nil { - return provider.SecretValue{}, err - } - return provider.NewSecretValue([]byte(pemKey)), nil - case provider.TypeCertificate: - resp, err := vc.certs.GetCertificate(ctx, o.Name, o.Version, nil) - if err != nil { - return provider.SecretValue{}, cleanError(err) - } - if len(resp.CER) == 0 { - return provider.SecretValue{}, errors.New("certificate has no public certificate yet (pending issuance?)") - } - return provider.NewSecretValue([]byte(certPEM(resp.CER))), nil - } - return provider.SecretValue{}, provider.ErrUnsupported + v, _, err := p.VersionedValue(ctx, o) + return v, err } // VersionedValue is Value, with the version read. func (p *Provider) VersionedValue(ctx context.Context, o provider.Object) (provider.SecretValue, string, error) { - if o.Type != provider.TypeSecret { - v, err := p.Value(ctx, o) - return v, o.Version, err - } vc, err := p.clients(o.Container.Detail) if err != nil { return provider.SecretValue{}, "", err } - resp, err := vc.secrets.GetSecret(ctx, o.Name, o.Version, nil) - if err != nil { - return provider.SecretValue{}, "", cleanError(err) + var text string + switch o.Type { + case provider.TypeSecret: + resp, err := vc.secrets.GetSecret(ctx, o.Name, o.Version, nil) + if err != nil { + return provider.SecretValue{}, "", cleanError(err) + } + version := o.Version + if resp.ID != nil { + version = resp.ID.Version() + } + return provider.NewSecretValue([]byte(deref(resp.Value))), version, nil + case provider.TypeKey: + resp, err := vc.keys.GetKey(ctx, o.Name, o.Version, nil) + if err != nil { + return provider.SecretValue{}, "", cleanError(err) + } + if text, err = publicKeyPEM(resp.Key); err != nil { + return provider.SecretValue{}, "", err + } + case provider.TypeCertificate: + resp, err := vc.certs.GetCertificate(ctx, o.Name, o.Version, nil) + if err != nil { + return provider.SecretValue{}, "", cleanError(err) + } + if len(resp.CER) == 0 { + return provider.SecretValue{}, "", errors.New("certificate has no public certificate yet (pending issuance?)") + } + text = certPEM(resp.CER) + default: + return provider.SecretValue{}, "", provider.ErrUnsupported } - version := o.Version - if resp.ID != nil { - version = resp.ID.Version() - } - return provider.NewSecretValue([]byte(deref(resp.Value))), version, nil + return provider.NewSecretValue([]byte(text)), o.Version, nil } var ( diff --git a/internal/provider/azure/convert.go b/internal/provider/azure/convert.go index d85b8d4..9abbf3d 100644 --- a/internal/provider/azure/convert.go +++ b/internal/provider/azure/convert.go @@ -55,8 +55,8 @@ func PortalResourceURL(tenant, resourceID, blade string) string { // portalAssetURL links to a Key Vault object's page: a version's with // one in objectID; a secret's own (its versions) without. -func portalAssetURL(tenant, kind, objectID string) string { - return portal + tenantPrefix(tenant) + "asset/Microsoft_Azure_KeyVault/" + kind + "/" + objectID +func portalAssetURL(tenant string, t provider.ObjectType, objectID string) string { + return portal + tenantPrefix(tenant) + "asset/Microsoft_Azure_KeyVault/" + string(t) + "/" + objectID } func tenantPrefix(tenant string) string { @@ -73,19 +73,13 @@ func ObjectListURL(c provider.Container, t provider.ObjectType) string { if !ok { return "" } - switch t { - case provider.TypeKey: - return base + "/keys" - case provider.TypeCertificate: - return base + "/certificates" - } - return base + "/secrets" + return base + "/" + t.Collection() } // objectURL is the data-plane URL of an object, e.g. // https://kv.vault.azure.net/secrets/name. -func objectURL(c provider.Container, collection, name string) string { - return strings.TrimSuffix(c.Detail, "/") + "/" + collection + "/" + name +func objectURL(c provider.Container, t provider.ObjectType, name string) string { + return strings.TrimSuffix(c.Detail, "/") + "/" + t.Collection() + "/" + name } // secretObject is a secret, or a version of one, from a listing. Its URL @@ -93,7 +87,7 @@ func objectURL(c provider.Container, collection, name string) string { // a version, that version's. func secretObject(c provider.Container, p *azsecrets.SecretProperties, tenant string) provider.Object { name := p.ID.Name() - url := objectURL(c, "secrets", name) + url := objectURL(c, provider.TypeSecret, name) o := provider.Object{ Container: c, Type: provider.TypeSecret, @@ -103,7 +97,7 @@ func secretObject(c provider.Container, p *azsecrets.SecretProperties, tenant st ContentType: deref(p.ContentType), Tags: tags(p.Tags), Reference: "@Microsoft.KeyVault(SecretUri=" + url + ")", - URL: portalAssetURL(tenant, "Secret", strings.TrimSuffix(url+"/"+p.ID.Version(), "/")), + URL: portalAssetURL(tenant, provider.TypeSecret, strings.TrimSuffix(url+"/"+p.ID.Version(), "/")), } if a := p.Attributes; a != nil { o.Enabled, o.Updated, o.Expires = a.Enabled, deref(a.Updated), deref(a.Expires) @@ -113,7 +107,7 @@ func secretObject(c provider.Container, p *azsecrets.SecretProperties, tenant st func keyObject(c provider.Container, p *azkeys.KeyProperties) provider.Object { name := p.KID.Name() - url := objectURL(c, "keys", name) + url := objectURL(c, provider.TypeKey, name) o := provider.Object{ Container: c, Type: provider.TypeKey, @@ -132,7 +126,7 @@ func keyObject(c provider.Container, p *azkeys.KeyProperties) provider.Object { func certObject(c provider.Container, p *azcertificates.CertificateProperties) provider.Object { name := p.ID.Name() - url := objectURL(c, "certificates", name) + url := objectURL(c, provider.TypeCertificate, name) o := provider.Object{ Container: c, Type: provider.TypeCertificate, diff --git a/internal/provider/azure/details.go b/internal/provider/azure/details.go index 2bd4c65..87d2342 100644 --- a/internal/provider/azure/details.go +++ b/internal/provider/azure/details.go @@ -70,13 +70,10 @@ func graphInfo(m map[string]any) vaultInfo { // armInfo reads a vault's settings from the management API. func armInfo(v *armkeyvault.Vault) vaultInfo { - info := vaultInfo{location: deref(v.Location), tags: map[string]string{}} + info := vaultInfo{location: deref(v.Location), tags: tags(v.Tags)} if id, err := arm.ParseResourceID(deref(v.ID)); err == nil { info.group = id.ResourceGroupName } - for k, t := range v.Tags { - info.tags[k] = deref(t) - } pr := v.Properties if pr == nil { return info @@ -122,7 +119,11 @@ func (v vaultInfo) attrs() []provider.Attr { } else { add("soft delete", "off") } - add("purge prot.", map[bool]string{true: "on", false: "off"}[on(v.purge, false)]) + if on(v.purge, false) { + add("purge prot.", "on") + } else { + add("purge prot.", "off") + } switch { case strings.EqualFold(v.publicAccess, "disabled"): add("network", "private endpoints only") @@ -131,8 +132,11 @@ func (v vaultInfo) attrs() []provider.Attr { default: add("network", "all networks") } - if v.endpoints > 0 { - add("private", fmt.Sprintf("%d endpoint%s", v.endpoints, map[bool]string{true: "s"}[v.endpoints > 1])) + switch { + case v.endpoints == 1: + add("private", "1 endpoint") + case v.endpoints > 1: + add("private", fmt.Sprintf("%d endpoints", v.endpoints)) } keys := make([]string, 0, len(v.tags)) for k := range v.tags { diff --git a/internal/provider/databricks/databricks.go b/internal/provider/databricks/databricks.go index 63883c6..ded7536 100644 --- a/internal/provider/databricks/databricks.go +++ b/internal/provider/databricks/databricks.go @@ -203,17 +203,20 @@ func (p *Provider) Containers(ctx context.Context, src provider.Source) ([]provi return out, nil } -// vaultURL is the backing vault of an AKV-backed scope, or "". It falls -// back to the scope's Detail, so a scope remembered from an earlier session -// (a favourite) works before its workspace's scopes are listed. -func (p *Provider) vaultURL(c provider.Container) string { +// fallbackVault is the backing vault of an AKV-backed scope, to read it +// from when Databricks fails, or "" when there's none to fall back to. It +// falls back to the scope's Detail, so a scope remembered from an earlier +// session (a favourite) works before its workspace's scopes are listed. +func (p *Provider) fallbackVault(ctx context.Context, c provider.Container) string { + if p.keyVault == nil || ctx.Err() != nil { + return "" + } p.mu.Lock() url := p.vaults[c.ID] p.mu.Unlock() if url == "" { - url, _ = strings.CutPrefix(c.Detail, akvDetail) - if url == c.Detail { - url = "" + if detail, ok := strings.CutPrefix(c.Detail, akvDetail); ok { + url = detail } } return url @@ -264,8 +267,8 @@ func (p *Provider) Objects(ctx context.Context, c provider.Container, t provider // vaultObjects lists an AKV-backed scope's secrets from the vault itself. func (p *Provider) vaultObjects(ctx context.Context, c provider.Container) ([]provider.Object, error) { - url := p.vaultURL(c) - if url == "" || p.keyVault == nil || ctx.Err() != nil { + url := p.fallbackVault(ctx, c) + if url == "" { return nil, errNoFallback } secrets, err := p.keyVault.Objects(ctx, provider.Container{Detail: url}, provider.TypeSecret) @@ -302,8 +305,8 @@ func (p *Provider) Value(ctx context.Context, o provider.Object) (provider.Secre } dbxErr := classify(pr, err) - url := p.vaultURL(o.Container) - if url == "" || p.keyVault == nil || ctx.Err() != nil { + url := p.fallbackVault(ctx, o.Container) + if url == "" { return provider.SecretValue{}, dbxErr } v, kvErr := p.keyVault.Value(ctx, provider.Object{ diff --git a/internal/provider/fake/fake.go b/internal/provider/fake/fake.go index 6e467ef..b680768 100644 --- a/internal/provider/fake/fake.go +++ b/internal/provider/fake/fake.go @@ -236,9 +236,7 @@ func (p *Provider) add(c provider.Container, t provider.ObjectType, o provider.O 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] + o.URL = base + "/" + t.Collection() } k := listKey{c.ID, t} p.objects[k] = append(p.objects[k], o) @@ -271,12 +269,13 @@ const ( var allTypes = []provider.ObjectType{provider.TypeSecret, provider.TypeKey, provider.TypeCertificate} +func secretRef(vault, name string) string { + return fmt.Sprintf("@Microsoft.KeyVault(SecretUri=https://%s.vault.azure.net/secrets/%s)", vault, name) +} + // 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) } @@ -388,9 +387,6 @@ func Azure(delay time.Duration) *Provider { // 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...) diff --git a/internal/provider/provider.go b/internal/provider/provider.go index a10ea66..4a05cf0 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -84,6 +84,20 @@ const ( TypeCertificate ObjectType = "Certificate" ) +// Collection is the type's path segment in Key Vault URLs and links, e.g. +// "secrets". +func (t ObjectType) Collection() string { + switch t { + case TypeSecret: + return "secrets" + case TypeKey: + return "keys" + case TypeCertificate: + return "certificates" + } + return "" +} + // Container holds objects: a Key Vault or a Databricks secret scope. type Container struct { Source Source diff --git a/internal/provider/repo/repo.go b/internal/provider/repo/repo.go index 3f61d07..e9e3b13 100644 --- a/internal/provider/repo/repo.go +++ b/internal/provider/repo/repo.go @@ -361,30 +361,23 @@ func (p *Provider) Objects(_ context.Context, c provider.Container, t provider.O if err != nil { return nil, err } - meta, isSops := readMeta(c.ID) + meta, isSops := parseMeta(c.ID, b) keys, err := parseKeys(c.ID, b) if err != nil { return nil, fmt.Errorf("%s: %w", c.Name, err) } - if !isSops { // a plain env file - out := make([]provider.Object, 0, len(keys)) - for _, k := range keys { - out = append(out, provider.Object{ - Container: c, Type: provider.TypeSecret, ID: c.ID + "\x00" + k.extract, Name: k.name, - Attrs: []provider.Attr{{Label: "format", Value: "env"}, {Label: "encrypted", Value: "no: plain text in the file"}}, - }) - } - return out, nil + attrs := []provider.Attr{{Label: "format", Value: "env"}, {Label: "encrypted", Value: "no: plain text in the file"}} + if isSops { + attrs = []provider.Attr{{Label: "format", Value: "sops " + formatOf(c.ID)}, {Label: "encryption", Value: strings.Join(meta.methods, ", ")}} } out := make([]provider.Object, 0, len(keys)) for _, k := range keys { o := provider.Object{ Container: c, Type: provider.TypeSecret, - ID: c.ID + "\x00" + k.extract, Name: k.name, Updated: meta.modified, - Attrs: []provider.Attr{{Label: "format", Value: "sops " + formatOf(c.ID)}, {Label: "encryption", Value: strings.Join(meta.methods, ", ")}}, + ID: c.ID + "\x00" + k.extract, Name: k.name, Updated: meta.modified, Attrs: attrs, } - if !k.encrypted { - o.Attrs = append(o.Attrs, provider.Attr{Label: "encrypted", Value: "no (left in plain text by the file's sops rules)"}) + if isSops && !k.encrypted { + o.Attrs = append(slices.Clip(attrs), provider.Attr{Label: "encrypted", Value: "no (left in plain text by the file's sops rules)"}) } out = append(out, o) } @@ -403,10 +396,7 @@ func (p *Provider) Versions(context.Context, provider.Object) ([]provider.Object func (p *Provider) Value(ctx context.Context, o provider.Object) (provider.SecretValue, error) { file, extract, ok := strings.Cut(o.ID, "\x00") if !ok { // made from a link: the repo's root, the file's path, the key - file = o.Container.ID - if file == "" { - file = filepath.Join(o.Container.Source.ID, filepath.FromSlash(o.Container.Name)) - } + file = filePath(o.Container) extract = extractFor(file, o.Name) } if _, isSops := readMeta(file); !isSops { @@ -434,12 +424,18 @@ func (p *Provider) Contents(_ context.Context, c provider.Container) ([]byte, er return os.ReadFile(f.Path) } +// filePath is the file c is. A container made from a link has no ID: the +// file is its path in the repo. +func filePath(c provider.Container) string { + if c.ID != "" { + return c.ID + } + return filepath.Join(c.Source.ID, filepath.FromSlash(c.Name)) +} + // File is the file c is, and the line o's key is on. func (p *Provider) File(c provider.Container, o *provider.Object) (provider.File, error) { - path := c.ID - if path == "" { // made from a link - path = filepath.Join(c.Source.ID, filepath.FromSlash(c.Name)) - } + path := filePath(c) f := provider.File{Path: path} if _, isSops := readMeta(path); isSops { f.Sops = p.s.Sops @@ -609,33 +605,38 @@ type lazysecretsFile struct { // ShareKey is the sharing.key of the repo's .lazysecrets.yaml, if any. func ShareKey(root string) (string, error) { + f, err := readLazysecretsFile(root) + return f.Sharing.Key, err +} + +// readLazysecretsFile reads the repo's .lazysecrets.yaml; without one, it's +// empty. +func readLazysecretsFile(root string) (lazysecretsFile, error) { + var f lazysecretsFile b, err := os.ReadFile(filepath.Join(root, ".lazysecrets.yaml")) if errors.Is(err, os.ErrNotExist) { - return "", nil + return f, nil } if err != nil { - return "", err + return f, err } - var f lazysecretsFile if err := yaml.Unmarshal(b, &f); err != nil { - return "", fmt.Errorf("%s/.lazysecrets.yaml: %w", root, err) + return f, fmt.Errorf("%s/.lazysecrets.yaml: %w", root, err) } - return f.Sharing.Key, nil + return f, nil } // selector says which files to look at: user (the include/exclude of the // repo's .lazysecrets.yaml or the config) for every file, and sopsWhere // (.sops.yaml's creation_rules, when there's no include) for SOPS files. func (p *Provider) selector(root string, files []string) (user, sopsWhere func(string) bool, err error) { + f, err := readLazysecretsFile(root) + if err != nil { + return nil, nil, err + } include, exclude := p.s.Include, p.s.Exclude - if b, err := os.ReadFile(filepath.Join(root, ".lazysecrets.yaml")); err == nil { - var f lazysecretsFile - if err := yaml.Unmarshal(b, &f); err != nil { - return nil, nil, fmt.Errorf("%s/.lazysecrets.yaml: %w", root, err) - } - if len(f.Include) > 0 || len(f.Exclude) > 0 { - include, exclude = f.Include, f.Exclude - } + if len(f.Include) > 0 || len(f.Exclude) > 0 { + include, exclude = f.Include, f.Exclude } inc, err := globs(include) if err != nil { @@ -907,12 +908,21 @@ func readMeta(file string) (sopsMeta, bool) { return sopsMeta{}, false // not a format sops writes: skip reading } b, err := os.ReadFile(file) - if err != nil || !bytes.Contains(b, []byte("sops")) { + if err != nil { + return sopsMeta{}, false + } + return parseMeta(file, b) +} + +// parseMeta is readMeta, of the file's contents b. +func parseMeta(file string, b []byte) (sopsMeta, bool) { + format := formatOf(file) + if len(b) > maxFileSize || format == "" || !bytes.Contains(b, []byte("sops")) { return sopsMeta{}, false // cheap: most files aren't sops, no parsing } var m sopsMeta known := []string{"age", "azure_kv", "kms", "gcp_kms", "hc_vault", "pgp"} - switch formatOf(file) { + switch format { case "yaml", "json": var doc struct { Sops map[string]any `yaml:"sops"` diff --git a/internal/ref/ref.go b/internal/ref/ref.go index d6fdc82..4896027 100644 --- a/internal/ref/ref.go +++ b/internal/ref/ref.go @@ -92,15 +92,9 @@ func (r Ref) ContainerName() string { // Container.Detail. func (r Ref) VaultURL() string { return "https://" + r.Host + "/" } -var collections = map[provider.ObjectType]string{ - provider.TypeSecret: "secrets", - provider.TypeKey: "keys", - provider.TypeCertificate: "certificates", -} - func typeOf(collection string) (provider.ObjectType, bool) { - for t, c := range collections { - if strings.EqualFold(c, collection) { + for _, t := range []provider.ObjectType{provider.TypeSecret, provider.TypeKey, provider.TypeCertificate} { + if strings.EqualFold(t.Collection(), collection) { return t, true } } @@ -118,7 +112,7 @@ func (r Ref) IdentifierURL() string { if r.Type == "" { return u + "/" } - for _, seg := range []string{collections[r.Type], r.Name, r.Version} { + for _, seg := range []string{r.Type.Collection(), r.Name, r.Version} { if seg != "" { u += "/" + url.PathEscape(seg) } @@ -140,7 +134,7 @@ func (r Ref) String() string { segs = append(segs, r.Scope, r.Name) default: if r.Type != "" { - segs = append(segs, collections[r.Type]) + segs = append(segs, r.Type.Collection()) } segs = append(segs, r.Name, r.Version) } @@ -273,7 +267,7 @@ func parseLink(s string) (Ref, error) { r.Host = strings.ToLower(segs[0]) rest := segs[1:] - switch strings.ToLower(u.Host) { + switch host := strings.ToLower(u.Host); host { case "azure": r.Kind = provider.KindAzure if r.Host == "subscriptions" { @@ -300,11 +294,11 @@ func parseLink(s string) (Ref, error) { } case "databricks", "repo": r.Kind = provider.KindDatabricks - if strings.EqualFold(u.Host, "repo") { + if host == "repo" { r.Kind = provider.KindRepo } if len(rest) > 2 { - return Ref{}, fmt.Errorf("%s link has too many parts", strings.ToLower(u.Host)) + return Ref{}, fmt.Errorf("%s link has too many parts", host) } if len(rest) > 0 { r.Scope = rest[0] diff --git a/internal/share/rules.go b/internal/share/rules.go index 9274cb1..5586e63 100644 --- a/internal/share/rules.go +++ b/internal/share/rules.go @@ -47,7 +47,11 @@ func (r Rules) Check(key []byte) error { needs = append(needs, fmt.Sprintf("%d (minLength)", r.MinLength)) } if kinds < r.MinClasses { - has = append(has, fmt.Sprintf("%d %s of character", kinds, map[bool]string{true: "kind", false: "kinds"}[kinds == 1])) + kind := "kinds" + if kinds == 1 { + kind = "kind" + } + has = append(has, fmt.Sprintf("%d %s of character", kinds, kind)) needs = append(needs, fmt.Sprintf("%d of lowercase, uppercase, digits and symbols (minClasses)", r.MinClasses)) } if len(has) == 0 {