Repository navigation
refactor(api/Principal): rebase #864 onto post-#907 group-only authz model (closes followup) #1010
Description
Summary
PR #864 (refactor(api): requireAuth returns resolved Principal, branch fix/194-wave18) is structurally incompatible with the current feat/multicloud-web-frontend base after PR #907 (group-membership-only authorization, removed Session.Role and User.Role) landed. The PR predates that reshape and still encodes role-based identity in the new Principal type. A rebase plus a small design call is needed before the PR can land. Adversarial review of PR #864 surfaced this in the failing CI run on commit f6d3dc51f (internal/api/middleware.go:166:20: session.Role undefined (type *Session has no field or method Role)).
Where it lives
internal/api/middleware.go(PR refactor(api): requireAuth returns resolved Principal (closes #194) #864 introduced types/funcs):Principal.Role string(line 67)principalFromBearerTokenreadssession.Role(line 164)principalFromUserAPIKeyassertsuserRaw.(interface{ GetID()string; GetEmail()string; GetRole()string })(lines 131-145)
internal/api/router_authuser_test.go: buildsSession{UserID, Role: "user"}and atestUserRecordwhoseGetRole()returns"user"(lines 72, 123, 154-187)- Base post-PR Revamp authorization: group-membership-only (remove roles), require >=1 group per user #907:
internal/api/types.goSessionhas onlyUserID,Email(noRole)internal/auth/types.goUserno longer hasRole;GroupIDs []stringreplaces itinternal/api/middleware.gorequireAdminusesHasPermissionAPI(ctx, userID, ActionAdmin, ResourceAll)instead ofsession.Role == "admin"requiresCSRFValidation(method, path, req)(addedreqparam, issue sec: CSRF exemption for approve/cancel POST is correct but overly broad — scope to token-only requests only #404)
Why this needs a design call rather than a tactical patch
If I had just dropped session.Role from Principal and the bearer-path Principal construction, three further problems would remain:
- Silent runtime regression on the user-API-key path.
principalFromUserAPIKeydoes a structural interface assertion againstuserRawrequiringGetID/GetEmail/GetRole. On base,*auth.Userexposes none of those accessors — the assertion ALWAYS fails closed at runtime. Every valid user-API-key request would 401. Tests don't catch it because they pass a synthetictestUserRecordthat satisfies the interface. Fixing it requires picking a real shape for the field extraction (add accessor methods onauth.User, or convert viaauth.ValidateUserAPIKeyAPIto a typed*APIUser, or push the conversion into the interface). - Principal as "identity + role" is the wrong shape post-Revamp authorization: group-membership-only (remove roles), require >=1 group per user #907. Authorization is now derived from group permissions, not a
Rolestring. The newPrincipalshould either carryGroupIDs []string(and let downstream handlers fan out toHasPermissionAPI), or expose a thinIsAdmin()/Permissionsview computed at construction. Both are reasonable but mutually exclusive. Picking one sets the contract every future caller will see (the whole point of PR refactor(api): requireAuth returns resolved Principal (closes #194) #864 is to give downstream handlers a typed identity). - Tests need to be re-written, not just adjusted. The
{has-auth, lacks-auth, admin, user}cross-product is currently expressed againstRole. In the group-only model the cells need to be{admin-api-key, user-with-admin-group, user-with-other-groups, user-with-zero-groups, no-credential, invalid-credential}— coverage that the current PR doesn't reach.
Proposed shape (for discussion)
type Principal struct {
Kind PrincipalKind
UserID string // empty for PrincipalAdminAPIKey
Email string // empty for PrincipalAdminAPIKey
GroupIDs []string // post-#907 authz scope; "*" sentinel for admin-API-key
Session *Session // non-nil only for PrincipalSession
}with a helper func (p *Principal) HasPermission(ctx, auth, action, resource) that fans out to AuthServiceInterface.HasPermissionAPI for session/user-API-key principals and short-circuits true for admin-API-key. That keeps the "callers don't re-resolve identity" goal of #194 without re-introducing the role string.
Acceptance criteria
- Rebase
fix/194-wave18onto currentfeat/multicloud-web-frontendHEAD; resolveinternal/api/middleware.go+internal/api/router_authuser_test.goconflicts. - Drop
Principal.Role; replace with whatever shape is agreed (default:GroupIDs []string+ small helper). - Replace the local
GetID/GetEmail/GetRoleinterface assertion inprincipalFromUserAPIKeywith the agreed extraction path; add an integration-style test that exercises the real*auth.Uservalue path (catches the regression that the current PR ships). - Tests cover the cross-product
{admin-api-key, user-with-admin-group, user-with-non-admin-group, user-with-zero-groups, no-credential, invalid-bearer, valid-user-api-key, invalid-user-api-key, nil-auth}— at least one cell per dimension. -
requireAdminis NOT re-introduced as a role-based check (Revamp authorization: group-membership-only (remove roles), require >=1 group per user #907 already moved it toHasPermissionAPI). - Pre-commit CI green on the merge ref (no
vetfailure).
Related
- PR refactor(api): requireAuth returns resolved Principal (closes #194) #864 (this work)
- Issue refactor(api): return resolved Principal from requireAuth/authenticate so handlers don't re-fetch identity #194 (original ask)
- PR Revamp authorization: group-membership-only (remove roles), require >=1 group per user #907 / commit
8880710a8(group-membership-only authz that caused the divergence) feedback_fail_closed_middleware.md(the nil-auth/bad-record paths must continue to fail closed)
Activity
cristim commentedon Jul 27, 2026
Verification sweep against
main(101f099fb) on 2026-07-27 finds this already resolved.Principal no longer has a Role field, so the structural-assertion regression this tracked is fixed.
Evidence: commit 702b886 (PR #864).
Recommending close.
cristim commentedon Aug 24, 2026
Already implemented on
main. Closing with evidence rather than scheduling work.Verified against
origin/main(ae1e632cb):Principal.Roleis gone.internal/api/middleware.go:73definesPrincipalwithKind,Session,UserID,Email,APIKeyIDand no role field. The remainingPrincipal/Roleco-occurrences in the tree are unrelated (Azure service principals, IAM policy tests).GetID/GetEmail/GetRoleassertion is replaced by a fail-closed concrete-type assertion.middleware.go:162doesuser, ok := userRaw.(*auth.User)and denies on failure, with the rationale recorded inline: a partially-populated Principal is never returned.*auth.Usertest coverage, the cross-product cells, andrequireAdmingoing throughHasPermissionAPI(ActionAdmin, ResourceAll)are all present.Landed in
702b8860b, extended by427e81b7c. Neither carried a closing trailer, which is why this stayed open.If a residual axis remains that this does not cover, please reopen naming it specifically.