feat(m2m): service layer + admin GraphQL API for ServiceAccount + TrustedIssuer [Phase 1 — PR 3/5]#644
Merged
lakhansamani merged 2 commits intoJul 6, 2026
Conversation
Wire the Phase 1 workload-identity service layer and admin GraphQL surface on top of the existing storage CRUD. - service layer: CreateServiceAccount/Update/Delete/RotateSecret/get/list and AddTrustedIssuer/Update/Delete/get/list, each gated by requireSuperAdmin and audited via the existing admin.* audit constants - secrets: 32-byte crypto/rand plaintext, bcrypt cost 12, hash-only storage, one-time reveal via CreateServiceAccountResponse; reads never surface it - allowed_scopes normalized (trim, drop-empty, dedupe) and rejected when empty on both create and update - IsActive set explicitly on create (never rely on the GORM default) - GraphQL schema types/inputs/queries/mutations + regenerated gqlgen output - AsAPIServiceAccount/AsAPITrustedIssuer converters (no client_secret field) - integration tests: one-time secret reveal, rotation invalidates old secret, service-account delete cascades to trusted issuers, empty-scope rejection, partial-update preservation, super-admin enforcement
UpdateTrustedIssuer let a caller blank expected_aud even though AddTrustedIssuer already rejects it empty. expected_aud backs the audience-binding invariant the token endpoint will enforce later, so the update path shouldn't be the hole that lets it go empty.
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the transport-agnostic service layer and admin GraphQL API for
ServiceAccountandTrustedIssuer, on top of the storage CRUD landed in #641/#642. Follows the repo'sadmin_webhooks.go/add_webhook.goidiom exactly — thin GraphQL resolvers delegating tointernal/service/.Chains on: #642 (merged into this integration branch)
What's in this PR
Service layer
internal/service/admin_service_accounts.go— Create/Update/Delete/Rotate/Get/List, plusgenerateServiceAccountSecret/normalizeScopeshelpersinternal/service/admin_trusted_issuers.go— Add/Update/Delete/Get/ListGraphQL (
internal/graph/schema.graphqls, regenerated viamake generate-graphql)ServiceAccount(no client_secret field),CreateServiceAccountResponse(the only place client_secret appears — reused by rotate),ServiceAccounts,TrustedIssuer,TrustedIssuers_create_service_account,_update_service_account,_delete_service_account,_rotate_service_account_secret,_add_trusted_issuer,_update_trusted_issuer,_delete_trusted_issuer_service_account,_service_accounts,_trusted_issuer,_trusted_issuersinternal/graphql/Schema conversions:
AsAPIServiceAccount()/AsAPITrustedIssuer()on the storage schemas (deferred from #641/#642 since the GraphQL model types didn't exist yet).Security-critical invariants
crypto/rand) → base64url, hashed withbcryptcost 12 (not the package default of 10) before storage.model.ServiceAccounthas no client_secret field, so no read path can leak it.IsActiveset explicitlytrueon create — does not rely on the GORM column default.allowed_scopesrejected on create and update (normalized: trimmed, deduped, empty segments dropped).AddTrustedIssuerdefaultssubject_claimto"sub"and rejects binding to a non-existent service account.PR chain
Test plan
make generate-graphqlmake fmt(Go only — reverted an unrelated frontend reformat side effect)make lint— lint-go: 0 issues (lint-ts fails on pre-existing frontend prettier drift unrelated to this PR)TestServiceAccountAdmin(8 subtests),TestTrustedIssuerAdmin(7 subtests) — cover secret one-time-reveal, bcrypt cost 12, cascade delete, empty-scope rejection, non-admin rejectionmake test-sqlite— pass