azureappconfiguration/keyvault.go (99 lines of code) (raw):

// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. package azureappconfiguration import ( "context" "encoding/json" "fmt" "net/url" "strings" "sync" "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets" ) // keyVaultReferenceResolver resolves Key Vault references to their actual secret values type keyVaultReferenceResolver struct { clients sync.Map // map[string]secretClient secretResolver SecretResolver credential azcore.TokenCredential } // secretMetadata contains parsed information about a Key Vault secret reference type secretMetadata struct { host string name string version string } // keyVaultReference represents the JSON structure of a Key Vault reference type keyVaultReference struct { URI string `json:"uri"` } type secretClient interface { GetSecret(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) } // resolveSecret resolves a Key Vault reference to its actual secret value func (r *keyVaultReferenceResolver) resolveSecret(ctx context.Context, keyVaultReference string) (string, error) { // vaultUri: "https://{keyVaultName}.vault.azure.net/secrets/{secretName}/{secretVersion}" uri, err := r.extractKeyVaultURI(keyVaultReference) if err != nil { return "", fmt.Errorf("failed to parse Key Vault reference: %w", err) } // Parse the URI to get metadata (host, secret name, version) secretMeta, err := parse(uri) if err != nil { return "", fmt.Errorf("invalid Key Vault reference: %w", err) } if r.secretResolver != nil { vaultUri, err := url.Parse(uri) if err != nil { return "", fmt.Errorf("invalid Key Vault reference: %w", err) } return r.secretResolver.ResolveSecret(ctx, *vaultUri) } vaultURL := fmt.Sprintf("https://%s", secretMeta.host) client, err := r.getSecretClient(vaultURL) if err != nil { return "", fmt.Errorf("failed to get Key Vault client: %w", err) } response, err := client.GetSecret(ctx, secretMeta.name, secretMeta.version, nil) if err != nil { return "", fmt.Errorf("failed to retrieve secret '%s' from Key Vault: %w", secretMeta.name, err) } if response.Value == nil { return "", nil } return *response.Value, nil } // extractKeyVaultURI tries to parse a Key Vault reference in various formats func (r *keyVaultReferenceResolver) extractKeyVaultURI(reference string) (string, error) { // Valid Key Vault Reference setting value to parse // { // "uri":"https://{keyVaultName}.vault.azure.net/secrets/{secretName}/{secretVersion}" // } var kvRef keyVaultReference if err := json.Unmarshal([]byte(reference), &kvRef); err == nil && kvRef.URI != "" { return kvRef.URI, nil } return "", fmt.Errorf("invalid Key Vault reference format: %s", reference) } // getSecretClient gets or creates a client for the specified vault URL func (r *keyVaultReferenceResolver) getSecretClient(vaultURL string) (secretClient, error) { if client, ok := r.clients.Load(vaultURL); ok { return client.(secretClient), nil } client, err := azsecrets.NewClient(vaultURL, r.credential, nil) if err != nil { return nil, fmt.Errorf("failed to create Key Vault client: %w", err) } // Store the client - if concurrent call already stored a client, use the existing one storedClient, loaded := r.clients.LoadOrStore(vaultURL, client) if loaded { // Another goroutine already created and stored a client return storedClient.(secretClient), nil } return client, nil } // parse extracts metadata from a Key Vault secret reference URI func parse(reference string) (*secretMetadata, error) { secretURL, err := url.Parse(reference) if err != nil { return nil, fmt.Errorf("invalid URL format: %w", err) } trimmedPath := strings.TrimPrefix(secretURL.Path, "/") segments := strings.Split(trimmedPath, "/") if len(segments) < 2 || strings.ToLower(segments[0]) != "secrets" || segments[1] == "" { return nil, fmt.Errorf("invalid Key Vault URL format: %s", reference) } secretName := segments[1] var secretVersion string if len(segments) > 2 { secretVersion = segments[2] } return &secretMetadata{ host: strings.ToLower(secretURL.Host), name: secretName, version: secretVersion, }, nil }