Skip to content

refactor(api/Principal): rebase #864 onto post-#907 group-only authz model (closes followup) #1010

Closed

Description

@cristim

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

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:

  1. 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).
  2. 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).
  3. 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)

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-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.
  • requireAdmin is NOT re-introduced as a role-based check (Revamp authorization: group-membership-only (remove roles), require >=1 group per user #907 already moved it to HasPermissionAPI).
  • Pre-commit CI green on the merge ref (no vet failure).

Related

Activity

  1. cristim commented on Jul 27, 2026

    @cristim
    MemberAuthor

    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.

  2. cristim commented on Aug 24, 2026

    @cristim
    MemberAuthor

    Already implemented on main. Closing with evidence rather than scheduling work.

    Verified against origin/main (ae1e632cb):

    • Principal.Role is gone. internal/api/middleware.go:73 defines Principal with Kind, Session, UserID, Email, APIKeyID and no role field. The remaining Principal/Role co-occurrences in the tree are unrelated (Azure service principals, IAM policy tests).
    • The structural GetID/GetEmail/GetRole assertion is replaced by a fail-closed concrete-type assertion. middleware.go:162 does user, ok := userRaw.(*auth.User) and denies on failure, with the rationale recorded inline: a partially-populated Principal is never returned.
    • The real-*auth.User test coverage, the cross-product cells, and requireAdmin going through HasPermissionAPI(ActionAdmin, ResourceAll) are all present.

    Landed in 702b8860b, extended by 427e81b7c. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions