You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
refactor(api/Principal): rebase #864 onto post-#907 group-only authz model (closes followup) #1010
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)).
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.principalFromUserAPIKey does a structural interface assertion against userRaw requiring GetID/GetEmail/GetRole. On base, *auth.User exposes 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 synthetic testUserRecord that satisfies the interface. Fixing it requires picking a real shape for the field extraction (add accessor methods on auth.User, or convert via auth.ValidateUserAPIKeyAPI to 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 Role string. The new Principal should either carry GroupIDs []string (and let downstream handlers fan out to HasPermissionAPI), or expose a thin IsAdmin()/Permissions view 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 against Role. 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)
typePrincipalstruct {
KindPrincipalKindUserIDstring// empty for PrincipalAdminAPIKeyEmailstring// empty for PrincipalAdminAPIKeyGroupIDs []string// post-#907 authz scope; "*" sentinel for admin-API-keySession*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-wave18 onto current feat/multicloud-web-frontend HEAD; resolve internal/api/middleware.go + internal/api/router_authuser_test.go conflicts.
Drop Principal.Role; replace with whatever shape is agreed (default: GroupIDs []string + small helper).
Replace the local GetID/GetEmail/GetRole interface assertion in principalFromUserAPIKey with the agreed extraction path; add an integration-style test that exercises the real *auth.User value 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.
Summary
PR #864 (
refactor(api): requireAuth returns resolved Principal, branchfix/194-wave18) is structurally incompatible with the currentfeat/multicloud-web-frontendbase after PR #907 (group-membership-only authorization, removedSession.RoleandUser.Role) landed. The PR predates that reshape and still encodes role-based identity in the newPrincipaltype. 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 commitf6d3dc51f(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)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.RolefromPrincipaland the bearer-path Principal construction, three further problems would remain: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).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).{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)
with a helper
func (p *Principal) HasPermission(ctx, auth, action, resource)that fans out toAuthServiceInterface.HasPermissionAPIfor 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
fix/194-wave18onto currentfeat/multicloud-web-frontendHEAD; resolveinternal/api/middleware.go+internal/api/router_authuser_test.goconflicts.Principal.Role; replace with whatever shape is agreed (default:GroupIDs []string+ small helper).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).{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).vetfailure).Related
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)