Skip to content

feat(m2m): ServiceAccount + TrustedIssuer schemas, storage interface, M2M constants [Phase 1 — PR 1/5]#641

Open
lakhansamani wants to merge 2 commits into
mainfrom
feat/workload-identity-phase1-schema
Open

feat(m2m): ServiceAccount + TrustedIssuer schemas, storage interface, M2M constants [Phase 1 — PR 1/5]#641
lakhansamani wants to merge 2 commits into
mainfrom
feat/workload-identity-phase1-schema

Conversation

@lakhansamani

Copy link
Copy Markdown
Contributor

Summary

Establishes the foundational storage layer for the Workload Identity Program — a 6-phase initiative adding machine/service authentication to Authorizer.

This is PR 1 of 5 for Phase 1 (client credentials grant). Nothing in this PR breaks existing flows; it is purely additive.

Spec: docs/specs/WORKLOAD_IDENTITY_PROGRAM.md

What's in this PR

New schemas (internal/storage/schemas/)

  • ServiceAccount — machine/workload OAuth2 client. ID = client_id, ClientSecret = bcrypt hash. No email, MFA, social login, or session fields — those belong to User. Deliberately separate per industry standard (Auth0, Okta, GCP, Keycloak all keep machine identity distinct from human users).
  • TrustedIssuer — external JWT issuer registry. Carries all fields needed for all 6 phases: KeySourceType, ExpectedAud, EnableTokenReview (K8s online validation, Phase 4), SpiffeRefreshHintSeconds (SPIFFE bundle refresh cadence, Phase 5), AuthMethod + proxy fields (X.509 mTLS, Phase 6). Schema defined once now to avoid the all-DB-tax of multiple migrations.

Collections (schemas/model.go)

  • authorizer_service_accounts
  • authorizer_trusted_issuers

Storage interface (storage/provider.go)

  • 5 ServiceAccount methods: Add, Update, Delete, GetByID, List
  • 6 TrustedIssuer methods: Add, Update, Delete, GetByID, GetByIssuerURL, List

SQL provider (storage/db/sql/) — full GORM implementation

  • service_account.go — CRUD following webhook.go pattern
  • trusted_issuer.go — CRUD with indexed GetByIssuerURL (hot path: called on every client_assertion validation)
  • provider.goAutoMigrate extended with both new entities

NoSQL providers (mongodb, arangodb, cassandradb, dynamodb, couchbase)

  • Stubs returning "not implemented" errors so the build compiles
  • Replaced with full implementations in PR 2/5

Constants (internal/constants/grant_types.go)

  • RFC 6749/8693 grant types: client_credentials, token exchange URN
  • RFC 7521/7523 client assertion types: jwt-bearer, jwt-spiffe (preview)
  • RFC 8693 token type URNs
  • KeySourceType, IssuerType, AuthMethod string constants

Audit events (internal/constants/audit_event.go)

  • admin.service_account_{created,updated,deleted,secret_rotated}
  • admin.trusted_issuer_{created,updated,deleted}
  • token.client_credentials, token.exchange, token.workload_auth

PR chain

PR 1 (this) — schema + interface + SQL + constants  →  main
PR 2         — NoSQL provider implementations        →  PR 1
PR 3         — service layer + admin GraphQL API     →  PR 2
PR 4         — gRPC admin RPCs                       →  PR 3
PR 5         — client_credentials grant + tests      →  PR 4

Reviewer checklist

  • ServiceAccount struct tags correct across all 6 DB tag formats (json, bson, cql, dynamo, gorm)
  • TrustedIssuer struct tags correct; all Phase 4/5/6 fields present
  • gorm:"default:true" on IsActive, gorm:"default:'jwt_assertion'" on AuthMethod
  • SQL GetTrustedIssuerByIssuerURL uses WHERE issuer_url = ? (indexed — this is the hot path)
  • No client_secret exposure path in ServiceAccount (no AsAPIServiceAccount in this PR — that's PR 3)
  • NoSQL stubs all return "not implemented" errors (not panics)
  • AutoMigrate includes both new types
  • CollectionList and Collections consistent

Test plan

  • go build ./... passes (verified locally — clean)
  • go vet ./... passes (verified locally — clean)
  • make test-sqlite passes (no new tests in this PR; existing tests unaffected — schema addition is additive)
…, and M2M constants

Introduces the two new storage entities for the workload identity program:

- ServiceAccount: machine/workload OAuth2 client (client_id = UUID,
  client_secret = bcrypt hash). Intentionally omits all human-user fields.
- TrustedIssuer: external JWT issuer registry for K8s SA tokens, SPIFFE
  JWT-SVIDs, and generic OIDC workload tokens. Carries all fields needed
  for Phases 1-6: KeySourceType, ExpectedAud, EnableTokenReview (Phase 4),
  SpiffeRefreshHintSeconds (Phase 5), AuthMethod + proxy fields (Phase 6).

Storage changes:
- Added ServiceAccount and TrustedIssuer to CollectionList and Collections
- Extended storage.Provider interface with 5 ServiceAccount + 6 TrustedIssuer methods
- SQL provider: full GORM CRUD implementation + AutoMigrate registration
- NoSQL providers (mongodb, arangodb, cassandradb, dynamodb, couchbase):
  stubs returning not-implemented errors, to be replaced in PR 3

Constants:
- grant_types.go: RFC 6749/8693 grant types, RFC 7521/7523 client assertion
  types, RFC 8693 token type URNs, and TrustedIssuer key-source/issuer-type/
  auth-method identifiers
- audit_event.go: service_account and trusted_issuer audit events + resource types

Spec: docs/specs/WORKLOAD_IDENTITY_PROGRAM.md
- Add gorm:"index" on TrustedIssuer.ServiceAccountID (was missing; SQL
  ListTrustedIssuers filters on this column and would full-table-scan)
- Fix Count error swallowed in ListServiceAccounts and ListTrustedIssuers;
  now checks countRes.Error consistent with webhook.go pattern
- Capture time.Now().Unix() once per Add call so CreatedAt == UpdatedAt
- Guard UpdateServiceAccount/UpdateTrustedIssuer against partial-struct
  callers: returns error if CreatedAt == 0 (load-then-mutate enforced)
- Cascade-delete TrustedIssuers inside DeleteServiceAccount, matching the
  webhook/webhooklog pattern; prevents orphaned issuers post-delete
- AllowedScopes comment: deny-all on empty, trim+drop-empty requirement
- IsActive gorm default:true comment: documents the zero-value footgun
- Add AuditServiceAccountDeactivated/Activated and
  AuditTrustedIssuerTokenReviewChanged events for queryable IR signals
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant