feat: a vault's settings come with the vault list, with no requests of their own
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
5381cf5d43
commit
879b961ac5
6 files changed
+157
-94
No files matched your search
@@ -1,7 +1,6 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -12,66 +11,131 @@ import (
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
||||
)
|
||||
|
||||
// Details are a vault's settings, from the Key Vault management API: where
|
||||
// it is, how access to it works, and how it's protected. Pinned vaults,
|
||||
// known only by their URL, have none.
|
||||
func (p *Provider) Details(ctx context.Context, c provider.Container) ([]provider.Attr, error) {
|
||||
id, err := arm.ParseResourceID(c.ID)
|
||||
if err != nil {
|
||||
return nil, provider.ErrUnsupported
|
||||
}
|
||||
client, err := armkeyvault.NewVaultsClient(id.SubscriptionID, p.cred, p.opts.arm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := client.Get(ctx, id.ResourceGroupName, id.Name, nil)
|
||||
if err != nil {
|
||||
return nil, cleanError(err)
|
||||
}
|
||||
return vaultAttrs(resp.Vault), nil
|
||||
// A vault's settings come with the listing, from Resource Graph or the
|
||||
// Key Vault management API, with no requests of their own: where it is,
|
||||
// how access to it works, and how it's protected.
|
||||
|
||||
type vaultInfo struct {
|
||||
group, location, sku string
|
||||
rbac, softDelete, purge *bool
|
||||
retention *int32
|
||||
publicAccess, defaultAction string
|
||||
ipRules, subnets, endpoints int
|
||||
policies int
|
||||
tags map[string]string
|
||||
}
|
||||
|
||||
func vaultAttrs(v armkeyvault.Vault) []provider.Attr {
|
||||
// vaultQuery lists vaults with just the settings shown, not their whole
|
||||
// properties (access policies can be long).
|
||||
const vaultQuery = `resources
|
||||
| where type =~ 'microsoft.keyvault/vaults'
|
||||
| project id, name, uri = tostring(properties.vaultUri), location, resourceGroup, tags,
|
||||
sku = tostring(properties.sku.name), rbac = properties.enableRbacAuthorization,
|
||||
softDelete = properties.enableSoftDelete, retention = properties.softDeleteRetentionInDays,
|
||||
purge = properties.enablePurgeProtection, publicAccess = tostring(properties.publicNetworkAccess),
|
||||
defaultAction = tostring(properties.networkAcls.defaultAction),
|
||||
ipRules = array_length(properties.networkAcls.ipRules),
|
||||
subnets = array_length(properties.networkAcls.virtualNetworkRules),
|
||||
endpoints = array_length(properties.privateEndpointConnections),
|
||||
policies = array_length(properties.accessPolicies)`
|
||||
|
||||
// graphInfo reads a vaultQuery row's settings.
|
||||
func graphInfo(m map[string]any) vaultInfo {
|
||||
str := func(k string) string { s, _ := m[k].(string); return s }
|
||||
num := func(k string) int { f, _ := m[k].(float64); return int(f) }
|
||||
flag := func(k string) *bool {
|
||||
if b, ok := m[k].(bool); ok {
|
||||
return &b
|
||||
}
|
||||
return nil
|
||||
}
|
||||
v := vaultInfo{
|
||||
group: str("resourceGroup"), location: str("location"), sku: str("sku"),
|
||||
rbac: flag("rbac"), softDelete: flag("softDelete"), purge: flag("purge"),
|
||||
publicAccess: str("publicAccess"), defaultAction: str("defaultAction"),
|
||||
ipRules: num("ipRules"), subnets: num("subnets"), endpoints: num("endpoints"), policies: num("policies"),
|
||||
}
|
||||
if f, ok := m["retention"].(float64); ok {
|
||||
r := int32(f)
|
||||
v.retention = &r
|
||||
}
|
||||
if tags, ok := m["tags"].(map[string]any); ok {
|
||||
v.tags = map[string]string{}
|
||||
for k, t := range tags {
|
||||
v.tags[k] = fmt.Sprint(t)
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// 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{}}
|
||||
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
|
||||
}
|
||||
if pr.SKU != nil && pr.SKU.Name != nil {
|
||||
info.sku = string(*pr.SKU.Name)
|
||||
}
|
||||
info.rbac, info.softDelete, info.purge = pr.EnableRbacAuthorization, pr.EnableSoftDelete, pr.EnablePurgeProtection
|
||||
info.retention, info.publicAccess = pr.SoftDeleteRetentionInDays, deref(pr.PublicNetworkAccess)
|
||||
if acl := pr.NetworkACLs; acl != nil {
|
||||
if acl.DefaultAction != nil {
|
||||
info.defaultAction = string(*acl.DefaultAction)
|
||||
}
|
||||
info.ipRules, info.subnets = len(acl.IPRules), len(acl.VirtualNetworkRules)
|
||||
}
|
||||
info.endpoints, info.policies = len(pr.PrivateEndpointConnections), len(pr.AccessPolicies)
|
||||
return info
|
||||
}
|
||||
|
||||
func (v vaultInfo) attrs() []provider.Attr {
|
||||
var out []provider.Attr
|
||||
add := func(label, value string) {
|
||||
if value != "" {
|
||||
out = append(out, provider.Attr{Label: label, Value: value})
|
||||
}
|
||||
}
|
||||
if id, err := arm.ParseResourceID(deref(v.ID)); err == nil {
|
||||
add("group", id.ResourceGroupName)
|
||||
}
|
||||
add("location", deref(v.Location))
|
||||
pr := v.Properties
|
||||
if pr == nil {
|
||||
return out
|
||||
}
|
||||
if pr.SKU != nil && pr.SKU.Name != nil {
|
||||
add("SKU", string(*pr.SKU.Name))
|
||||
}
|
||||
add("group", v.group)
|
||||
add("location", v.location)
|
||||
add("SKU", v.sku)
|
||||
// Who may read secrets is decided by Azure roles or by the vault's
|
||||
// own access policies, never both.
|
||||
if on(pr.EnableRbacAuthorization, false) {
|
||||
if on(v.rbac, false) {
|
||||
add("access", "Azure roles (RBAC)")
|
||||
} else {
|
||||
add("access", fmt.Sprintf("access policies (%d)", len(pr.AccessPolicies)))
|
||||
add("access", fmt.Sprintf("access policies (%d)", v.policies))
|
||||
}
|
||||
if on(pr.EnableSoftDelete, true) {
|
||||
if on(v.softDelete, true) {
|
||||
days := int32(90)
|
||||
if pr.SoftDeleteRetentionInDays != nil {
|
||||
days = *pr.SoftDeleteRetentionInDays
|
||||
if v.retention != nil {
|
||||
days = *v.retention
|
||||
}
|
||||
add("soft delete", fmt.Sprintf("on, %d days", days))
|
||||
} else {
|
||||
add("soft delete", "off")
|
||||
}
|
||||
add("purge prot.", map[bool]string{true: "on", false: "off"}[on(pr.EnablePurgeProtection, false)])
|
||||
add("network", network(pr))
|
||||
if n := len(pr.PrivateEndpointConnections); n > 0 {
|
||||
add("private", fmt.Sprintf("%d endpoint%s", n, map[bool]string{true: "s"}[n > 1]))
|
||||
add("purge prot.", map[bool]string{true: "on", false: "off"}[on(v.purge, false)])
|
||||
switch {
|
||||
case strings.EqualFold(v.publicAccess, "disabled"):
|
||||
add("network", "private endpoints only")
|
||||
case strings.EqualFold(v.defaultAction, "deny"):
|
||||
add("network", fmt.Sprintf("selected networks (%d IP ranges, %d subnets)", v.ipRules, v.subnets))
|
||||
default:
|
||||
add("network", "all networks")
|
||||
}
|
||||
keys := make([]string, 0, len(v.Tags))
|
||||
for k := range v.Tags {
|
||||
if v.endpoints > 0 {
|
||||
add("private", fmt.Sprintf("%d endpoint%s", v.endpoints, map[bool]string{true: "s"}[v.endpoints > 1]))
|
||||
}
|
||||
keys := make([]string, 0, len(v.tags))
|
||||
for k := range v.tags {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
slices.Sort(keys)
|
||||
@@ -80,23 +144,11 @@ func vaultAttrs(v armkeyvault.Vault) []provider.Attr {
|
||||
if i == 0 {
|
||||
label = "tags"
|
||||
}
|
||||
out = append(out, provider.Attr{Label: label, Value: k + ": " + deref(v.Tags[k])})
|
||||
out = append(out, provider.Attr{Label: label, Value: k + ": " + v.tags[k]})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// network is where the vault can be reached from.
|
||||
func network(pr *armkeyvault.VaultProperties) string {
|
||||
if strings.EqualFold(deref(pr.PublicNetworkAccess), "disabled") {
|
||||
return "private endpoints only"
|
||||
}
|
||||
acl := pr.NetworkACLs
|
||||
if acl == nil || acl.DefaultAction == nil || *acl.DefaultAction != armkeyvault.NetworkRuleActionDeny {
|
||||
return "all networks"
|
||||
}
|
||||
return fmt.Sprintf("selected networks (%d IP ranges, %d subnets)", len(acl.IPRules), len(acl.VirtualNetworkRules))
|
||||
}
|
||||
|
||||
// on is *b, or def when it isn't set.
|
||||
func on(b *bool, def bool) bool {
|
||||
if b == nil {
|
||||
|
||||
Reference in new issue
Block a user