diff --git a/internal/cloud/cloudstore/identity.go b/internal/cloud/cloudstore/identity.go index ce80acc1..de246b30 100644 --- a/internal/cloud/cloudstore/identity.go +++ b/internal/cloud/cloudstore/identity.go @@ -906,9 +906,22 @@ func guardLastActiveAdminTx(ctx context.Context, tx *sql.Tx, principalID string, return nil } +// auditKeyValueExempt reports whether a (key, value) pair should bypass the +// sensitive-key heuristic based on its value. issued_token is a boolean flag +// written by the bootstrap completion audit (see cloudBootstrapCompletionMetadata); +// it is exempt ONLY when the value is actually a bool, so the exemption cannot +// be used to smuggle a secret string under a trusted key name. +func auditKeyValueExempt(key string, value any) bool { + if strings.EqualFold(strings.TrimSpace(key), "issued_token") { + _, ok := value.(bool) + return ok + } + return false +} + func rejectSensitiveAuthAuditMetadata(metadata map[string]any) error { for key, value := range metadata { - if sensitiveAuthAuditKey(key) { + if !auditKeyValueExempt(key, value) && sensitiveAuthAuditKey(key) { return fmt.Errorf("%w: %s", ErrSensitiveAuditMetadata, key) } if err := rejectSensitiveAuthAuditValue(value); err != nil { @@ -936,10 +949,11 @@ func rejectSensitiveAuthAuditValue(value any) error { } for _, key := range reflected.MapKeys() { keyText := key.String() - if sensitiveAuthAuditKey(keyText) { + entryValue := reflected.MapIndex(key).Interface() + if !auditKeyValueExempt(keyText, entryValue) && sensitiveAuthAuditKey(keyText) { return fmt.Errorf("%w: %s", ErrSensitiveAuditMetadata, keyText) } - if err := rejectSensitiveAuthAuditValue(reflected.MapIndex(key).Interface()); err != nil { + if err := rejectSensitiveAuthAuditValue(entryValue); err != nil { return err } } @@ -955,6 +969,10 @@ func rejectSensitiveAuthAuditValue(value any) error { func sensitiveAuthAuditKey(key string) bool { key = strings.ToLower(strings.TrimSpace(key)) + // token_prefix is the short, non-secret token prefix; it matches the + // fragment heuristic below but never carries secret material. issued_token + // is handled by auditKeyValueExempt instead, because it is only safe when + // its value is the boolean flag the bootstrap completion audit records. if key == "token_prefix" { return false } diff --git a/internal/cloud/cloudstore/identity_storage_test.go b/internal/cloud/cloudstore/identity_storage_test.go index 3e45a535..79359416 100644 --- a/internal/cloud/cloudstore/identity_storage_test.go +++ b/internal/cloud/cloudstore/identity_storage_test.go @@ -536,6 +536,31 @@ func TestCloudstoreIdentityPureHelpers(t *testing.T) { if err := rejectSensitiveAuthAuditMetadata(map[string]any{"events": []map[string]any{{"raw_token": "secret"}}}); !errors.Is(err, ErrSensitiveAuditMetadata) { t.Fatalf("typed nested slice sensitive audit metadata must be rejected, got %v", err) } + // Regression: the bootstrap completion audit metadata (created by + // cloudBootstrapCompletionMetadata) carries the boolean flag "issued_token". + // It is not a secret and must be accepted, otherwise + // `engram cloud bootstrap admin --issue-token` fails atomically and no admin + // token is ever minted. + if err := rejectSensitiveAuthAuditMetadata(map[string]any{"issued_token": true, "username": "admin", "created_admin": true}); err != nil { + t.Fatalf("bootstrap completion metadata must be accepted, got %v", err) + } + // issued_token is false for grant-only bootstraps (no --issue-token); both + // boolean values must be accepted. + if err := rejectSensitiveAuthAuditMetadata(map[string]any{"issued_token": false}); err != nil { + t.Fatalf("false issued_token must be accepted, got %v", err) + } + // issued_token is exempt ONLY as a boolean flag. A non-boolean value (e.g. a + // string that could carry secret material) must NOT bypass the filter, at + // the top level or nested. + if err := rejectSensitiveAuthAuditMetadata(map[string]any{"issued_token": "egc_live_would_be_leaked"}); !errors.Is(err, ErrSensitiveAuditMetadata) { + t.Fatalf("non-boolean issued_token must be rejected, got %v", err) + } + if err := rejectSensitiveAuthAuditMetadata(map[string]any{"outer": map[string]any{"issued_token": "egc_live_would_be_leaked"}}); !errors.Is(err, ErrSensitiveAuditMetadata) { + t.Fatalf("nested non-boolean issued_token must be rejected, got %v", err) + } + if err := rejectSensitiveAuthAuditMetadata(map[string]any{"outer": map[string]any{"issued_token": true}}); err != nil { + t.Fatalf("nested boolean issued_token must be accepted, got %v", err) + } } func tableExists(t *testing.T, db *sql.DB, table string) bool {