refactor: one place for Key Vault collections and paging in the providers #75

Merged
keb merged 1 commits from refactor/provider-duplication into main 2026-10-15 10:41:43 +02:00
9 changed files with 197 additions and 200 deletions

No files matched your search

+87 -109
View File
@@ -16,6 +16,7 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore" "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/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/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph" "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 // Sources lists the subscriptions the signed-in identity can see, skipping
// disabled and deleted ones. // disabled and deleted ones.
func (p *Provider) Sources(ctx context.Context) ([]provider.Source, error) { func (p *Provider) Sources(ctx context.Context) ([]provider.Source, error) {
var out []provider.Source out, err := all(ctx, p.subs.NewListPager(nil), func(page armsubscriptions.ClientListResponse) (srcs []provider.Source) {
pager := p.subs.NewListPager(nil)
for pager.More() {
page, err := pager.NextPage(ctx)
if err != nil {
return nil, cleanError(err)
}
for _, s := range page.Value { for _, s := range page.Value {
switch deref(s.State) { switch deref(s.State) {
case armsubscriptions.SubscriptionStateDisabled, armsubscriptions.SubscriptionStateDeleted: case armsubscriptions.SubscriptionStateDisabled, armsubscriptions.SubscriptionStateDeleted:
@@ -118,7 +113,7 @@ func (p *Provider) Sources(ctx context.Context) ([]provider.Source, error) {
p.mu.Lock() p.mu.Lock()
p.tenants[id] = tenant p.tenants[id] = tenant
p.mu.Unlock() p.mu.Unlock()
out = append(out, provider.Source{ srcs = append(srcs, provider.Source{
Kind: provider.KindAzure, Kind: provider.KindAzure,
ID: id, ID: id,
Name: deref(s.DisplayName), Name: deref(s.DisplayName),
@@ -126,6 +121,10 @@ func (p *Provider) Sources(ctx context.Context) ([]provider.Source, error) {
URL: PortalResourceURL(tenant, "/subscriptions/"+id, "overview"), 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)) }) 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 { if len(p.opts.Vaults) > 0 {
@@ -230,22 +229,29 @@ func (p *Provider) armVaults(ctx context.Context, src provider.Source) ([]provid
if err != nil { if err != nil {
return nil, err return nil, err
} }
var out []provider.Container return all(ctx, client.NewListBySubscriptionPager(nil), func(page armkeyvault.VaultsClientListBySubscriptionResponse) (vaults []provider.Container) {
pager := client.NewListBySubscriptionPager(nil) 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() { for pager.More() {
page, err := pager.NextPage(ctx) page, err := pager.NextPage(ctx)
if err != nil { if err != nil {
return nil, cleanError(err) return nil, cleanError(err)
} }
for _, v := range page.Value { out = append(out, items(page)...)
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(),
})
}
} }
return out, nil return out, nil
} }
@@ -288,51 +294,40 @@ func (p *Provider) Objects(ctx context.Context, c provider.Container, t provider
var out []provider.Object var out []provider.Object
switch t { switch t {
case provider.TypeSecret: case provider.TypeSecret:
pager := vc.secrets.NewListSecretPropertiesPager(nil) tenant := p.tenant(c.Source.ID)
for pager.More() { out, err = all(ctx, vc.secrets.NewListSecretPropertiesPager(nil), func(page azsecrets.ListSecretPropertiesResponse) (objs []provider.Object) {
page, err := pager.NextPage(ctx)
if err != nil {
return nil, cleanError(err)
}
for _, s := range page.Value { for _, s := range page.Value {
// Certificate-backing secrets appear under Certificates. // Certificate-backing secrets appear under Certificates.
if s.ID == nil || deref(s.Managed) { if s.ID != nil && !deref(s.Managed) {
continue objs = append(objs, secretObject(c, s, tenant))
}
out = append(out, secretObject(c, s, p.tenant(c.Source.ID)))
} }
} }
return objs
})
case provider.TypeKey: case provider.TypeKey:
pager := vc.keys.NewListKeyPropertiesPager(nil) out, err = all(ctx, vc.keys.NewListKeyPropertiesPager(nil), func(page azkeys.ListKeyPropertiesResponse) (objs []provider.Object) {
for pager.More() {
page, err := pager.NextPage(ctx)
if err != nil {
return nil, cleanError(err)
}
for _, k := range page.Value { for _, k := range page.Value {
if k.KID == nil || deref(k.Managed) { if k.KID != nil && !deref(k.Managed) {
continue objs = append(objs, keyObject(c, k))
}
out = append(out, keyObject(c, k))
} }
} }
return objs
})
case provider.TypeCertificate: case provider.TypeCertificate:
pager := vc.certs.NewListCertificatePropertiesPager(nil) out, err = all(ctx, vc.certs.NewListCertificatePropertiesPager(nil), func(page azcertificates.ListCertificatePropertiesResponse) (objs []provider.Object) {
for pager.More() {
page, err := pager.NextPage(ctx)
if err != nil {
return nil, cleanError(err)
}
for _, cp := range page.Value { for _, cp := range page.Value {
if cp.ID == nil { if cp.ID != nil {
continue objs = append(objs, certObject(c, cp))
}
out = append(out, certObject(c, cp))
} }
} }
return objs
})
default: default:
return nil, provider.ErrUnsupported 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)) }) slices.SortFunc(out, func(a, b provider.Object) int { return cmp.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name)) })
return out, nil 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 != nil {
if resp.Key.KID != nil { if resp.Key.KID != nil {
o.Version = resp.Key.KID.Version() 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) 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 { if resp.ID != nil {
o.Version = resp.ID.Version() 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 { if pol := resp.Policy; pol != nil && pol.SecretProperties != nil {
o.ContentType = deref(pol.SecretProperties.ContentType) o.ContentType = deref(pol.SecretProperties.ContentType)
@@ -381,56 +376,46 @@ func (p *Provider) Versions(ctx context.Context, o provider.Object) ([]provider.
if err != nil { if err != nil {
return nil, err return nil, err
} }
tenant := p.tenant(o.Container.Source.ID)
var out []provider.Object var out []provider.Object
switch o.Type { switch o.Type {
case provider.TypeSecret: case provider.TypeSecret:
pager := vc.secrets.NewListSecretPropertiesVersionsPager(o.Name, nil) out, err = all(ctx, vc.secrets.NewListSecretPropertiesVersionsPager(o.Name, nil), func(page azsecrets.ListSecretPropertiesVersionsResponse) (objs []provider.Object) {
for pager.More() {
page, err := pager.NextPage(ctx)
if err != nil {
return nil, cleanError(err)
}
for _, s := range page.Value { for _, s := range page.Value {
if s.ID != nil { 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: case provider.TypeKey:
pager := vc.keys.NewListKeyPropertiesVersionsPager(o.Name, nil) out, err = all(ctx, vc.keys.NewListKeyPropertiesVersionsPager(o.Name, nil), func(page azkeys.ListKeyPropertiesVersionsResponse) (objs []provider.Object) {
for pager.More() {
page, err := pager.NextPage(ctx)
if err != nil {
return nil, cleanError(err)
}
for _, k := range page.Value { for _, k := range page.Value {
if k.KID != nil { if k.KID != nil {
out = append(out, keyObject(o.Container, k)) objs = append(objs, keyObject(o.Container, k))
}
} }
} }
return objs
})
case provider.TypeCertificate: case provider.TypeCertificate:
pager := vc.certs.NewListCertificatePropertiesVersionsPager(o.Name, nil) out, err = all(ctx, vc.certs.NewListCertificatePropertiesVersionsPager(o.Name, nil), func(page azcertificates.ListCertificatePropertiesVersionsResponse) (objs []provider.Object) {
for pager.More() {
page, err := pager.NextPage(ctx)
if err != nil {
return nil, cleanError(err)
}
for _, cp := range page.Value { for _, cp := range page.Value {
if cp.ID != nil { if cp.ID != nil {
out = append(out, certObject(o.Container, cp)) objs = append(objs, certObject(o.Container, cp))
}
} }
} }
return objs
})
default: default:
return nil, provider.ErrUnsupported return nil, provider.ErrUnsupported
} }
tenant := p.tenant(o.Container.Source.ID) if err != nil {
return nil, err
}
for i := range out { for i := range out {
out[i].Reference = versionedReference(out[i]) out[i].Reference = versionedReference(out[i])
if o.Type != provider.TypeSecret { // secretObject's is the version's already 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, o.Type, out[i].ID+"/"+out[i].Version) // the version's page
out[i].URL = portalAssetURL(tenant, kind, 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) }) slices.SortFunc(out, func(a, b provider.Object) int { return b.Updated.Compare(a.Updated) })
@@ -452,47 +437,19 @@ func versionedReference(o provider.Object) string {
// Value returns a secret's value, a key's public key or a certificate's // Value returns a secret's value, a key's public key or a certificate's
// public certificate, as of o.Version (latest if empty). // public certificate, as of o.Version (latest if empty).
func (p *Provider) Value(ctx context.Context, o provider.Object) (provider.SecretValue, error) { 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) v, _, err := p.VersionedValue(ctx, o)
return v, err 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
} }
// VersionedValue is Value, with the version read. // VersionedValue is Value, with the version read.
func (p *Provider) VersionedValue(ctx context.Context, o provider.Object) (provider.SecretValue, string, error) { 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) vc, err := p.clients(o.Container.Detail)
if err != nil { if err != nil {
return provider.SecretValue{}, "", err return provider.SecretValue{}, "", err
} }
var text string
switch o.Type {
case provider.TypeSecret:
resp, err := vc.secrets.GetSecret(ctx, o.Name, o.Version, nil) resp, err := vc.secrets.GetSecret(ctx, o.Name, o.Version, nil)
if err != nil { if err != nil {
return provider.SecretValue{}, "", cleanError(err) return provider.SecretValue{}, "", cleanError(err)
@@ -502,6 +459,27 @@ func (p *Provider) VersionedValue(ctx context.Context, o provider.Object) (provi
version = resp.ID.Version() version = resp.ID.Version()
} }
return provider.NewSecretValue([]byte(deref(resp.Value))), version, nil 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
}
return provider.NewSecretValue([]byte(text)), o.Version, nil
} }
var ( var (
+9 -15
View File
@@ -55,8 +55,8 @@ func PortalResourceURL(tenant, resourceID, blade string) string {
// portalAssetURL links to a Key Vault object's page: a version's with // portalAssetURL links to a Key Vault object's page: a version's with
// one in objectID; a secret's own (its versions) without. // one in objectID; a secret's own (its versions) without.
func portalAssetURL(tenant, kind, objectID string) string { func portalAssetURL(tenant string, t provider.ObjectType, objectID string) string {
return portal + tenantPrefix(tenant) + "asset/Microsoft_Azure_KeyVault/" + kind + "/" + objectID return portal + tenantPrefix(tenant) + "asset/Microsoft_Azure_KeyVault/" + string(t) + "/" + objectID
} }
func tenantPrefix(tenant string) string { func tenantPrefix(tenant string) string {
@@ -73,19 +73,13 @@ func ObjectListURL(c provider.Container, t provider.ObjectType) string {
if !ok { if !ok {
return "" return ""
} }
switch t { return base + "/" + t.Collection()
case provider.TypeKey:
return base + "/keys"
case provider.TypeCertificate:
return base + "/certificates"
}
return base + "/secrets"
} }
// objectURL is the data-plane URL of an object, e.g. // objectURL is the data-plane URL of an object, e.g.
// https://kv.vault.azure.net/secrets/name. // https://kv.vault.azure.net/secrets/name.
func objectURL(c provider.Container, collection, name string) string { func objectURL(c provider.Container, t provider.ObjectType, name string) string {
return strings.TrimSuffix(c.Detail, "/") + "/" + collection + "/" + name return strings.TrimSuffix(c.Detail, "/") + "/" + t.Collection() + "/" + name
} }
// secretObject is a secret, or a version of one, from a listing. Its URL // 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. // a version, that version's.
func secretObject(c provider.Container, p *azsecrets.SecretProperties, tenant string) provider.Object { func secretObject(c provider.Container, p *azsecrets.SecretProperties, tenant string) provider.Object {
name := p.ID.Name() name := p.ID.Name()
url := objectURL(c, "secrets", name) url := objectURL(c, provider.TypeSecret, name)
o := provider.Object{ o := provider.Object{
Container: c, Container: c,
Type: provider.TypeSecret, Type: provider.TypeSecret,
@@ -103,7 +97,7 @@ func secretObject(c provider.Container, p *azsecrets.SecretProperties, tenant st
ContentType: deref(p.ContentType), ContentType: deref(p.ContentType),
Tags: tags(p.Tags), Tags: tags(p.Tags),
Reference: "@Microsoft.KeyVault(SecretUri=" + url + ")", 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 { if a := p.Attributes; a != nil {
o.Enabled, o.Updated, o.Expires = a.Enabled, deref(a.Updated), deref(a.Expires) 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 { func keyObject(c provider.Container, p *azkeys.KeyProperties) provider.Object {
name := p.KID.Name() name := p.KID.Name()
url := objectURL(c, "keys", name) url := objectURL(c, provider.TypeKey, name)
o := provider.Object{ o := provider.Object{
Container: c, Container: c,
Type: provider.TypeKey, 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 { func certObject(c provider.Container, p *azcertificates.CertificateProperties) provider.Object {
name := p.ID.Name() name := p.ID.Name()
url := objectURL(c, "certificates", name) url := objectURL(c, provider.TypeCertificate, name)
o := provider.Object{ o := provider.Object{
Container: c, Container: c,
Type: provider.TypeCertificate, Type: provider.TypeCertificate,
+11 -7
View File
@@ -71,13 +71,10 @@ func graphInfo(m map[string]any) vaultInfo {
// armInfo reads a vault's settings from the management API. // armInfo reads a vault's settings from the management API.
func armInfo(v *armkeyvault.Vault) vaultInfo { 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 { if id, err := arm.ParseResourceID(deref(v.ID)); err == nil {
info.group = id.ResourceGroupName info.group = id.ResourceGroupName
} }
for k, t := range v.Tags {
info.tags[k] = deref(t)
}
pr := v.Properties pr := v.Properties
if pr == nil { if pr == nil {
return info return info
@@ -123,7 +120,11 @@ func (v vaultInfo) attrs() []provider.Attr {
} else { } else {
add("soft delete", "off") 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 { switch {
case strings.EqualFold(v.publicAccess, "disabled"): case strings.EqualFold(v.publicAccess, "disabled"):
add("network", "private endpoints only") add("network", "private endpoints only")
@@ -132,8 +133,11 @@ func (v vaultInfo) attrs() []provider.Attr {
default: default:
add("network", "all networks") add("network", "all networks")
} }
if v.endpoints > 0 { switch {
add("private", fmt.Sprintf("%d endpoint%s", v.endpoints, map[bool]string{true: "s"}[v.endpoints > 1])) case v.endpoints == 1:
add("private", "1 endpoint")
case v.endpoints > 1:
add("private", fmt.Sprintf("%d endpoints", v.endpoints))
} }
for i, k := range slices.Sorted(maps.Keys(v.tags)) { for i, k := range slices.Sorted(maps.Keys(v.tags)) {
label := "" label := ""
+14 -11
View File
@@ -203,17 +203,20 @@ func (p *Provider) Containers(ctx context.Context, src provider.Source) ([]provi
return out, nil return out, nil
} }
// vaultURL is the backing vault of an AKV-backed scope, or "". It falls // fallbackVault is the backing vault of an AKV-backed scope, to read it
// back to the scope's Detail, so a scope remembered from an earlier session // from when Databricks fails, or "" when there's none to fall back to. It
// (a favourite) works before its workspace's scopes are listed. // falls back to the scope's Detail, so a scope remembered from an earlier
func (p *Provider) vaultURL(c provider.Container) string { // 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() p.mu.Lock()
url := p.vaults[c.ID] url := p.vaults[c.ID]
p.mu.Unlock() p.mu.Unlock()
if url == "" { if url == "" {
url, _ = strings.CutPrefix(c.Detail, akvDetail) if detail, ok := strings.CutPrefix(c.Detail, akvDetail); ok {
if url == c.Detail { url = detail
url = ""
} }
} }
return url 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. // 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) { func (p *Provider) vaultObjects(ctx context.Context, c provider.Container) ([]provider.Object, error) {
url := p.vaultURL(c) url := p.fallbackVault(ctx, c)
if url == "" || p.keyVault == nil || ctx.Err() != nil { if url == "" {
return nil, errNoFallback return nil, errNoFallback
} }
secrets, err := p.keyVault.Objects(ctx, provider.Container{Detail: url}, provider.TypeSecret) 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) dbxErr := classify(pr, err)
url := p.vaultURL(o.Container) url := p.fallbackVault(ctx, o.Container)
if url == "" || p.keyVault == nil || ctx.Err() != nil { if url == "" {
return provider.SecretValue{}, dbxErr return provider.SecretValue{}, dbxErr
} }
v, kvErr := p.keyVault.Value(ctx, provider.Object{ v, kvErr := p.keyVault.Value(ctx, provider.Object{
+5 -9
View File
@@ -225,9 +225,7 @@ func (p *Provider) add(c provider.Container, t provider.ObjectType, o provider.O
o.Type = t o.Type = t
o.ID = c.ID + "/" + strings.ToLower(string(t)) + "/" + o.Name o.ID = c.ID + "/" + strings.ToLower(string(t)) + "/" + o.Name
if base, ok := strings.CutSuffix(c.URL, "/overview"); ok { if base, ok := strings.CutSuffix(c.URL, "/overview"); ok {
o.URL = base + map[provider.ObjectType]string{ o.URL = base + "/" + t.Collection()
provider.TypeSecret: "/secrets", provider.TypeKey: "/keys", provider.TypeCertificate: "/certificates",
}[t]
} }
k := listKey{c.ID, t} k := listKey{c.ID, t}
p.objects[k] = append(p.objects[k], o) p.objects[k] = append(p.objects[k], o)
@@ -256,12 +254,13 @@ const (
var allTypes = []provider.ObjectType{provider.TypeSecret, provider.TypeKey, provider.TypeCertificate} 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. // Azure returns a fake Azure provider with two subscriptions.
func Azure(delay time.Duration) *Provider { func Azure(delay time.Duration) *Provider {
p := newProvider(provider.KindAzure, delay) 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 { objectURL := func(vault, kind, name string) string {
return fmt.Sprintf("https://%s.vault.azure.net/%s/%s", vault, kind, name) return fmt.Sprintf("https://%s.vault.azure.net/%s/%s", vault, kind, name)
} }
@@ -373,9 +372,6 @@ func Azure(delay time.Duration) *Provider {
// its own, like an admin account next to a personal one. // its own, like an admin account next to a personal one.
func AzureOps(delay time.Duration) *Provider { func AzureOps(delay time.Duration) *Provider {
p := newProvider(provider.KindAzure, delay) 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") prod := p.addSource("sub-prod", "platform-prod", "tenant: example.onmicrosoft.com")
breakGlass := p.addContainer(prod, "kv-breakglass", "https://kv-breakglass.vault.azure.net", allTypes...) breakGlass := p.addContainer(prod, "kv-breakglass", "https://kv-breakglass.vault.azure.net", allTypes...)
+14
View File
@@ -84,6 +84,20 @@ const (
TypeCertificate ObjectType = "Certificate" 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. // Container holds objects: a Key Vault or a Databricks secret scope.
type Container struct { type Container struct {
Source Source Source Source
+45 -35
View File
@@ -356,30 +356,23 @@ func (p *Provider) Objects(_ context.Context, c provider.Container, t provider.O
if err != nil { if err != nil {
return nil, err return nil, err
} }
meta, isSops := readMeta(c.ID) meta, isSops := parseMeta(c.ID, b)
keys, err := parseKeys(c.ID, b) keys, err := parseKeys(c.ID, b)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s: %w", c.Name, err) return nil, fmt.Errorf("%s: %w", c.Name, err)
} }
if !isSops { // a plain env file attrs := []provider.Attr{{Label: "format", Value: "env"}, {Label: "encrypted", Value: "no: plain text in the file"}}
out := make([]provider.Object, 0, len(keys)) if isSops {
for _, k := range keys { attrs = []provider.Attr{{Label: "format", Value: "sops " + formatOf(c.ID)}, {Label: "encryption", Value: strings.Join(meta.methods, ", ")}}
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
} }
out := make([]provider.Object, 0, len(keys)) out := make([]provider.Object, 0, len(keys))
for _, k := range keys { for _, k := range keys {
o := provider.Object{ o := provider.Object{
Container: c, Type: provider.TypeSecret, Container: c, Type: provider.TypeSecret,
ID: c.ID + "\x00" + k.extract, Name: k.name, Updated: meta.modified, ID: c.ID + "\x00" + k.extract, Name: k.name, Updated: meta.modified, Attrs: attrs,
Attrs: []provider.Attr{{Label: "format", Value: "sops " + formatOf(c.ID)}, {Label: "encryption", Value: strings.Join(meta.methods, ", ")}},
} }
if !k.encrypted { if isSops && !k.encrypted {
o.Attrs = append(o.Attrs, provider.Attr{Label: "encrypted", Value: "no (left in plain text by the file's sops rules)"}) 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) out = append(out, o)
} }
@@ -398,10 +391,7 @@ func (p *Provider) Versions(context.Context, provider.Object) ([]provider.Object
func (p *Provider) Value(ctx context.Context, o provider.Object) (provider.SecretValue, error) { func (p *Provider) Value(ctx context.Context, o provider.Object) (provider.SecretValue, error) {
file, extract, ok := strings.Cut(o.ID, "\x00") file, extract, ok := strings.Cut(o.ID, "\x00")
if !ok { // made from a link: the repo's root, the file's path, the key if !ok { // made from a link: the repo's root, the file's path, the key
file = o.Container.ID file = filePath(o.Container)
if file == "" {
file = filepath.Join(o.Container.Source.ID, filepath.FromSlash(o.Container.Name))
}
extract = extractFor(file, o.Name) extract = extractFor(file, o.Name)
} }
if _, isSops := readMeta(file); !isSops { if _, isSops := readMeta(file); !isSops {
@@ -429,12 +419,18 @@ func (p *Provider) Contents(_ context.Context, c provider.Container) ([]byte, er
return os.ReadFile(f.Path) 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. // 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) { func (p *Provider) File(c provider.Container, o *provider.Object) (provider.File, error) {
path := c.ID path := filePath(c)
if path == "" { // made from a link
path = filepath.Join(c.Source.ID, filepath.FromSlash(c.Name))
}
f := provider.File{Path: path} f := provider.File{Path: path}
if _, isSops := readMeta(path); isSops { if _, isSops := readMeta(path); isSops {
f.Sops = p.s.Sops f.Sops = p.s.Sops
@@ -604,34 +600,39 @@ type lazysecretsFile struct {
// ShareKey is the sharing.key of the repo's .lazysecrets.yaml, if any. // ShareKey is the sharing.key of the repo's .lazysecrets.yaml, if any.
func ShareKey(root string) (string, error) { 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")) b, err := os.ReadFile(filepath.Join(root, ".lazysecrets.yaml"))
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
return "", nil return f, nil
} }
if err != nil { if err != nil {
return "", err return f, err
} }
var f lazysecretsFile
if err := yaml.Unmarshal(b, &f); err != nil { 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 // 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 // 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. // (.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) { func (p *Provider) selector(root string, files []string) (user, sopsWhere func(string) bool, err error) {
include, exclude := p.s.Include, p.s.Exclude f, err := readLazysecretsFile(root)
if b, err := os.ReadFile(filepath.Join(root, ".lazysecrets.yaml")); err == nil { if err != nil {
var f lazysecretsFile return nil, nil, err
if err := yaml.Unmarshal(b, &f); err != nil {
return nil, nil, fmt.Errorf("%s/.lazysecrets.yaml: %w", root, err)
} }
include, exclude := p.s.Include, p.s.Exclude
if len(f.Include) > 0 || len(f.Exclude) > 0 { if len(f.Include) > 0 || len(f.Exclude) > 0 {
include, exclude = f.Include, f.Exclude include, exclude = f.Include, f.Exclude
} }
}
inc, err := globs(include) inc, err := globs(include)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@@ -902,12 +903,21 @@ func readMeta(file string) (sopsMeta, bool) {
return sopsMeta{}, false // not a format sops writes: skip reading return sopsMeta{}, false // not a format sops writes: skip reading
} }
b, err := os.ReadFile(file) 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 return sopsMeta{}, false // cheap: most files aren't sops, no parsing
} }
var m sopsMeta var m sopsMeta
known := []string{"age", "azure_kv", "kms", "gcp_kms", "hc_vault", "pgp"} known := []string{"age", "azure_kv", "kms", "gcp_kms", "hc_vault", "pgp"}
switch formatOf(file) { switch format {
case "yaml", "json": case "yaml", "json":
var doc struct { var doc struct {
Sops map[string]any `yaml:"sops"` Sops map[string]any `yaml:"sops"`
+7 -13
View File
@@ -92,15 +92,9 @@ func (r Ref) ContainerName() string {
// Container.Detail. // Container.Detail.
func (r Ref) VaultURL() string { return "https://" + r.Host + "/" } 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) { func typeOf(collection string) (provider.ObjectType, bool) {
for t, c := range collections { for _, t := range []provider.ObjectType{provider.TypeSecret, provider.TypeKey, provider.TypeCertificate} {
if strings.EqualFold(c, collection) { if strings.EqualFold(t.Collection(), collection) {
return t, true return t, true
} }
} }
@@ -118,7 +112,7 @@ func (r Ref) IdentifierURL() string {
if r.Type == "" { if r.Type == "" {
return u + "/" 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 != "" { if seg != "" {
u += "/" + url.PathEscape(seg) u += "/" + url.PathEscape(seg)
} }
@@ -140,7 +134,7 @@ func (r Ref) String() string {
segs = append(segs, r.Scope, r.Name) segs = append(segs, r.Scope, r.Name)
default: default:
if r.Type != "" { if r.Type != "" {
segs = append(segs, collections[r.Type]) segs = append(segs, r.Type.Collection())
} }
segs = append(segs, r.Name, r.Version) segs = append(segs, r.Name, r.Version)
} }
@@ -273,7 +267,7 @@ func parseLink(s string) (Ref, error) {
r.Host = strings.ToLower(segs[0]) r.Host = strings.ToLower(segs[0])
rest := segs[1:] rest := segs[1:]
switch strings.ToLower(u.Host) { switch host := strings.ToLower(u.Host); host {
case "azure": case "azure":
r.Kind = provider.KindAzure r.Kind = provider.KindAzure
if r.Host == "subscriptions" { if r.Host == "subscriptions" {
@@ -300,11 +294,11 @@ func parseLink(s string) (Ref, error) {
} }
case "databricks", "repo": case "databricks", "repo":
r.Kind = provider.KindDatabricks r.Kind = provider.KindDatabricks
if strings.EqualFold(u.Host, "repo") { if host == "repo" {
r.Kind = provider.KindRepo r.Kind = provider.KindRepo
} }
if len(rest) > 2 { 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 { if len(rest) > 0 {
r.Scope = rest[0] r.Scope = rest[0]
+5 -1
View File
@@ -48,7 +48,11 @@ func (r Rules) Check(key []byte) error {
needs = append(needs, fmt.Sprintf("%d (minLength)", r.MinLength)) needs = append(needs, fmt.Sprintf("%d (minLength)", r.MinLength))
} }
if kinds < r.MinClasses { 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)) needs = append(needs, fmt.Sprintf("%d of lowercase, uppercase, digits and symbols (minClasses)", r.MinClasses))
} }
if len(has) == 0 { if len(has) == 0 {