diff --git a/api/v1alpha1/identitytokenrole_types.go b/api/v1alpha1/identitytokenrole_types.go index ba83cbdf..5477e28c 100644 --- a/api/v1alpha1/identitytokenrole_types.go +++ b/api/v1alpha1/identitytokenrole_types.go @@ -148,6 +148,14 @@ func (d *IdentityTokenRole) IsInitialized() bool { } func (d *IdentityTokenRole) PrepareInternalValues(context context.Context, object client.Object) error { + if d.Spec.Template == "" { + return nil + } + resolved, err := vaultutils.ResolveAuthAccessors(context, d.Spec.Template) + if err != nil { + return err + } + d.Spec.Template = resolved return nil } diff --git a/api/v1alpha1/policy_types.go b/api/v1alpha1/policy_types.go index 6bd8eaaa..6558a031 100644 --- a/api/v1alpha1/policy_types.go +++ b/api/v1alpha1/policy_types.go @@ -18,12 +18,8 @@ package v1alpha1 import ( "context" - "errors" "reflect" - "regexp" - "strings" - vault "github.com/hashicorp/vault/api" vaultutils "github.com/redhat-cop/vault-config-operator/api/v1alpha1/utils" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -76,35 +72,15 @@ func (d *Policy) IsDeletable() bool { } func (d *Policy) PrepareInternalValues(context context.Context, object client.Object) error { - // Fast path escape if no "${..}" placeholder is detected - match, err := regexp.MatchString("\\${[^}]+}", d.Spec.Policy) - if err != nil || !match { - return nil - } - - log := log.FromContext(context) - - // Retrieves the list of auth engines to get their accessors - // Kinda duplicates logic found in VaultEngineObject.retrieveAccessor - vaultClient := context.Value("vaultClient").(*vault.Client) - secret, err := vaultClient.Logical().Read("sys/auth") + resolved, err := vaultutils.ResolveAuthAccessors(context, d.Spec.Policy) if err != nil { - // Log but ignore the error: do not resolve placeholders - log.Error(err, "could not resolve auth engine accessor(s) in policy rule - unable to retrieve auth engines at", "path", "sys/auth") - return nil + return err } - if secret == nil { - return errors.New("could not resolve auth engine accessor(s) in policy rule - listing auth engines at sys/auth unexpectedly returned null") + if resolved != d.Spec.Policy { + d.Spec.Policy = resolved + log := log.FromContext(context) + log.V(1).Info("Auth engine accessor(s) resolved", "policy", d.Spec.Policy) } - - for key, data := range secret.Data { - authenginepath := strings.Trim(key, "/") - placeholder := "${auth/" + authenginepath + "/@accessor}" - accessor := data.(map[string]interface{})["accessor"].(string) - d.Spec.Policy = strings.ReplaceAll(d.Spec.Policy, placeholder, accessor) - } - - log.V(1).Info("Auth engine accessor(s) resolved", "policy", d.Spec.Policy) return nil } diff --git a/api/v1alpha1/utils/commons.go b/api/v1alpha1/utils/commons.go index 00f20720..769a52cb 100644 --- a/api/v1alpha1/utils/commons.go +++ b/api/v1alpha1/utils/commons.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "os" + "regexp" "strings" "sync" @@ -433,6 +434,38 @@ func GetFinalizer(instance client.Object) string { return "controller-" + strings.ToLower(instance.GetObjectKind().GroupVersionKind().Kind) } +// ResolveAuthAccessors replaces all occurrences of ${auth//@accessor} in the given +// input string with the actual accessor value from Vault. It reads all auth engines from sys/auth +// and performs the replacement for each one. If no placeholders are found, the input is returned unchanged. +// The Vault role used for authentication must have read and list access to sys/auth for this to work. +func ResolveAuthAccessors(context context.Context, input string) (string, error) { + match, err := regexp.MatchString("\\${[^}]+}", input) + if err != nil || !match { + return input, nil + } + + log := log.FromContext(context) + + vaultClient := context.Value("vaultClient").(*vault.Client) + secret, err := vaultClient.Logical().Read("sys/auth") + if err != nil { + log.Error(err, "could not resolve auth engine accessor(s) - unable to retrieve auth engines at", "path", "sys/auth") + return input, nil + } + if secret == nil { + return input, errors.New("could not resolve auth engine accessor(s) - listing auth engines at sys/auth unexpectedly returned null") + } + + for key, data := range secret.Data { + authenginepath := strings.Trim(key, "/") + placeholder := "${auth/" + authenginepath + "/@accessor}" + accessor := data.(map[string]interface{})["accessor"].(string) + input = strings.ReplaceAll(input, placeholder, accessor) + } + + return input, nil +} + // +kubebuilder:object:generate=true type TargetNamespaceConfig struct { // TargetNamespaceSelector is a selector of namespaces from which service accounts will receove this role. Either TargetNamespaceSelector or TargetNamespaces can be specified diff --git a/docs/identities.md b/docs/identities.md index 5888ef9d..5589e30a 100644 --- a/docs/identities.md +++ b/docs/identities.md @@ -226,4 +226,29 @@ The following fields are available: - `key` - (required) A configured named key; the key must already exist. - `template` - (optional) The template string to use for generating tokens. May be in string-ified JSON or base64 format. - `clientID` - (optional) Client ID. A random ID will be generated if left unset. -- `ttl` - (optional, default: `"24h"`) TTL of the tokens generated against the role. \ No newline at end of file +- `ttl` - (optional, default: `"24h"`) TTL of the tokens generated against the role. + +### Auth engine accessor resolution in templates + +The `template` field supports the same `${auth//@accessor}` placeholder syntax as [Policy](../docs/policy-management.md#policy). The operator will automatically replace these placeholders with the accessor of the auth engine mounted at the given path before writing the role to Vault. + +For example: + +```yaml +apiVersion: redhatcop.redhat.io/v1alpha1 +kind: IdentityTokenRole +metadata: + name: identitytokenrole-with-accessor +spec: + authentication: + path: kubernetes + role: policy-admin + key: identitytokenkey-sample + template: | + { + "namespace": {{identity.entity.aliases.${auth/kubernetes/@accessor}.metadata.service_account_namespace}} + } + ttl: "24h" +``` + +Note: the Vault role used for authentication (specified in the `authentication` section) must have `read` and `list` access to Vault's `sys/auth` API endpoint for this automated resolution to work. Any unresolved placeholder is left as-is in the configured template. \ No newline at end of file