This is an automated email from the ASF dual-hosted git repository.

lahirujayathilake pushed a commit to branch auth
in repository https://gitbox.apache.org/repos/asf/airavata-custos.git


The following commit(s) were added to refs/heads/auth by this push:
     new 83be015aa Replace auth middleware with identity router and privilege 
validation
83be015aa is described below

commit 83be015aaea19b78d992ffc74bdcaa70b9184bf0
Author: lahiruj <[email protected]>
AuthorDate: Tue Jun 23 11:59:02 2026 -0400

    Replace auth middleware with identity router and privilege validation
---
 .../migrations/000014_project_memberships.up.sql   |   2 +-
 internal/server/audit_handlers_test.go             |  10 +-
 internal/server/auth.go                            | 159 --------
 internal/server/integration_common_test.go         |   5 +-
 internal/server/privilege.go                       |  56 ++-
 internal/server/privilege_integration_test.go      |  69 ++--
 internal/server/role.go                            | 100 +++--
 internal/server/server.go                          | 441 +++++++++++----------
 internal/server/server_trace_test.go               |   4 +-
 internal/server/testidentity_test.go               |  33 ++
 pkg/identity/identity.go                           |  14 +
 11 files changed, 386 insertions(+), 507 deletions(-)

diff --git a/internal/db/migrations/000014_project_memberships.up.sql 
b/internal/db/migrations/000014_project_memberships.up.sql
index e14847b38..7626447e6 100644
--- a/internal/db/migrations/000014_project_memberships.up.sql
+++ b/internal/db/migrations/000014_project_memberships.up.sql
@@ -28,7 +28,7 @@ CREATE TABLE project_memberships (
     CONSTRAINT fk_project_memberships_project FOREIGN KEY (project_id) 
REFERENCES projects(id) ON DELETE CASCADE,
     CONSTRAINT fk_project_memberships_user    FOREIGN KEY (user_id)    
REFERENCES users(id)    ON DELETE CASCADE,
     CONSTRAINT chk_project_memberships_role   CHECK (role IN ('PI', 'CO_PI', 
'ALLOCATION_MANAGER'))
-);
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
 
 -- Carry forward existing special roles that 000013 wrote onto
 -- compute_allocation_memberships. The same user can appear on multiple
diff --git a/internal/server/audit_handlers_test.go 
b/internal/server/audit_handlers_test.go
index 027b3b4a6..d8596320c 100644
--- a/internal/server/audit_handlers_test.go
+++ b/internal/server/audit_handlers_test.go
@@ -27,6 +27,7 @@ import (
        "time"
 
        "github.com/apache/airavata-custos/internal/store"
+       "github.com/apache/airavata-custos/pkg/identity"
        "github.com/apache/airavata-custos/pkg/models"
 )
 
@@ -85,9 +86,16 @@ func mustHexTraceID(t *testing.T) string {
        return strings.Repeat("ab", 16)
 }
 
+// newAuditServer boots an httptest.Server around the audit handlers with a
+// fake AuditTraceStore. svc is nil, audit handlers don't reach into *Service.
 func newAuditServer(t *testing.T, deps *AdminDeps) *httptest.Server {
        t.Helper()
-       srv := httptest.NewServer(New(nil, deps))
+       router := identity.NewRouter(http.NewServeMux())
+       inner := New(nil, router, deps)
+       wrap := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+               inner.ServeHTTP(w, withTestCaller(r, "test-user"))
+       })
+       srv := httptest.NewServer(wrap)
        t.Cleanup(srv.Close)
        return srv
 }
diff --git a/internal/server/auth.go b/internal/server/auth.go
deleted file mode 100644
index 14f5aaefc..000000000
--- a/internal/server/auth.go
+++ /dev/null
@@ -1,159 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-package server
-
-import (
-       "context"
-       "errors"
-       "net/http"
-       "sync"
-       "time"
-
-       "github.com/apache/airavata-custos/pkg/common"
-       "github.com/apache/airavata-custos/pkg/models"
-)
-
-// callerHeader identifies the caller. TODO A JWT-verification middleware 
should set
-// this header from the verified `sub` claim after validating the token
-// against the IdP's JWKS endpoint. Until that lands, the value
-// is supplied directly by the caller.
-const callerHeader = "X-Custos-User-Id"
-
-// authProfileTTL bounds how long the middleware will trust a cached
-// privilege set before re-reading the DB.
-//
-// TODO: make configurable via env (eg. AUTH_CACHE_TTL_SECONDS), cap at 60s.
-// TODO support caching for multi-instance, the cache is per-process.
-const authProfileTTL = 5 * time.Second
-
-// authProfile is the cached snapshot of a user's effective privileges.
-type authProfile struct {
-       privileges map[models.PrivilegeKey]struct{}
-}
-
-func (p *authProfile) has(privilege models.PrivilegeKey) bool {
-       if p == nil {
-               return false
-       }
-       _, ok := p.privileges[privilege]
-       return ok
-}
-
-// authProfileCache is a tiny in-process TTL cache for "userID -> privilege 
set".
-// An empty profile (zero privileges) is still cached so users with no grants 
do not read the DB everytime.
-type authProfileCache struct {
-       mu      sync.Mutex
-       entries map[string]authProfileCacheEntry
-       ttl     time.Duration
-}
-
-type authProfileCacheEntry struct {
-       profile *authProfile
-       expires time.Time
-}
-
-func newAuthProfileCache(ttl time.Duration) *authProfileCache {
-       return &authProfileCache{
-               entries: make(map[string]authProfileCacheEntry),
-               ttl:     ttl,
-       }
-}
-
-func (c *authProfileCache) get(userID string) (*authProfile, bool) {
-       c.mu.Lock()
-       defer c.mu.Unlock()
-       e, ok := c.entries[userID]
-       if !ok || time.Now().After(e.expires) {
-               return nil, false
-       }
-       return e.profile, true
-}
-
-func (c *authProfileCache) set(userID string, profile *authProfile) {
-       c.mu.Lock()
-       defer c.mu.Unlock()
-       c.entries[userID] = authProfileCacheEntry{
-               profile: profile,
-               expires: time.Now().Add(c.ttl),
-       }
-}
-
-// invalidate drops the cache entry for userID. Called after any grant or
-// revoke so subsequent requests reflect the new state without waiting for TTL.
-func (c *authProfileCache) invalidate(userID string) {
-       c.mu.Lock()
-       defer c.mu.Unlock()
-       delete(c.entries, userID)
-}
-
-// invalidateAll empties the cache. Used when one mutation affects many
-// users at once: adding or removing a privilege from a role (every holder
-// gains/loses it) or deleting a role.
-func (c *authProfileCache) invalidateAll() {
-       c.mu.Lock()
-       defer c.mu.Unlock()
-       c.entries = make(map[string]authProfileCacheEntry)
-}
-
-// requirePrivilege returns a middleware that admits the request only if the
-// caller (identified by callerHeader) holds the named active privilege.
-//
-// Responses:
-//   - 401 Unauthorized - no caller header
-//   - 403 Forbidden - caller is identified but does not hold the privilege
-//   - 503 Service Unavailable - auth-profile lookup failed
-//
-// Fail-closed: a DB failure NEVER reads as 403
-func (s *Server) requirePrivilege(p models.PrivilegeKey, next 
http.HandlerFunc) http.HandlerFunc {
-       return func(w http.ResponseWriter, r *http.Request) {
-               callerID := r.Header.Get(callerHeader)
-               if callerID == "" {
-                       common.WriteError(w, http.StatusUnauthorized, 
errors.New("missing "+callerHeader+" header"))
-                       return
-               }
-               profile, err := s.lookupAuthProfile(r.Context(), callerID)
-               if err != nil {
-                       common.WriteError(w, http.StatusServiceUnavailable, 
errors.New("auth lookup failed"))
-                       return
-               }
-               if !profile.has(p) {
-                       common.WriteError(w, http.StatusForbidden, 
errors.New("insufficient privilege"))
-                       return
-               }
-               next(w, r)
-       }
-}
-
-// lookupAuthProfile returns the caller's effective privilege snapshot
-// (direct grants + role-derived privileges), hitting the cache first
-// and falling back to the DB. Errors propagate so middleware can fail closed.
-func (s *Server) lookupAuthProfile(ctx context.Context, userID string) 
(*authProfile, error) {
-       if cached, ok := s.authCache.get(userID); ok {
-               return cached, nil
-       }
-       keys, err := s.svc.EffectivePrivileges(ctx, userID)
-       if err != nil {
-               return nil, err
-       }
-       profile := &authProfile{privileges: 
make(map[models.PrivilegeKey]struct{}, len(keys))}
-       for _, k := range keys {
-               profile.privileges[k] = struct{}{}
-       }
-       s.authCache.set(userID, profile)
-       return profile, nil
-}
diff --git a/internal/server/integration_common_test.go 
b/internal/server/integration_common_test.go
index f3a9282c7..d36225331 100644
--- a/internal/server/integration_common_test.go
+++ b/internal/server/integration_common_test.go
@@ -20,6 +20,7 @@
 package server
 
 import (
+       "net/http"
        "os"
        "sync"
        "testing"
@@ -29,6 +30,7 @@ import (
 
        "github.com/apache/airavata-custos/internal/db"
        "github.com/apache/airavata-custos/pkg/events"
+       "github.com/apache/airavata-custos/pkg/identity"
        "github.com/apache/airavata-custos/pkg/models"
        "github.com/apache/airavata-custos/pkg/service"
 )
@@ -69,7 +71,8 @@ func setupTestStack(t *testing.T) (*sqlx.DB, 
*service.Service, *Server) {
        }
        truncateAll(t, sharedDB)
        svc := service.New(sharedDB, events.New())
-       return sharedDB, svc, New(svc)
+       router := identity.NewRouter(http.NewServeMux())
+       return sharedDB, svc, New(svc, router, nil)
 }
 
 func truncateAll(t *testing.T, database *sqlx.DB) {
diff --git a/internal/server/privilege.go b/internal/server/privilege.go
index 401cb7d73..3f1553cbb 100644
--- a/internal/server/privilege.go
+++ b/internal/server/privilege.go
@@ -22,42 +22,35 @@ import (
        "net/http"
 
        "github.com/apache/airavata-custos/pkg/common"
+       "github.com/apache/airavata-custos/pkg/identity"
        "github.com/apache/airavata-custos/pkg/models"
 )
 
 // @Summary    Get caller's effective privileges
 // @Description        Effective set is direct grants UNION every privilege 
carried by every role the caller holds.
 // @Tags       Caller
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Success    200     {object}        
object{privileges=[]models.PrivilegeKey}        "Effective privilege set"
-// @Failure    401     {object}        object{error=string}    "Missing 
X-Custos-User-Id header"
-// @Failure    503     {object}        object{error=string}    "Auth lookup 
failed"
+// @Failure    401     {object}        object{error=string}    "Missing 
authenticated caller"
 // @Router     /user/privileges [get]
 func (s *Server) getCallerPrivileges(w http.ResponseWriter, r *http.Request) {
-       callerID := r.Header.Get(callerHeader)
-       if callerID == "" {
-               common.WriteError(w, http.StatusUnauthorized, 
errors.New("missing "+callerHeader+" header"))
+       if requireCaller(w, r) == nil {
                return
        }
-       profile, err := s.lookupAuthProfile(r.Context(), callerID)
-       if err != nil {
-               common.WriteError(w, http.StatusServiceUnavailable, 
errors.New("auth lookup failed"))
-               return
-       }
-       keys := make([]models.PrivilegeKey, 0, len(profile.privileges))
-       for k := range profile.privileges {
-               keys = append(keys, k)
+       keys := identity.PrivilegesFromContext(r.Context())
+       if keys == nil {
+               keys = []models.PrivilegeKey{}
        }
        common.WriteJSON(w, http.StatusOK, map[string]any{"privileges": keys})
 }
 
 // @Summary    List the declared privilege catalog
 // @Tags       Privileges
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Success    200     {array} models.PrivilegeKey
-// @Failure    401     {object}        object{error=string}    "Missing caller 
header"
+// @Failure    401     {object}        object{error=string}    "Missing or 
invalid bearer token"
 // @Failure    403     {object}        object{error=string}    "Caller lacks 
privileges:grant"
 // @Router     /privileges/catalog [get]
 func (s *Server) getPrivilegeCatalog(w http.ResponseWriter, _ *http.Request) {
@@ -67,7 +60,7 @@ func (s *Server) getPrivilegeCatalog(w http.ResponseWriter, _ 
*http.Request) {
 // @Summary    List a user's direct privilege grants
 // @Description        Returns DIRECT grants only (not role-derived). Combine 
with `GET /users/{id}/roles` for the full picture.
 // @Tags       Privileges
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "User ID"
 // @Success    200     {array} models.UserPrivilege
@@ -91,7 +84,7 @@ func (s *Server) listUserPrivileges(w http.ResponseWriter, r 
*http.Request) {
 // @Summary    List direct holders of a privilege
 // @Description        Role-derived holders are NOT listed here.
 // @Tags       Privileges
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      key     path    models.PrivilegeKey     true    "Privilege key"
 // @Success    200     {array} models.UserPrivilege
@@ -118,14 +111,14 @@ type grantPrivilegeRequest struct {
 
 // @Summary    Grant a direct privilege to a user
 // @Tags       Privileges
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      id      path    string  true    "User ID"
 // @Param      request body    grantPrivilegeRequest   true    "Grant payload"
 // @Success    201     {object}        models.UserPrivilege
 // @Failure    400     {object}        object{error=string}    "Bad request"
-// @Failure    401     {object}        object{error=string}    "Missing caller 
header"
+// @Failure    401     {object}        object{error=string}    "Missing or 
invalid bearer token"
 // @Failure    403     {object}        object{error=string}    "Caller lacks 
privileges:grant"
 // @Failure    409     {object}        object{error=string}    "Privilege 
already active for user"
 // @Router     /users/{id}/privileges [post]
@@ -135,9 +128,8 @@ func (s *Server) grantPrivilege(w http.ResponseWriter, r 
*http.Request) {
                common.WriteError(w, http.StatusBadRequest, errors.New("user id 
is required"))
                return
        }
-       granterID := r.Header.Get(callerHeader)
-       if granterID == "" {
-               common.WriteError(w, http.StatusUnauthorized, 
errors.New("missing "+callerHeader+" header"))
+       caller := requireCaller(w, r)
+       if caller == nil {
                return
        }
        var req grantPrivilegeRequest
@@ -145,12 +137,12 @@ func (s *Server) grantPrivilege(w http.ResponseWriter, r 
*http.Request) {
                common.WriteError(w, http.StatusBadRequest, err)
                return
        }
-       grant, err := s.svc.GrantPrivilege(r.Context(), userID, req.Privilege, 
granterID, req.Reason)
+       grant, err := s.svc.GrantPrivilege(r.Context(), userID, req.Privilege, 
caller.UserID, req.Reason)
        if err != nil {
                common.WriteServiceError(w, err)
                return
        }
-       s.authCache.invalidate(userID)
+       s.svc.InvalidateAllIdentities()
        common.WriteJSON(w, http.StatusCreated, grant)
 }
 
@@ -161,14 +153,14 @@ type revokePrivilegeRequest struct {
 // @Summary    Revoke a direct privilege from a user
 // @Description        `privileges:grant` itself cannot be self-revoked or 
revoked from the last holder.
 // @Tags       Privileges
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Param      id      path    string  true    "User ID"
 // @Param      key     path    models.PrivilegeKey     true    "Privilege key"
 // @Param      request body    revokePrivilegeRequest  false   "Optional 
reason"
 // @Success    204     "No Content"
 // @Failure    400     {object}        object{error=string}    "Self-revoke 
meta, last-holder, or unknown key"
-// @Failure    401     {object}        object{error=string}    "Missing caller 
header"
+// @Failure    401     {object}        object{error=string}    "Missing or 
invalid bearer token"
 // @Failure    403     {object}        object{error=string}    "Caller lacks 
privileges:grant"
 // @Failure    404     {object}        object{error=string}    "No active 
grant for that key"
 // @Router     /users/{id}/privileges/{key} [delete]
@@ -179,18 +171,16 @@ func (s *Server) revokePrivilege(w http.ResponseWriter, r 
*http.Request) {
                common.WriteError(w, http.StatusBadRequest, errors.New("user id 
and privilege key are required"))
                return
        }
-       revokerID := r.Header.Get(callerHeader)
-       if revokerID == "" {
-               common.WriteError(w, http.StatusUnauthorized, 
errors.New("missing "+callerHeader+" header"))
+       caller := requireCaller(w, r)
+       if caller == nil {
                return
        }
        var req revokePrivilegeRequest
        _ = common.DecodeJSON(r, &req)
-       if err := s.svc.RevokePrivilege(r.Context(), userID, key, revokerID, 
req.Reason); err != nil {
+       if err := s.svc.RevokePrivilege(r.Context(), userID, key, 
caller.UserID, req.Reason); err != nil {
                common.WriteServiceError(w, err)
                return
        }
-       s.authCache.invalidate(userID)
-       s.authCache.invalidate(revokerID)
+       s.svc.InvalidateAllIdentities()
        w.WriteHeader(http.StatusNoContent)
 }
diff --git a/internal/server/privilege_integration_test.go 
b/internal/server/privilege_integration_test.go
index 18cc29d0f..c8a595aab 100644
--- a/internal/server/privilege_integration_test.go
+++ b/internal/server/privilege_integration_test.go
@@ -30,21 +30,24 @@ import (
        "github.com/apache/airavata-custos/pkg/models"
 )
 
-func TestGetCallerPrivileges_MissingHeader_401(t *testing.T) {
+func TestGetCallerPrivileges_NoCaller_401(t *testing.T) {
        _, _, srv := setupTestStack(t)
        rr := httptest.NewRecorder()
        srv.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, 
"/user/privileges", nil))
+       // Route is router.RequireAuth, no privilege check. Without a caller on
+       // ctx, the handler's requireCaller returns 401.
        if rr.Code != http.StatusUnauthorized {
                t.Errorf("status: got %d, want 401", rr.Code)
        }
 }
 
 func TestGetCallerPrivileges_NoGrants_ReturnsEmpty(t *testing.T) {
-       database, _, srv := setupTestStack(t)
-       user := seedUser(t, database, "[email protected]")
+       _, _, srv := setupTestStack(t)
        rr := httptest.NewRecorder()
        req := httptest.NewRequest(http.MethodGet, "/user/privileges", nil)
-       req.Header.Set(callerHeader, user)
+       // Empty privilege set on ctx mirrors a caller the middleware resolved
+       // with no direct grants and no role-derived privileges.
+       req = withTestCaller(req, "u-1")
        srv.ServeHTTP(rr, req)
        if rr.Code != http.StatusOK {
                t.Fatalf("status: got %d, want 200", rr.Code)
@@ -61,13 +64,10 @@ func TestGetCallerPrivileges_NoGrants_ReturnsEmpty(t 
*testing.T) {
 }
 
 func TestGetCallerPrivileges_WithGrants(t *testing.T) {
-       database, _, srv := setupTestStack(t)
-       user := seedUser(t, database, "[email protected]")
-       seedPrivilegeGrant(t, database, user)
-
+       _, _, srv := setupTestStack(t)
        rr := httptest.NewRecorder()
        req := httptest.NewRequest(http.MethodGet, "/user/privileges", nil)
-       req.Header.Set(callerHeader, user)
+       req = withTestCaller(req, "u-1", models.PrivilegeGrant)
        srv.ServeHTTP(rr, req)
        if rr.Code != http.StatusOK {
                t.Fatalf("status: got %d, want 200", rr.Code)
@@ -88,7 +88,8 @@ func TestRequirePrivilege_NoGrants_403(t *testing.T) {
        user := seedUser(t, database, "[email protected]")
        rr := httptest.NewRecorder()
        req := httptest.NewRequest(http.MethodGet, "/privileges/catalog", nil)
-       req.Header.Set(callerHeader, user)
+       // Inject a caller whose ctx privilege set does NOT include 
privileges:grant.
+       req = withTestCaller(req, user)
        srv.ServeHTTP(rr, req)
        if rr.Code != http.StatusForbidden {
                t.Errorf("status: got %d, want 403", rr.Code)
@@ -101,7 +102,7 @@ func TestRequirePrivilege_WithGrant_200(t *testing.T) {
        seedPrivilegeGrant(t, database, user)
        rr := httptest.NewRecorder()
        req := httptest.NewRequest(http.MethodGet, "/privileges/catalog", nil)
-       req.Header.Set(callerHeader, user)
+       req = withTestCaller(req, user, models.PrivilegeGrant)
        srv.ServeHTTP(rr, req)
        if rr.Code != http.StatusOK {
                t.Errorf("status: got %d, want 200", rr.Code)
@@ -124,8 +125,8 @@ func TestGrantPrivilegeEndpoint_HappyPath(t *testing.T) {
        body, _ := json.Marshal(map[string]any{"privilege": "amie:read", 
"reason": "ops view"})
        rr := httptest.NewRecorder()
        req := httptest.NewRequest(http.MethodPost, 
"/users/"+target+"/privileges", bytes.NewReader(body))
-       req.Header.Set(callerHeader, granter)
        req.Header.Set("Content-Type", "application/json")
+       req = withTestCaller(req, granter, models.PrivilegeGrant)
        srv.ServeHTTP(rr, req)
        if rr.Code != http.StatusCreated {
                t.Fatalf("status: got %d, want 201, body=%s", rr.Code, 
rr.Body.String())
@@ -143,7 +144,8 @@ func TestGrantPrivilegeEndpoint_GranterWithoutMeta_403(t 
*testing.T) {
        body, _ := json.Marshal(map[string]any{"privilege": "amie:read"})
        rr := httptest.NewRecorder()
        req := httptest.NewRequest(http.MethodPost, 
"/users/"+target+"/privileges", bytes.NewReader(body))
-       req.Header.Set(callerHeader, plain)
+       // Caller does NOT carry privileges:grant in its ctx set → gate denies.
+       req = withTestCaller(req, plain)
        srv.ServeHTTP(rr, req)
        if rr.Code != http.StatusForbidden {
                t.Errorf("status: got %d, want 403 (granter lacks 
privileges:grant)", rr.Code)
@@ -161,7 +163,7 @@ func TestRevokePrivilegeEndpoint_HappyPath(t *testing.T) {
        body, _ := json.Marshal(map[string]any{"reason": "rotated"})
        rr := httptest.NewRecorder()
        req := httptest.NewRequest(http.MethodDelete, 
"/users/"+target+"/privileges/amie:read", bytes.NewReader(body))
-       req.Header.Set(callerHeader, granter)
+       req = withTestCaller(req, granter, models.PrivilegeGrant)
        srv.ServeHTTP(rr, req)
        if rr.Code != http.StatusNoContent {
                t.Fatalf("status: got %d, want 204, body=%s", rr.Code, 
rr.Body.String())
@@ -177,7 +179,7 @@ func TestRevokePrivilegeEndpoint_SelfRevokeMeta_400(t 
*testing.T) {
        seedPrivilegeGrant(t, database, user)
        rr := httptest.NewRecorder()
        req := httptest.NewRequest(http.MethodDelete, 
"/users/"+user+"/privileges/privileges:grant", nil)
-       req.Header.Set(callerHeader, user)
+       req = withTestCaller(req, user, models.PrivilegeGrant)
        srv.ServeHTTP(rr, req)
        if rr.Code != http.StatusBadRequest {
                t.Errorf("status: got %d, want 400 (self-revoke of meta)", 
rr.Code)
@@ -194,7 +196,7 @@ func TestListUserPrivilegesEndpoint(t *testing.T) {
        }
        rr := httptest.NewRecorder()
        req := httptest.NewRequest(http.MethodGet, 
"/users/"+target+"/privileges", nil)
-       req.Header.Set(callerHeader, granter)
+       req = withTestCaller(req, granter, models.PrivilegeGrant)
        srv.ServeHTTP(rr, req)
        if rr.Code != http.StatusOK {
                t.Fatalf("status: got %d, want 200, body=%s", rr.Code, 
rr.Body.String())
@@ -218,7 +220,7 @@ func TestListPrivilegeHoldersEndpoint(t *testing.T) {
        }
        rr := httptest.NewRecorder()
        req := httptest.NewRequest(http.MethodGet, 
"/privileges/amie:read/holders", nil)
-       req.Header.Set(callerHeader, granter)
+       req = withTestCaller(req, granter, models.PrivilegeGrant)
        srv.ServeHTTP(rr, req)
        if rr.Code != http.StatusOK {
                t.Fatalf("status: got %d, want 200, body=%s", rr.Code, 
rr.Body.String())
@@ -232,36 +234,23 @@ func TestListPrivilegeHoldersEndpoint(t *testing.T) {
        }
 }
 
-func TestRequirePrivilege_StaleCacheStillReturns403AfterRevoke(t *testing.T) {
+func TestRevokePropagatesCacheInvalidation(t *testing.T) {
        database, svc, srv := setupTestStack(t)
        a := seedUser(t, database, "[email protected]")
        b := seedUser(t, database, "[email protected]")
        seedPrivilegeGrant(t, database, a)
        seedPrivilegeGrant(t, database, b)
 
-       // b warms the cache.
-       warm := httptest.NewRecorder()
-       warmReq := httptest.NewRequest(http.MethodGet, "/privileges/catalog", 
nil)
-       warmReq.Header.Set(callerHeader, b)
-       srv.ServeHTTP(warm, warmReq)
-       if warm.Code != http.StatusOK {
-               t.Fatalf("warm-up: got %d, want 200", warm.Code)
-       }
-
-       // a revokes b's privileges:grant via service (avoids the self-revoke
-       // guard since a != b).
-       if err := svc.RevokePrivilege(context.Background(), b, 
models.PrivilegeGrant, a, "rotated"); err != nil {
-               t.Fatalf("revoke b's grant: %v", err)
+       // Drive the revoke and confirm a subsequent service lookup reflects it.
+       rr := httptest.NewRecorder()
+       req := httptest.NewRequest(http.MethodDelete, 
"/users/"+b+"/privileges/privileges:grant", nil)
+       req = withTestCaller(req, a, models.PrivilegeGrant)
+       srv.ServeHTTP(rr, req)
+       if rr.Code != http.StatusNoContent {
+               t.Fatalf("revoke: got %d, want 204, body=%s", rr.Code, 
rr.Body.String())
        }
-       // invalidate b's cache the way the HTTP handler would.
-       srv.authCache.invalidate(b)
 
-       // b retries and now gets 403.
-       again := httptest.NewRecorder()
-       againReq := httptest.NewRequest(http.MethodGet, "/privileges/catalog", 
nil)
-       againReq.Header.Set(callerHeader, b)
-       srv.ServeHTTP(again, againReq)
-       if again.Code != http.StatusForbidden {
-               t.Errorf("post-revoke status for b: got %d, want 403", 
again.Code)
+       if has, err := svc.HasPrivilege(context.Background(), b, 
models.PrivilegeGrant); err != nil || has {
+               t.Errorf("HasPrivilege after revoke: has=%v err=%v", has, err)
        }
 }
diff --git a/internal/server/role.go b/internal/server/role.go
index e7a8fdbbe..e16c4ee7f 100644
--- a/internal/server/role.go
+++ b/internal/server/role.go
@@ -27,7 +27,7 @@ import (
 
 // @Summary    List all roles
 // @Tags       Roles
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Success    200     {array} models.Role
 // @Failure    401     {object}        object{error=string}
@@ -44,7 +44,7 @@ func (s *Server) listRoles(w http.ResponseWriter, r 
*http.Request) {
 
 // @Summary    Get a role with its privilege bundle
 // @Tags       Roles
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Role ID"
 // @Success    200     {object}        
object{role=models.Role,privileges=[]models.PrivilegeKey}
@@ -74,13 +74,13 @@ func (s *Server) getRole(w http.ResponseWriter, r 
*http.Request) {
 }
 
 type createRoleRequest struct {
-       Name        string `JSON:"name"`
-       Description string `JSON:"description"`
+       Name        string `json:"name"`
+       Description string `json:"description"`
 }
 
 // @Summary    Create a role
 // @Tags       Roles
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      request body    createRoleRequest       true    "Role payload"
@@ -90,9 +90,8 @@ type createRoleRequest struct {
 // @Failure    409     {object}        object{error=string}    "Role name 
collides"
 // @Router     /roles [post]
 func (s *Server) createRole(w http.ResponseWriter, r *http.Request) {
-       actorID := r.Header.Get(callerHeader)
-       if actorID == "" {
-               common.WriteError(w, http.StatusUnauthorized, 
errors.New("missing "+callerHeader+" header"))
+       caller := requireCaller(w, r)
+       if caller == nil {
                return
        }
        var req createRoleRequest
@@ -100,7 +99,7 @@ func (s *Server) createRole(w http.ResponseWriter, r 
*http.Request) {
                common.WriteError(w, http.StatusBadRequest, err)
                return
        }
-       role, err := s.svc.CreateRole(r.Context(), req.Name, req.Description, 
actorID)
+       role, err := s.svc.CreateRole(r.Context(), req.Name, req.Description, 
caller.UserID)
        if err != nil {
                common.WriteServiceError(w, err)
                return
@@ -109,14 +108,14 @@ func (s *Server) createRole(w http.ResponseWriter, r 
*http.Request) {
 }
 
 type updateRoleRequest struct {
-       Name        string `JSON:"name"`
-       Description string `JSON:"description"`
+       Name        string `json:"name"`
+       Description string `json:"description"`
 }
 
 // @Summary    Update role name / description
 // @Description        System roles cannot be renamed.
 // @Tags       Roles
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      id      path    string  true    "Role ID"
@@ -131,9 +130,8 @@ func (s *Server) updateRole(w http.ResponseWriter, r 
*http.Request) {
                common.WriteError(w, http.StatusBadRequest, errors.New("role id 
is required"))
                return
        }
-       actorID := r.Header.Get(callerHeader)
-       if actorID == "" {
-               common.WriteError(w, http.StatusUnauthorized, 
errors.New("missing "+callerHeader+" header"))
+       caller := requireCaller(w, r)
+       if caller == nil {
                return
        }
        var req updateRoleRequest
@@ -141,19 +139,19 @@ func (s *Server) updateRole(w http.ResponseWriter, r 
*http.Request) {
                common.WriteError(w, http.StatusBadRequest, err)
                return
        }
-       role, err := s.svc.UpdateRole(r.Context(), roleID, req.Name, 
req.Description, actorID)
+       role, err := s.svc.UpdateRole(r.Context(), roleID, req.Name, 
req.Description, caller.UserID)
        if err != nil {
                common.WriteServiceError(w, err)
                return
        }
-       s.authCache.invalidateAll()
+       s.svc.InvalidateAllIdentities()
        common.WriteJSON(w, http.StatusOK, role)
 }
 
 // @Summary    Delete a role
 // @Description        System roles cannot be deleted. CASCADE drops every 
assignment of this role.
 // @Tags       Roles
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Param      id      path    string  true    "Role ID"
 // @Success    204     "No Content"
 // @Failure    400     {object}        object{error=string}    "System role / 
unknown role"
@@ -165,27 +163,26 @@ func (s *Server) deleteRole(w http.ResponseWriter, r 
*http.Request) {
                common.WriteError(w, http.StatusBadRequest, errors.New("role id 
is required"))
                return
        }
-       actorID := r.Header.Get(callerHeader)
-       if actorID == "" {
-               common.WriteError(w, http.StatusUnauthorized, 
errors.New("missing "+callerHeader+" header"))
+       caller := requireCaller(w, r)
+       if caller == nil {
                return
        }
-       if err := s.svc.DeleteRole(r.Context(), roleID, actorID); err != nil {
+       if err := s.svc.DeleteRole(r.Context(), roleID, caller.UserID); err != 
nil {
                common.WriteServiceError(w, err)
                return
        }
-       s.authCache.invalidateAll()
+       s.svc.InvalidateAllIdentities()
        w.WriteHeader(http.StatusNoContent)
 }
 
 type rolePrivilegeRequest struct {
-       Privilege models.PrivilegeKey `JSON:"privilege"`
+       Privilege models.PrivilegeKey `json:"privilege"`
 }
 
 // @Summary    Add a privilege to a role
 // @Description        The new privilege propagates to every current holder.
 // @Tags       Roles
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Param      id      path    string  true    "Role ID"
 // @Param      request body    rolePrivilegeRequest    true    "Privilege key"
@@ -200,9 +197,8 @@ func (s *Server) addRolePrivilege(w http.ResponseWriter, r 
*http.Request) {
                common.WriteError(w, http.StatusBadRequest, errors.New("role id 
is required"))
                return
        }
-       actorID := r.Header.Get(callerHeader)
-       if actorID == "" {
-               common.WriteError(w, http.StatusUnauthorized, 
errors.New("missing "+callerHeader+" header"))
+       caller := requireCaller(w, r)
+       if caller == nil {
                return
        }
        var req rolePrivilegeRequest
@@ -210,18 +206,18 @@ func (s *Server) addRolePrivilege(w http.ResponseWriter, 
r *http.Request) {
                common.WriteError(w, http.StatusBadRequest, err)
                return
        }
-       if err := s.svc.AddPrivilegeToRole(r.Context(), roleID, req.Privilege, 
actorID); err != nil {
+       if err := s.svc.AddPrivilegeToRole(r.Context(), roleID, req.Privilege, 
caller.UserID); err != nil {
                common.WriteServiceError(w, err)
                return
        }
-       s.authCache.invalidateAll()
+       s.svc.InvalidateAllIdentities()
        w.WriteHeader(http.StatusNoContent)
 }
 
 // @Summary    Remove a privilege from a role
 // @Description        Removal propagates to every holder. Refuses to remove 
`privileges:grant` or `roles:manage` if that would leave no role anywhere 
carrying it.
 // @Tags       Roles
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Param      id      path    string  true    "Role ID"
 // @Param      key     path    models.PrivilegeKey     true    "Privilege key"
 // @Success    204     "No Content"
@@ -236,22 +232,21 @@ func (s *Server) removeRolePrivilege(w 
http.ResponseWriter, r *http.Request) {
                common.WriteError(w, http.StatusBadRequest, errors.New("role id 
and privilege key are required"))
                return
        }
-       actorID := r.Header.Get(callerHeader)
-       if actorID == "" {
-               common.WriteError(w, http.StatusUnauthorized, 
errors.New("missing "+callerHeader+" header"))
+       caller := requireCaller(w, r)
+       if caller == nil {
                return
        }
-       if err := s.svc.RemovePrivilegeFromRole(r.Context(), roleID, key, 
actorID); err != nil {
+       if err := s.svc.RemovePrivilegeFromRole(r.Context(), roleID, key, 
caller.UserID); err != nil {
                common.WriteServiceError(w, err)
                return
        }
-       s.authCache.invalidateAll()
+       s.svc.InvalidateAllIdentities()
        w.WriteHeader(http.StatusNoContent)
 }
 
 // @Summary    List roles a user holds
 // @Tags       Role Assignments
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "User ID"
 // @Success    200     {array} models.UserRole
@@ -273,7 +268,7 @@ func (s *Server) listUserRoles(w http.ResponseWriter, r 
*http.Request) {
 
 // @Summary    List users holding the role
 // @Tags       Roles
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Role ID"
 // @Success    200     {array} models.UserRole
@@ -294,13 +289,13 @@ func (s *Server) listRoleHolders(w http.ResponseWriter, r 
*http.Request) {
 }
 
 type grantRoleRequest struct {
-       RoleID string `JSON:"role_id"`
-       Reason string `JSON:"reason"`
+       RoleID string `json:"role_id"`
+       Reason string `json:"reason"`
 }
 
 // @Summary    Grant a role to a user
 // @Tags       Role Assignments
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      id      path    string  true    "User ID"
@@ -316,9 +311,8 @@ func (s *Server) grantRoleToUser(w http.ResponseWriter, r 
*http.Request) {
                common.WriteError(w, http.StatusBadRequest, errors.New("user id 
is required"))
                return
        }
-       granterID := r.Header.Get(callerHeader)
-       if granterID == "" {
-               common.WriteError(w, http.StatusUnauthorized, 
errors.New("missing "+callerHeader+" header"))
+       caller := requireCaller(w, r)
+       if caller == nil {
                return
        }
        var req grantRoleRequest
@@ -326,23 +320,23 @@ func (s *Server) grantRoleToUser(w http.ResponseWriter, r 
*http.Request) {
                common.WriteError(w, http.StatusBadRequest, err)
                return
        }
-       assignment, err := s.svc.GrantRoleToUser(r.Context(), userID, 
req.RoleID, granterID, req.Reason)
+       assignment, err := s.svc.GrantRoleToUser(r.Context(), userID, 
req.RoleID, caller.UserID, req.Reason)
        if err != nil {
                common.WriteServiceError(w, err)
                return
        }
-       s.authCache.invalidate(userID)
+       s.svc.InvalidateAllIdentities()
        common.WriteJSON(w, http.StatusCreated, assignment)
 }
 
 type revokeRoleRequest struct {
-       Reason string `JSON:"reason"`
+       Reason string `json:"reason"`
 }
 
 // @Summary    Revoke a role from a user
 // @Description        Refuses if revoking would leave no holder of 
`privileges:grant` or `roles:manage` anywhere (last-meta-holder guard).
 // @Tags       Role Assignments
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Param      id      path    string  true    "User ID"
 // @Param      roleId  path    string  true    "Role ID"
@@ -359,18 +353,16 @@ func (s *Server) revokeRoleFromUser(w 
http.ResponseWriter, r *http.Request) {
                common.WriteError(w, http.StatusBadRequest, errors.New("user id 
and role id are required"))
                return
        }
-       revokerID := r.Header.Get(callerHeader)
-       if revokerID == "" {
-               common.WriteError(w, http.StatusUnauthorized, 
errors.New("missing "+callerHeader+" header"))
+       caller := requireCaller(w, r)
+       if caller == nil {
                return
        }
        var req revokeRoleRequest
        _ = common.DecodeJSON(r, &req)
-       if err := s.svc.RevokeRoleFromUser(r.Context(), userID, roleID, 
revokerID, req.Reason); err != nil {
+       if err := s.svc.RevokeRoleFromUser(r.Context(), userID, roleID, 
caller.UserID, req.Reason); err != nil {
                common.WriteServiceError(w, err)
                return
        }
-       s.authCache.invalidate(userID)
-       s.authCache.invalidate(revokerID)
+       s.svc.InvalidateAllIdentities()
        w.WriteHeader(http.StatusNoContent)
 }
diff --git a/internal/server/server.go b/internal/server/server.go
index 72f24554c..a33bcef87 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -29,167 +29,174 @@ import (
        "github.com/apache/airavata-custos/internal/httputil"
        "github.com/apache/airavata-custos/internal/store"
        "github.com/apache/airavata-custos/pkg/common"
+       "github.com/apache/airavata-custos/pkg/identity"
        "github.com/apache/airavata-custos/pkg/models"
        "github.com/apache/airavata-custos/pkg/service"
 )
 
 // AdminDeps wires the /audit/* endpoints. A nil field makes its routes return 
503.
-// Connector endpoints (/connectors/{name}/...) are registered by the 
connector.
+// TODO(server): fold AuditTraces onto *Service and drop AdminDeps (see 
followups.md).
 type AdminDeps struct {
        AuditTraces store.AuditTraceStore
 }
 
 // Server is an HTTP handler that exposes the service API.
 type Server struct {
-       svc       *service.Service
-       mux       *http.ServeMux
-       authCache *authProfileCache
-       admin     *AdminDeps
+       svc    *service.Service
+       router *identity.Router
+       admin  *AdminDeps
 }
 
-// New builds an HTTP handler wired to the supplied service. admin may be nil
-// to disable the admin routes.
-func New(svc *service.Service, admin *AdminDeps) *Server {
-       s := &Server{
-               svc:       svc,
-               mux:       http.NewServeMux(),
-               authCache: newAuthProfileCache(authProfileTTL),
-               admin:     admin,
-       }
+// New builds an HTTP handler wired to the supplied service. The router owns 
the mux
+// and gates every authenticated route.
+func New(svc *service.Service, router *identity.Router, admin *AdminDeps) 
*Server {
+       s := &Server{svc: svc, router: router, admin: admin}
        s.routes()
        return s
 }
 
 // ServeHTTP satisfies http.Handler.
 func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-       s.mux.ServeHTTP(w, r)
+       s.router.ServeHTTP(w, r)
 }
 
-// Mux exposes the underlying mux so connectors can register their own routes.
-func (s *Server) Mux() *http.ServeMux { return s.mux }
+// requireCaller pulls the verified caller off the request context. Returns
+// nil and writes a 401 when the middleware did not attach one.
+func requireCaller(w http.ResponseWriter, r *http.Request) *identity.Caller {
+       c := identity.CallerFromContext(r.Context())
+       if c == nil {
+               common.WriteError(w, http.StatusUnauthorized, 
errors.New("missing authenticated caller"))
+               return nil
+       }
+       return c
+}
 
+// TODO(auth): tighten RequireAuth routes to RequirePrivilege as new keys land.
 func (s *Server) routes() {
-       s.mux.HandleFunc("GET /healthz", s.healthz)
-
-       s.mux.HandleFunc("POST /organizations", s.createOrganization)
-       s.mux.HandleFunc("GET /organizations/{id}", s.getOrganization)
-
-       s.mux.HandleFunc("POST /users", s.createUser)
-       s.mux.HandleFunc("GET /users/{id}", s.getUser)
-       s.mux.HandleFunc("PUT /users/{id}/status", s.updateUserStatus)
-       s.mux.HandleFunc("POST /users/merge", s.mergeUsers)
-
-       s.mux.HandleFunc("GET /projects", s.listProjects)
-       s.mux.HandleFunc("POST /projects", s.createProject)
-       s.mux.HandleFunc("GET /projects/{id}", s.getProject)
-       s.mux.HandleFunc("PUT /projects/{id}/status", s.updateProjectStatus)
-       s.mux.HandleFunc("GET /projects/{id}/members", s.listProjectMembers)
-
-       s.mux.HandleFunc("POST /compute-clusters", s.createComputeCluster)
-       s.mux.HandleFunc("GET /compute-clusters", s.listComputeClusters)
-       s.mux.HandleFunc("GET /compute-clusters/{id}", s.getComputeCluster)
-
-       s.mux.HandleFunc("POST /compute-cluster-users", 
s.createComputeClusterUser)
-       s.mux.HandleFunc("GET /compute-cluster-users/{id}", 
s.getComputeClusterUser)
-       s.mux.HandleFunc("PUT /compute-cluster-users/{id}", 
s.updateComputeClusterUser)
-       s.mux.HandleFunc("DELETE /compute-cluster-users/{id}", 
s.deleteComputeClusterUser)
-       s.mux.HandleFunc("GET /compute-clusters/{id}/users", 
s.listComputeClusterUsersByCluster)
-       s.mux.HandleFunc("GET /compute-clusters/{id}/users/{userId}", 
s.getComputeClusterUserByPair)
-       s.mux.HandleFunc("GET /users/{id}/compute-cluster-users", 
s.listComputeClusterUsersByUser)
-
-       s.mux.HandleFunc("GET /compute-allocations", s.listComputeAllocations)
-       s.mux.HandleFunc("POST /compute-allocations", s.createComputeAllocation)
-       s.mux.HandleFunc("GET /compute-allocations/{id}", 
s.getComputeAllocation)
-
-       s.mux.HandleFunc("POST /compute-allocation-resources", 
s.createComputeAllocationResource)
-       s.mux.HandleFunc("GET /compute-allocation-resources", 
s.listComputeAllocationResources)
-       s.mux.HandleFunc("GET /compute-allocation-resources/{id}", 
s.getComputeAllocationResource)
-
-       s.mux.HandleFunc("GET /compute-allocations/{id}/resources", 
s.listResourcesForAllocation)
-       s.mux.HandleFunc("POST /compute-allocations/{id}/resources", 
s.attachResourceToAllocation)
-       s.mux.HandleFunc("PUT 
/compute-allocations/{id}/resources/{resourceId}", 
s.updateAllocationResourceMapping)
-       s.mux.HandleFunc("DELETE 
/compute-allocations/{id}/resources/{resourceId}", 
s.detachResourceFromAllocation)
-       s.mux.HandleFunc("GET /compute-allocation-resources/{id}/allocations", 
s.listAllocationsForResource)
-
-       s.mux.HandleFunc("POST /compute-allocation-resource-rates", 
s.createComputeAllocationResourceRate)
-       s.mux.HandleFunc("GET /compute-allocation-resource-rates/{id}", 
s.getComputeAllocationResourceRate)
-       s.mux.HandleFunc("GET /compute-allocation-resources/{id}/rates", 
s.listRatesForResource)
-       s.mux.HandleFunc("GET 
/compute-allocation-resources/{id}/rates/effective", 
s.getEffectiveRateForResource)
-
-       s.mux.HandleFunc("POST /compute-allocation-diffs", 
s.createComputeAllocationDiff)
-       s.mux.HandleFunc("GET /compute-allocation-diffs/{id}", 
s.getComputeAllocationDiff)
-       s.mux.HandleFunc("DELETE /compute-allocation-diffs/{id}", 
s.deleteComputeAllocationDiff)
-       s.mux.HandleFunc("GET /compute-allocations/{id}/diffs", 
s.listDiffsForAllocation)
-       s.mux.HandleFunc("GET /compute-allocations/{id}/diffs/latest", 
s.getLatestDiffForAllocation)
-
-       s.mux.HandleFunc("GET /compute-allocation-change-requests", 
s.listChangeRequests)
-       s.mux.HandleFunc("POST /compute-allocation-change-requests", 
s.createComputeAllocationChangeRequest)
-       s.mux.HandleFunc("GET /compute-allocation-change-requests/{id}", 
s.getComputeAllocationChangeRequest)
-       s.mux.HandleFunc("PUT /compute-allocation-change-requests/{id}", 
s.updateComputeAllocationChangeRequest)
-       s.mux.HandleFunc("DELETE /compute-allocation-change-requests/{id}", 
s.deleteComputeAllocationChangeRequest)
-       s.mux.HandleFunc("GET /compute-allocations/{id}/change-requests", 
s.listChangeRequestsForAllocation)
-       s.mux.HandleFunc("GET /users/{id}/change-requests", 
s.listChangeRequestsByRequester)
-
-       s.mux.HandleFunc("POST /compute-allocation-change-request-events", 
s.createComputeAllocationChangeRequestEvent)
-       s.mux.HandleFunc("GET /compute-allocation-change-request-events/{id}", 
s.getComputeAllocationChangeRequestEvent)
-       s.mux.HandleFunc("DELETE 
/compute-allocation-change-request-events/{id}", 
s.deleteComputeAllocationChangeRequestEvent)
-       s.mux.HandleFunc("GET /compute-allocation-change-requests/{id}/events", 
s.listEventsForChangeRequest)
-       s.mux.HandleFunc("GET 
/compute-allocation-change-requests/{id}/events/latest", 
s.getLatestEventForChangeRequest)
-
-       s.mux.HandleFunc("POST /compute-allocation-memberships", 
s.createComputeAllocationMembership)
-       s.mux.HandleFunc("GET /compute-allocation-memberships/{id}", 
s.getComputeAllocationMembership)
-       s.mux.HandleFunc("PUT /compute-allocation-memberships/{id}", 
s.updateComputeAllocationMembership)
-       s.mux.HandleFunc("PUT /compute-allocation-memberships/{id}/status", 
s.updateMembershipStatus)
-       s.mux.HandleFunc("DELETE /compute-allocation-memberships/{id}", 
s.deleteComputeAllocationMembership)
-       s.mux.HandleFunc("GET /compute-allocations/{id}/memberships", 
s.listMembersForAllocation)
-       s.mux.HandleFunc("GET /users/{id}/compute-allocation-memberships", 
s.listAllocationsForUser)
-       s.mux.HandleFunc("GET 
/compute-allocation-memberships/{id}/resource-overrides", 
s.listOverridesForMembership)
-
-       s.mux.HandleFunc("POST 
/compute-allocation-membership-resource-overrides", 
s.createComputeAllocationMembershipResourceOverride)
-       s.mux.HandleFunc("GET 
/compute-allocation-membership-resource-overrides/{id}", 
s.getComputeAllocationMembershipResourceOverride)
-       s.mux.HandleFunc("PUT 
/compute-allocation-membership-resource-overrides/{id}", 
s.updateComputeAllocationMembershipResourceOverride)
-       s.mux.HandleFunc("DELETE 
/compute-allocation-membership-resource-overrides/{id}", 
s.deleteComputeAllocationMembershipResourceOverride)
-       s.mux.HandleFunc("GET 
/compute-allocation-resources/{id}/membership-overrides", 
s.listOverridesForResource)
-
-       s.mux.HandleFunc("POST /compute-allocation-usages", 
s.createComputeAllocationUsage)
-       s.mux.HandleFunc("GET /compute-allocation-usages/{id}", 
s.getComputeAllocationUsage)
-       s.mux.HandleFunc("DELETE /compute-allocation-usages/{id}", 
s.deleteComputeAllocationUsage)
-       s.mux.HandleFunc("GET /compute-allocations/{id}/usages", 
s.listUsagesForAllocation)
-       s.mux.HandleFunc("GET /compute-allocations/{id}/usages/total", 
s.getTotalSUUsageForAllocation)
-       s.mux.HandleFunc("GET 
/compute-allocations/{id}/users/{userId}/usages/total", 
s.getTotalSUUsageForUserInAllocation)
-       s.mux.HandleFunc("GET /users/{id}/compute-allocation-usages", 
s.listUsagesByUser)
-
-       s.mux.HandleFunc("POST /user-identities", s.createUserIdentity)
-       s.mux.HandleFunc("GET /user-identities/{id}", s.getUserIdentity)
-       s.mux.HandleFunc("PUT /user-identities/{id}", s.updateUserIdentity)
-       s.mux.HandleFunc("DELETE /user-identities/{id}", s.deleteUserIdentity)
-       s.mux.HandleFunc("GET 
/user-identities/sources/{source}/external/{externalId}", 
s.getUserIdentityBySourceAndExternalID)
-       s.mux.HandleFunc("GET /user-identities/oidc-subjects/{oidcSub}", 
s.getUserIdentityByOIDCSub)
-       s.mux.HandleFunc("GET /users/{id}/user-identities", 
s.listUserIdentitiesForUser)
-
-       s.mux.HandleFunc("GET /user/privileges", s.getCallerPrivileges)
-       s.mux.HandleFunc("GET /privileges/catalog", 
s.requirePrivilege(models.PrivilegeGrant, s.getPrivilegeCatalog))
-       s.mux.HandleFunc("GET /users/{id}/privileges", 
s.requirePrivilege(models.PrivilegeGrant, s.listUserPrivileges))
-       s.mux.HandleFunc("GET /privileges/{key}/holders", 
s.requirePrivilege(models.PrivilegeGrant, s.listPrivilegeHolders))
-       s.mux.HandleFunc("POST /users/{id}/privileges", 
s.requirePrivilege(models.PrivilegeGrant, s.grantPrivilege))
-       s.mux.HandleFunc("DELETE /users/{id}/privileges/{key}", 
s.requirePrivilege(models.PrivilegeGrant, s.revokePrivilege))
-
-       s.mux.HandleFunc("GET /roles", 
s.requirePrivilege(models.PrivilegeRolesManage, s.listRoles))
-       s.mux.HandleFunc("POST /roles", 
s.requirePrivilege(models.PrivilegeRolesManage, s.createRole))
-       s.mux.HandleFunc("GET /roles/{id}", 
s.requirePrivilege(models.PrivilegeRolesManage, s.getRole))
-       s.mux.HandleFunc("PUT /roles/{id}", 
s.requirePrivilege(models.PrivilegeRolesManage, s.updateRole))
-       s.mux.HandleFunc("DELETE /roles/{id}", 
s.requirePrivilege(models.PrivilegeRolesManage, s.deleteRole))
-       s.mux.HandleFunc("POST /roles/{id}/privileges", 
s.requirePrivilege(models.PrivilegeRolesManage, s.addRolePrivilege))
-       s.mux.HandleFunc("DELETE /roles/{id}/privileges/{key}", 
s.requirePrivilege(models.PrivilegeRolesManage, s.removeRolePrivilege))
-       s.mux.HandleFunc("GET /roles/{id}/holders", 
s.requirePrivilege(models.PrivilegeRolesManage, s.listRoleHolders))
-       s.mux.HandleFunc("GET /users/{id}/roles", 
s.requirePrivilege(models.PrivilegeRolesManage, s.listUserRoles))
-       s.mux.HandleFunc("POST /users/{id}/roles", 
s.requirePrivilege(models.PrivilegeRolesManage, s.grantRoleToUser))
-       s.mux.HandleFunc("DELETE /users/{id}/roles/{roleId}", 
s.requirePrivilege(models.PrivilegeRolesManage, s.revokeRoleFromUser))
-
-       s.mux.HandleFunc("GET /audit/traces", s.handleListTraces)
-       s.mux.HandleFunc("GET /audit/traces/{trace_id}", s.handleGetTrace)
-       s.mux.HandleFunc("GET /audit/events", s.handleListEvents)
-       s.mux.HandleFunc("GET /audit/sources", s.handleListSources)
+       s.router.Public("GET /healthz", s.healthz)
+       // TODO: /ready endpoint not implemented; add router.Public("GET 
/ready", ...) when it lands.
+
+       s.router.RequireAuth("POST /organizations", s.createOrganization)
+       s.router.RequireAuth("GET /organizations/{id}", s.getOrganization)
+
+       s.router.RequireAuth("POST /users", s.createUser)
+       s.router.RequireAuth("GET /users/{id}", s.getUser)
+       s.router.RequireAuth("PUT /users/{id}/status", s.updateUserStatus)
+       s.router.RequireAuth("POST /users/merge", s.mergeUsers)
+
+       s.router.RequireAuth("GET /projects", s.listProjects)
+       s.router.RequireAuth("POST /projects", s.createProject)
+       s.router.RequireAuth("GET /projects/{id}", s.getProject)
+       s.router.RequireAuth("PUT /projects/{id}/status", s.updateProjectStatus)
+       s.router.RequireAuth("GET /projects/{id}/members", s.listProjectMembers)
+
+       s.router.RequirePrivilege("POST /compute-clusters", 
models.PrivilegeHPCWrite, s.createComputeCluster)
+       s.router.RequirePrivilege("GET /compute-clusters", 
models.PrivilegeHPCRead, s.listComputeClusters)
+       s.router.RequirePrivilege("GET /compute-clusters/{id}", 
models.PrivilegeHPCRead, s.getComputeCluster)
+
+       s.router.RequireAuth("POST /compute-cluster-users", 
s.createComputeClusterUser)
+       s.router.RequireAuth("GET /compute-cluster-users/{id}", 
s.getComputeClusterUser)
+       s.router.RequireAuth("PUT /compute-cluster-users/{id}", 
s.updateComputeClusterUser)
+       s.router.RequireAuth("DELETE /compute-cluster-users/{id}", 
s.deleteComputeClusterUser)
+       s.router.RequireAuth("GET /compute-clusters/{id}/users", 
s.listComputeClusterUsersByCluster)
+       s.router.RequireAuth("GET /compute-clusters/{id}/users/{userId}", 
s.getComputeClusterUserByPair)
+       s.router.RequireAuth("GET /users/{id}/compute-cluster-users", 
s.listComputeClusterUsersByUser)
+
+       s.router.RequireAuth("GET /compute-allocations", 
s.listComputeAllocations)
+       s.router.RequireAuth("POST /compute-allocations", 
s.createComputeAllocation)
+       s.router.RequireAuth("GET /compute-allocations/{id}", 
s.getComputeAllocation)
+
+       s.router.RequireAuth("POST /compute-allocation-resources", 
s.createComputeAllocationResource)
+       s.router.RequireAuth("GET /compute-allocation-resources", 
s.listComputeAllocationResources)
+       s.router.RequireAuth("GET /compute-allocation-resources/{id}", 
s.getComputeAllocationResource)
+
+       s.router.RequireAuth("GET /compute-allocations/{id}/resources", 
s.listResourcesForAllocation)
+       s.router.RequireAuth("POST /compute-allocations/{id}/resources", 
s.attachResourceToAllocation)
+       s.router.RequireAuth("PUT 
/compute-allocations/{id}/resources/{resourceId}", 
s.updateAllocationResourceMapping)
+       s.router.RequireAuth("DELETE 
/compute-allocations/{id}/resources/{resourceId}", 
s.detachResourceFromAllocation)
+       s.router.RequireAuth("GET 
/compute-allocation-resources/{id}/allocations", s.listAllocationsForResource)
+
+       s.router.RequireAuth("POST /compute-allocation-resource-rates", 
s.createComputeAllocationResourceRate)
+       s.router.RequireAuth("GET /compute-allocation-resource-rates/{id}", 
s.getComputeAllocationResourceRate)
+       s.router.RequireAuth("GET /compute-allocation-resources/{id}/rates", 
s.listRatesForResource)
+       s.router.RequireAuth("GET 
/compute-allocation-resources/{id}/rates/effective", 
s.getEffectiveRateForResource)
+
+       s.router.RequireAuth("POST /compute-allocation-diffs", 
s.createComputeAllocationDiff)
+       s.router.RequireAuth("GET /compute-allocation-diffs/{id}", 
s.getComputeAllocationDiff)
+       s.router.RequireAuth("DELETE /compute-allocation-diffs/{id}", 
s.deleteComputeAllocationDiff)
+       s.router.RequireAuth("GET /compute-allocations/{id}/diffs", 
s.listDiffsForAllocation)
+       s.router.RequireAuth("GET /compute-allocations/{id}/diffs/latest", 
s.getLatestDiffForAllocation)
+
+       s.router.RequireAuth("GET /compute-allocation-change-requests", 
s.listChangeRequests)
+       s.router.RequireAuth("POST /compute-allocation-change-requests", 
s.createComputeAllocationChangeRequest)
+       s.router.RequireAuth("GET /compute-allocation-change-requests/{id}", 
s.getComputeAllocationChangeRequest)
+       s.router.RequireAuth("PUT /compute-allocation-change-requests/{id}", 
s.updateComputeAllocationChangeRequest)
+       s.router.RequireAuth("DELETE /compute-allocation-change-requests/{id}", 
s.deleteComputeAllocationChangeRequest)
+       s.router.RequireAuth("GET /compute-allocations/{id}/change-requests", 
s.listChangeRequestsForAllocation)
+       s.router.RequireAuth("GET /users/{id}/change-requests", 
s.listChangeRequestsByRequester)
+
+       s.router.RequireAuth("POST /compute-allocation-change-request-events", 
s.createComputeAllocationChangeRequestEvent)
+       s.router.RequireAuth("GET 
/compute-allocation-change-request-events/{id}", 
s.getComputeAllocationChangeRequestEvent)
+       s.router.RequireAuth("DELETE 
/compute-allocation-change-request-events/{id}", 
s.deleteComputeAllocationChangeRequestEvent)
+       s.router.RequireAuth("GET 
/compute-allocation-change-requests/{id}/events", s.listEventsForChangeRequest)
+       s.router.RequireAuth("GET 
/compute-allocation-change-requests/{id}/events/latest", 
s.getLatestEventForChangeRequest)
+
+       s.router.RequireAuth("POST /compute-allocation-memberships", 
s.createComputeAllocationMembership)
+       s.router.RequireAuth("GET /compute-allocation-memberships/{id}", 
s.getComputeAllocationMembership)
+       s.router.RequireAuth("PUT /compute-allocation-memberships/{id}", 
s.updateComputeAllocationMembership)
+       s.router.RequireAuth("PUT /compute-allocation-memberships/{id}/status", 
s.updateMembershipStatus)
+       s.router.RequireAuth("DELETE /compute-allocation-memberships/{id}", 
s.deleteComputeAllocationMembership)
+       s.router.RequireAuth("GET /compute-allocations/{id}/memberships", 
s.listMembersForAllocation)
+       s.router.RequireAuth("GET /users/{id}/compute-allocation-memberships", 
s.listAllocationsForUser)
+       s.router.RequireAuth("GET 
/compute-allocation-memberships/{id}/resource-overrides", 
s.listOverridesForMembership)
+
+       s.router.RequireAuth("POST 
/compute-allocation-membership-resource-overrides", 
s.createComputeAllocationMembershipResourceOverride)
+       s.router.RequireAuth("GET 
/compute-allocation-membership-resource-overrides/{id}", 
s.getComputeAllocationMembershipResourceOverride)
+       s.router.RequireAuth("PUT 
/compute-allocation-membership-resource-overrides/{id}", 
s.updateComputeAllocationMembershipResourceOverride)
+       s.router.RequireAuth("DELETE 
/compute-allocation-membership-resource-overrides/{id}", 
s.deleteComputeAllocationMembershipResourceOverride)
+       s.router.RequireAuth("GET 
/compute-allocation-resources/{id}/membership-overrides", 
s.listOverridesForResource)
+
+       s.router.RequireAuth("POST /compute-allocation-usages", 
s.createComputeAllocationUsage)
+       s.router.RequireAuth("GET /compute-allocation-usages/{id}", 
s.getComputeAllocationUsage)
+       s.router.RequireAuth("DELETE /compute-allocation-usages/{id}", 
s.deleteComputeAllocationUsage)
+       s.router.RequireAuth("GET /compute-allocations/{id}/usages", 
s.listUsagesForAllocation)
+       s.router.RequireAuth("GET /compute-allocations/{id}/usages/total", 
s.getTotalSUUsageForAllocation)
+       s.router.RequireAuth("GET 
/compute-allocations/{id}/users/{userId}/usages/total", 
s.getTotalSUUsageForUserInAllocation)
+       s.router.RequireAuth("GET /users/{id}/compute-allocation-usages", 
s.listUsagesByUser)
+
+       s.router.RequireAuth("POST /user-identities", s.createUserIdentity)
+       s.router.RequireAuth("GET /user-identities/{id}", s.getUserIdentity)
+       s.router.RequireAuth("PUT /user-identities/{id}", s.updateUserIdentity)
+       s.router.RequireAuth("DELETE /user-identities/{id}", 
s.deleteUserIdentity)
+       s.router.RequireAuth("GET 
/user-identities/sources/{source}/external/{externalId}", 
s.getUserIdentityBySourceAndExternalID)
+       s.router.RequireAuth("GET /user-identities/oidc-subjects/{oidcSub}", 
s.getUserIdentityByOIDCSub)
+       s.router.RequireAuth("GET /users/{id}/user-identities", 
s.listUserIdentitiesForUser)
+
+       // Any authenticated caller may read their own effective privilege set 
with no privilege check.
+       s.router.RequireAuth("GET /user/privileges", s.getCallerPrivileges)
+       s.router.RequirePrivilege("GET /privileges/catalog", 
models.PrivilegeGrant, s.getPrivilegeCatalog)
+       s.router.RequirePrivilege("GET /users/{id}/privileges", 
models.PrivilegeGrant, s.listUserPrivileges)
+       s.router.RequirePrivilege("GET /privileges/{key}/holders", 
models.PrivilegeGrant, s.listPrivilegeHolders)
+       s.router.RequirePrivilege("POST /users/{id}/privileges", 
models.PrivilegeGrant, s.grantPrivilege)
+       s.router.RequirePrivilege("DELETE /users/{id}/privileges/{key}", 
models.PrivilegeGrant, s.revokePrivilege)
+
+       s.router.RequirePrivilege("GET /roles", models.PrivilegeRolesManage, 
s.listRoles)
+       s.router.RequirePrivilege("POST /roles", models.PrivilegeRolesManage, 
s.createRole)
+       s.router.RequirePrivilege("GET /roles/{id}", 
models.PrivilegeRolesManage, s.getRole)
+       s.router.RequirePrivilege("PUT /roles/{id}", 
models.PrivilegeRolesManage, s.updateRole)
+       s.router.RequirePrivilege("DELETE /roles/{id}", 
models.PrivilegeRolesManage, s.deleteRole)
+       s.router.RequirePrivilege("POST /roles/{id}/privileges", 
models.PrivilegeRolesManage, s.addRolePrivilege)
+       s.router.RequirePrivilege("DELETE /roles/{id}/privileges/{key}", 
models.PrivilegeRolesManage, s.removeRolePrivilege)
+       s.router.RequirePrivilege("GET /roles/{id}/holders", 
models.PrivilegeRolesManage, s.listRoleHolders)
+       s.router.RequirePrivilege("GET /users/{id}/roles", 
models.PrivilegeRolesManage, s.listUserRoles)
+       s.router.RequirePrivilege("POST /users/{id}/roles", 
models.PrivilegeRolesManage, s.grantRoleToUser)
+       s.router.RequirePrivilege("DELETE /users/{id}/roles/{roleId}", 
models.PrivilegeRolesManage, s.revokeRoleFromUser)
+
+       // TODO(auth): introduce audit-read and switch to 
router.RequirePrivilege.
+       s.router.RequireAuth("GET /audit/traces", s.handleListTraces)
+       s.router.RequireAuth("GET /audit/traces/{trace_id}", s.handleGetTrace)
+       s.router.RequireAuth("GET /audit/events", s.handleListEvents)
+       s.router.RequireAuth("GET /audit/sources", s.handleListSources)
 }
 
 func (s *Server) healthz(w http.ResponseWriter, _ *http.Request) {
@@ -198,7 +205,7 @@ func (s *Server) healthz(w http.ResponseWriter, _ 
*http.Request) {
 
 // @Summary    Create an organization
 // @Tags       Organizations
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      request body    models.Organization     true    "Organization 
payload"
@@ -221,7 +228,7 @@ func (s *Server) createOrganization(w http.ResponseWriter, 
r *http.Request) {
 
 // @Summary    Get an organization by ID
 // @Tags       Organizations
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Organization ID"
 // @Success    200     {object}        models.Organization
@@ -238,7 +245,7 @@ func (s *Server) getOrganization(w http.ResponseWriter, r 
*http.Request) {
 
 // @Summary    Create a user
 // @Tags       Users
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      request body    models.User     true    "User payload"
@@ -261,7 +268,7 @@ func (s *Server) createUser(w http.ResponseWriter, r 
*http.Request) {
 
 // @Summary    Get a user by ID
 // @Tags       Users
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "User ID"
 // @Success    200     {object}        models.User
@@ -278,7 +285,7 @@ func (s *Server) getUser(w http.ResponseWriter, r 
*http.Request) {
 
 // @Summary    Create a project
 // @Tags       Projects
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      request body    models.Project  true    "Project payload"
@@ -301,7 +308,7 @@ func (s *Server) createProject(w http.ResponseWriter, r 
*http.Request) {
 
 // @Summary    Get a project by ID
 // @Tags       Projects
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Project ID"
 // @Success    200     {object}        ProjectResponse
@@ -329,7 +336,7 @@ func projectResponseFrom(p *store.ProjectWithPI) 
ProjectResponse {
 
 // @Summary    Create a compute cluster
 // @Tags       Compute Clusters
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      request body    models.ComputeCluster   true    "Cluster 
payload"
@@ -352,7 +359,7 @@ func (s *Server) createComputeCluster(w 
http.ResponseWriter, r *http.Request) {
 
 // @Summary    Get a compute cluster by ID
 // @Tags       Compute Clusters
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Compute cluster ID"
 // @Success    200     {object}        models.ComputeCluster
@@ -369,7 +376,7 @@ func (s *Server) getComputeCluster(w http.ResponseWriter, r 
*http.Request) {
 
 // @Summary    List compute clusters
 // @Tags       Compute Clusters
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Success    200     {array} models.ComputeCluster
 // @Failure    500     {object}        object{error=string}
@@ -385,7 +392,7 @@ func (s *Server) listComputeClusters(w http.ResponseWriter, 
r *http.Request) {
 
 // @Summary    Create a compute cluster user
 // @Tags       Compute Cluster Users
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      request body    models.ComputeClusterUser       true    
"Cluster user payload"
@@ -408,7 +415,7 @@ func (s *Server) createComputeClusterUser(w 
http.ResponseWriter, r *http.Request
 
 // @Summary    Get a compute cluster user by ID
 // @Tags       Compute Cluster Users
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Compute cluster user ID"
 // @Success    200     {object}        models.ComputeClusterUser
@@ -425,7 +432,7 @@ func (s *Server) getComputeClusterUser(w 
http.ResponseWriter, r *http.Request) {
 
 // @Summary    Update a compute cluster user
 // @Tags       Compute Cluster Users
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      id      path    string  true    "Compute cluster user ID"
@@ -450,7 +457,7 @@ func (s *Server) updateComputeClusterUser(w 
http.ResponseWriter, r *http.Request
 
 // @Summary    Delete a compute cluster user
 // @Tags       Compute Cluster Users
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Param      id      path    string  true    "Compute cluster user ID"
 // @Success    204     "No Content"
 // @Failure    404     {object}        object{error=string}
@@ -465,7 +472,7 @@ func (s *Server) deleteComputeClusterUser(w 
http.ResponseWriter, r *http.Request
 
 // @Summary    List users on a compute cluster
 // @Tags       Compute Cluster Users
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Compute cluster ID"
 // @Success    200     {array} models.ComputeClusterUser
@@ -482,7 +489,7 @@ func (s *Server) listComputeClusterUsersByCluster(w 
http.ResponseWriter, r *http
 
 // @Summary    Get a compute cluster user by (cluster, user) pair
 // @Tags       Compute Cluster Users
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Compute cluster ID"
 // @Param      userId  path    string  true    "User ID"
@@ -500,7 +507,7 @@ func (s *Server) getComputeClusterUserByPair(w 
http.ResponseWriter, r *http.Requ
 
 // @Summary    List compute cluster users for a user
 // @Tags       Compute Cluster Users
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "User ID"
 // @Success    200     {array} models.ComputeClusterUser
@@ -517,7 +524,7 @@ func (s *Server) listComputeClusterUsersByUser(w 
http.ResponseWriter, r *http.Re
 
 // @Summary    Create a compute allocation
 // @Tags       Compute Allocations
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      request body    models.ComputeAllocation        true    
"Compute allocation payload"
@@ -540,7 +547,7 @@ func (s *Server) createComputeAllocation(w 
http.ResponseWriter, r *http.Request)
 
 // @Summary    Get a compute allocation by ID
 // @Tags       Compute Allocations
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Compute allocation ID"
 // @Success    200     {object}        models.ComputeAllocation
@@ -558,7 +565,7 @@ func (s *Server) getComputeAllocation(w 
http.ResponseWriter, r *http.Request) {
 // @Summary    Create a compute allocation resource
 // @Description        Defines a resource (partition) — e.g. a CPU or GPU 
partition that allocations can be attached to.
 // @Tags       Compute Allocation Resources
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      request body    models.ComputeAllocationResource        true    
"Resource payload"
@@ -582,7 +589,7 @@ func (s *Server) createComputeAllocationResource(w 
http.ResponseWriter, r *http.
 
 // @Summary    Get a compute allocation resource
 // @Tags       Compute Allocation Resources
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Resource ID"
 // @Success    200     {object}        models.ComputeAllocationResource
@@ -599,7 +606,7 @@ func (s *Server) getComputeAllocationResource(w 
http.ResponseWriter, r *http.Req
 
 // @Summary    List all compute allocation resources
 // @Tags       Compute Allocation Resources
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Success    200     {array} models.ComputeAllocationResource
 // @Failure    401     {object}        object{error=string}
@@ -622,7 +629,7 @@ type attachResourceRequest struct {
 // @Summary    Attach a resource to a compute allocation
 // @Description        Creates a mapping between a compute allocation and a 
resource (partition) with a specific amount and wall-clock time grant.
 // @Tags       Compute Allocation Resources
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      id      path    string  true    "Compute allocation ID"
@@ -652,7 +659,7 @@ type updateAllocationResourceMappingRequest struct {
 
 // @Summary    Update a compute allocation -> resource mapping
 // @Tags       Compute Allocation Resources
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      id      path    string  true    "Compute allocation ID"
@@ -678,7 +685,7 @@ func (s *Server) updateAllocationResourceMapping(w 
http.ResponseWriter, r *http.
 
 // @Summary    Detach a resource from a compute allocation
 // @Tags       Compute Allocation Resources
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Param      id      path    string  true    "Compute allocation ID"
 // @Param      resourceId      path    string  true    "Compute allocation 
resource ID"
 // @Success    204     "No Content"
@@ -694,7 +701,7 @@ func (s *Server) detachResourceFromAllocation(w 
http.ResponseWriter, r *http.Req
 
 // @Summary    List resources attached to a compute allocation
 // @Tags       Compute Allocation Resources
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Compute allocation ID"
 // @Success    200     {array} models.ComputeAllocationResourceMapping
@@ -711,7 +718,7 @@ func (s *Server) listResourcesForAllocation(w 
http.ResponseWriter, r *http.Reque
 
 // @Summary    List compute allocations attached to a resource
 // @Tags       Compute Allocation Resources
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Compute allocation resource ID"
 // @Success    200     {array} models.ComputeAllocationResourceMapping
@@ -729,7 +736,7 @@ func (s *Server) listAllocationsForResource(w 
http.ResponseWriter, r *http.Reque
 // @Summary    Create a compute allocation resource rate
 // @Description        Records the SU rate (e.g. SU per CPU-hour) for a 
resource over a time window.
 // @Tags       Compute Allocation Resource Rates
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      request body    models.ComputeAllocationResourceRate    true    
"Rate payload"
@@ -753,7 +760,7 @@ func (s *Server) createComputeAllocationResourceRate(w 
http.ResponseWriter, r *h
 
 // @Summary    Get a compute allocation resource rate
 // @Tags       Compute Allocation Resource Rates
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Rate ID"
 // @Success    200     {object}        models.ComputeAllocationResourceRate
@@ -770,7 +777,7 @@ func (s *Server) getComputeAllocationResourceRate(w 
http.ResponseWriter, r *http
 
 // @Summary    List rate history for a resource
 // @Tags       Compute Allocation Resource Rates
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Compute allocation resource ID"
 // @Success    200     {array} models.ComputeAllocationResourceRate
@@ -788,7 +795,7 @@ func (s *Server) listRatesForResource(w 
http.ResponseWriter, r *http.Request) {
 // @Summary    Get the effective rate for a resource at a given time
 // @Description        Returns the rate whose `[start_time, end_time)` window 
contains the `at` timestamp. Defaults to now when `at` is omitted.
 // @Tags       Compute Allocation Resource Rates
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Compute allocation resource ID"
 // @Param      at      query   string  false   "RFC3339 time; defaults to now"
@@ -817,7 +824,7 @@ func (s *Server) getEffectiveRateForResource(w 
http.ResponseWriter, r *http.Requ
 // @Summary    Create a compute allocation diff
 // @Description        Records a discrete change against a compute allocation 
(e.g. USAGE_UPDATE or ALLOCATION_STATUS_CHANGE).
 // @Tags       Compute Allocation Diffs
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      request body    models.ComputeAllocationDiff    true    "Diff 
payload"
@@ -841,7 +848,7 @@ func (s *Server) createComputeAllocationDiff(w 
http.ResponseWriter, r *http.Requ
 
 // @Summary    Get a compute allocation diff
 // @Tags       Compute Allocation Diffs
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Diff ID"
 // @Success    200     {object}        models.ComputeAllocationDiff
@@ -858,7 +865,7 @@ func (s *Server) getComputeAllocationDiff(w 
http.ResponseWriter, r *http.Request
 
 // @Summary    Delete a compute allocation diff
 // @Tags       Compute Allocation Diffs
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Param      id      path    string  true    "Diff ID"
 // @Success    204     "No Content"
 // @Failure    404     {object}        object{error=string}
@@ -873,7 +880,7 @@ func (s *Server) deleteComputeAllocationDiff(w 
http.ResponseWriter, r *http.Requ
 
 // @Summary    List diffs for a compute allocation
 // @Tags       Compute Allocation Diffs
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Compute allocation ID"
 // @Success    200     {array} models.ComputeAllocationDiff
@@ -890,7 +897,7 @@ func (s *Server) listDiffsForAllocation(w 
http.ResponseWriter, r *http.Request)
 
 // @Summary    Get the most recent diff for a compute allocation
 // @Tags       Compute Allocation Diffs
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Compute allocation ID"
 // @Success    200     {object}        models.ComputeAllocationDiff
@@ -907,7 +914,7 @@ func (s *Server) getLatestDiffForAllocation(w 
http.ResponseWriter, r *http.Reque
 
 // @Summary    Create a compute allocation change request
 // @Tags       Compute Allocation Change Requests
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      request body    models.ComputeAllocationChangeRequest   true    
"Change request payload"
@@ -930,7 +937,7 @@ func (s *Server) createComputeAllocationChangeRequest(w 
http.ResponseWriter, r *
 
 // @Summary    Get a compute allocation change request
 // @Tags       Compute Allocation Change Requests
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Change request ID"
 // @Success    200     {object}        models.ComputeAllocationChangeRequest
@@ -947,7 +954,7 @@ func (s *Server) getComputeAllocationChangeRequest(w 
http.ResponseWriter, r *htt
 
 // @Summary    Update a compute allocation change request
 // @Tags       Compute Allocation Change Requests
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      id      path    string  true    "Change request ID"
@@ -973,7 +980,7 @@ func (s *Server) updateComputeAllocationChangeRequest(w 
http.ResponseWriter, r *
 
 // @Summary    Delete a compute allocation change request
 // @Tags       Compute Allocation Change Requests
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Param      id      path    string  true    "Change request ID"
 // @Success    204     "No Content"
 // @Failure    404     {object}        object{error=string}
@@ -988,7 +995,7 @@ func (s *Server) deleteComputeAllocationChangeRequest(w 
http.ResponseWriter, r *
 
 // @Summary    List change requests for a compute allocation
 // @Tags       Compute Allocation Change Requests
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Compute allocation ID"
 // @Success    200     {array} models.ComputeAllocationChangeRequest
@@ -1005,7 +1012,7 @@ func (s *Server) listChangeRequestsForAllocation(w 
http.ResponseWriter, r *http.
 
 // @Summary    List change requests submitted by a user
 // @Tags       Compute Allocation Change Requests
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "User ID"
 // @Success    200     {array} models.ComputeAllocationChangeRequest
@@ -1022,7 +1029,7 @@ func (s *Server) listChangeRequestsByRequester(w 
http.ResponseWriter, r *http.Re
 
 // @Summary    Create a change request event
 // @Tags       Change Request Events
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      request body    models.ComputeAllocationChangeRequestEvent      
true    "Event payload"
@@ -1045,7 +1052,7 @@ func (s *Server) 
createComputeAllocationChangeRequestEvent(w http.ResponseWriter
 
 // @Summary    Get a change request event
 // @Tags       Change Request Events
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Event ID"
 // @Success    200     {object}        
models.ComputeAllocationChangeRequestEvent
@@ -1062,7 +1069,7 @@ func (s *Server) getComputeAllocationChangeRequestEvent(w 
http.ResponseWriter, r
 
 // @Summary    Delete a change request event
 // @Tags       Change Request Events
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Param      id      path    string  true    "Event ID"
 // @Success    204     "No Content"
 // @Failure    404     {object}        object{error=string}
@@ -1077,7 +1084,7 @@ func (s *Server) 
deleteComputeAllocationChangeRequestEvent(w http.ResponseWriter
 
 // @Summary    List events for a change request
 // @Tags       Change Request Events
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Change request ID"
 // @Success    200     {array} models.ComputeAllocationChangeRequestEvent
@@ -1094,7 +1101,7 @@ func (s *Server) listEventsForChangeRequest(w 
http.ResponseWriter, r *http.Reque
 
 // @Summary    Get the most recent event for a change request
 // @Tags       Change Request Events
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Change request ID"
 // @Success    200     {object}        
models.ComputeAllocationChangeRequestEvent
@@ -1111,7 +1118,7 @@ func (s *Server) getLatestEventForChangeRequest(w 
http.ResponseWriter, r *http.R
 
 // @Summary    Create a compute allocation membership
 // @Tags       Compute Allocation Memberships
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      request body    models.ComputeAllocationMembership      true    
"Membership payload"
@@ -1134,7 +1141,7 @@ func (s *Server) createComputeAllocationMembership(w 
http.ResponseWriter, r *htt
 
 // @Summary    Get a compute allocation membership
 // @Tags       Compute Allocation Memberships
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Membership ID"
 // @Success    200     {object}        models.ComputeAllocationMembership
@@ -1151,7 +1158,7 @@ func (s *Server) getComputeAllocationMembership(w 
http.ResponseWriter, r *http.R
 
 // @Summary    Update a compute allocation membership
 // @Tags       Compute Allocation Memberships
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      id      path    string  true    "Membership ID"
@@ -1177,7 +1184,7 @@ func (s *Server) updateComputeAllocationMembership(w 
http.ResponseWriter, r *htt
 
 // @Summary    Update a membership's status
 // @Tags       Compute Allocation Memberships
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      id      path    string  true    "Membership ID"
@@ -1204,7 +1211,7 @@ func (s *Server) updateMembershipStatus(w 
http.ResponseWriter, r *http.Request)
 
 // @Summary    Delete a compute allocation membership
 // @Tags       Compute Allocation Memberships
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Param      id      path    string  true    "Membership ID"
 // @Success    204     "No Content"
 // @Failure    404     {object}        object{error=string}
@@ -1219,7 +1226,7 @@ func (s *Server) deleteComputeAllocationMembership(w 
http.ResponseWriter, r *htt
 
 // @Summary    List members of a compute allocation
 // @Tags       Compute Allocation Memberships
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Compute allocation ID"
 // @Success    200     {array} AllocationMembershipResponse
@@ -1245,7 +1252,7 @@ func (s *Server) listMembersForAllocation(w 
http.ResponseWriter, r *http.Request
 
 // @Summary    List a user's compute allocation memberships
 // @Tags       Compute Allocation Memberships
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "User ID"
 // @Success    200     {array} models.ComputeAllocationMembership
@@ -1262,7 +1269,7 @@ func (s *Server) listAllocationsForUser(w 
http.ResponseWriter, r *http.Request)
 
 // @Summary    Create a compute allocation usage record
 // @Tags       Compute Allocation Usages
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      request body    models.ComputeAllocationUsage   true    "Usage 
payload"
@@ -1285,7 +1292,7 @@ func (s *Server) createComputeAllocationUsage(w 
http.ResponseWriter, r *http.Req
 
 // @Summary    Get a compute allocation usage record
 // @Tags       Compute Allocation Usages
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Usage ID"
 // @Success    200     {object}        models.ComputeAllocationUsage
@@ -1302,7 +1309,7 @@ func (s *Server) getComputeAllocationUsage(w 
http.ResponseWriter, r *http.Reques
 
 // @Summary    Delete a compute allocation usage record
 // @Tags       Compute Allocation Usages
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Param      id      path    string  true    "Usage ID"
 // @Success    204     "No Content"
 // @Failure    404     {object}        object{error=string}
@@ -1317,7 +1324,7 @@ func (s *Server) deleteComputeAllocationUsage(w 
http.ResponseWriter, r *http.Req
 
 // @Summary    List usages for a compute allocation
 // @Tags       Compute Allocation Usages
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Compute allocation ID"
 // @Success    200     {array} models.ComputeAllocationUsage
@@ -1334,7 +1341,7 @@ func (s *Server) listUsagesForAllocation(w 
http.ResponseWriter, r *http.Request)
 
 // @Summary    List usages submitted by a user
 // @Tags       Compute Allocation Usages
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "User ID"
 // @Success    200     {array} models.ComputeAllocationUsage
@@ -1351,7 +1358,7 @@ func (s *Server) listUsagesByUser(w http.ResponseWriter, 
r *http.Request) {
 
 // @Summary    Create a membership resource override
 // @Tags       Membership Resource Overrides
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      request body    
models.ComputeAllocationMembershipResourceOverride      true    "Override 
payload"
@@ -1374,7 +1381,7 @@ func (s *Server) 
createComputeAllocationMembershipResourceOverride(w http.Respon
 
 // @Summary    Get a membership resource override
 // @Tags       Membership Resource Overrides
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Override ID"
 // @Success    200     {object}        
models.ComputeAllocationMembershipResourceOverride
@@ -1391,7 +1398,7 @@ func (s *Server) 
getComputeAllocationMembershipResourceOverride(w http.ResponseW
 
 // @Summary    Update a membership resource override
 // @Tags       Membership Resource Overrides
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      id      path    string  true    "Override ID"
@@ -1417,7 +1424,7 @@ func (s *Server) 
updateComputeAllocationMembershipResourceOverride(w http.Respon
 
 // @Summary    Delete a membership resource override
 // @Tags       Membership Resource Overrides
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Param      id      path    string  true    "Override ID"
 // @Success    204     "No Content"
 // @Failure    404     {object}        object{error=string}
@@ -1432,7 +1439,7 @@ func (s *Server) 
deleteComputeAllocationMembershipResourceOverride(w http.Respon
 
 // @Summary    List resource overrides for a membership
 // @Tags       Membership Resource Overrides
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Membership ID"
 // @Success    200     {array} 
models.ComputeAllocationMembershipResourceOverride
@@ -1449,7 +1456,7 @@ func (s *Server) listOverridesForMembership(w 
http.ResponseWriter, r *http.Reque
 
 // @Summary    List membership overrides referencing a resource
 // @Tags       Membership Resource Overrides
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Compute allocation resource ID"
 // @Success    200     {array} 
models.ComputeAllocationMembershipResourceOverride
@@ -1466,7 +1473,7 @@ func (s *Server) listOverridesForResource(w 
http.ResponseWriter, r *http.Request
 
 // @Summary    Get total SU usage for a compute allocation
 // @Tags       Compute Allocation Usages
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Compute allocation ID"
 // @Success    200     {object}        AllocationSUTotalResponse
@@ -1486,7 +1493,7 @@ func (s *Server) getTotalSUUsageForAllocation(w 
http.ResponseWriter, r *http.Req
 
 // @Summary    Get total SU usage for a user within a compute allocation
 // @Tags       Compute Allocation Usages
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Compute allocation ID"
 // @Param      userId  path    string  true    "User ID"
@@ -1514,7 +1521,7 @@ type statusUpdateRequest struct {
 
 // @Summary    Update a user's status
 // @Tags       Users
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      id      path    string  true    "User ID"
@@ -1539,7 +1546,7 @@ func (s *Server) updateUserStatus(w http.ResponseWriter, 
r *http.Request) {
 
 // @Summary    Update a project's status
 // @Tags       Projects
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      id      path    string  true    "Project ID"
@@ -1564,7 +1571,7 @@ func (s *Server) updateProjectStatus(w 
http.ResponseWriter, r *http.Request) {
 
 // @Summary    Create a user identity
 // @Tags       User Identities
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      request body    models.UserIdentity     true    "Identity 
payload"
@@ -1587,7 +1594,7 @@ func (s *Server) createUserIdentity(w 
http.ResponseWriter, r *http.Request) {
 
 // @Summary    Get a user identity
 // @Tags       User Identities
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Identity ID"
 // @Success    200     {object}        models.UserIdentity
@@ -1604,7 +1611,7 @@ func (s *Server) getUserIdentity(w http.ResponseWriter, r 
*http.Request) {
 
 // @Summary    Get a user identity by source and external ID
 // @Tags       User Identities
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      source  path    string  true    "Identity source"
 // @Param      externalId      path    string  true    "Identity's external ID 
at that source"
@@ -1622,7 +1629,7 @@ func (s *Server) getUserIdentityBySourceAndExternalID(w 
http.ResponseWriter, r *
 
 // @Summary    Get a user identity by its OIDC subject claim
 // @Tags       User Identities
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      oidcSub path    string  true    "OIDC subject claim"
 // @Success    200     {object}        models.UserIdentity
@@ -1639,7 +1646,7 @@ func (s *Server) getUserIdentityByOIDCSub(w 
http.ResponseWriter, r *http.Request
 
 // @Summary    List a user's identities
 // @Tags       User Identities
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "User ID"
 // @Success    200     {array} models.UserIdentity
@@ -1656,7 +1663,7 @@ func (s *Server) listUserIdentitiesForUser(w 
http.ResponseWriter, r *http.Reques
 
 // @Summary    Update a user identity
 // @Tags       User Identities
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      id      path    string  true    "Identity ID"
@@ -1681,7 +1688,7 @@ func (s *Server) updateUserIdentity(w 
http.ResponseWriter, r *http.Request) {
 
 // @Summary    Delete a user identity
 // @Tags       User Identities
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Param      id      path    string  true    "Identity ID"
 // @Success    204     "No Content"
 // @Failure    404     {object}        object{error=string}
@@ -1702,7 +1709,7 @@ type mergeUsersRequest struct {
 // @Summary    Merge two users
 // @Description        Merges the retiring user into the surviving user; the 
surviving record is returned.
 // @Tags       Users
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Accept     json
 // @Produce    json
 // @Param      request body    mergeUsersRequest       true    "Merge payload"
@@ -1748,7 +1755,7 @@ func LoggingMiddleware(next http.Handler) http.Handler {
 
 // @Summary    List projects (filtered + paginated, PI joined)
 // @Tags       Projects
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      pi_id   query   string  false   "Filter by PI user ID"
 // @Param      status  query   string  false   "Filter by status"
@@ -1780,7 +1787,7 @@ func (s *Server) listProjects(w http.ResponseWriter, r 
*http.Request) {
 
 // @Summary    List project members (one row per distinct user, with 
allocations)
 // @Tags       Projects
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      id      path    string  true    "Project ID"
 // @Success    200     {array} ProjectMemberResponse
@@ -1836,7 +1843,7 @@ func (s *Server) listProjectMembers(w 
http.ResponseWriter, r *http.Request) {
 
 // @Summary    List compute allocations (filtered + paginated)
 // @Tags       Compute Allocations
-// @Security   CustosUserHeader
+// @Security   BearerAuth
 // @Produce    json
 // @Param      project_id      query   string  false   "Filter by project ID"
 // @Param      status  query   string  false   "Filter by status"
diff --git a/internal/server/server_trace_test.go 
b/internal/server/server_trace_test.go
index 2d8f98576..fe50ce8e4 100644
--- a/internal/server/server_trace_test.go
+++ b/internal/server/server_trace_test.go
@@ -27,6 +27,7 @@ import (
        "go.opentelemetry.io/otel/sdk/trace/tracetest"
 
        "github.com/apache/airavata-custos/internal/tracing"
+       "github.com/apache/airavata-custos/pkg/identity"
 )
 
 // TestLoggingWrapsTracingProducesTraceIdHeader verifies the cmd/server stack
@@ -39,7 +40,8 @@ func TestLoggingWrapsTracingProducesTraceIdHeader(t 
*testing.T) {
        otel.SetTracerProvider(tp)
        t.Cleanup(func() { otel.SetTracerProvider(prev) })
 
-       handler := LoggingMiddleware(tracing.Middleware(New(nil, nil)))
+       router := identity.NewRouter(http.NewServeMux())
+       handler := LoggingMiddleware(tracing.Middleware(New(nil, router, nil)))
 
        req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
        rec := httptest.NewRecorder()
diff --git a/internal/server/testidentity_test.go 
b/internal/server/testidentity_test.go
new file mode 100644
index 000000000..4e98db01e
--- /dev/null
+++ b/internal/server/testidentity_test.go
@@ -0,0 +1,33 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package server
+
+import (
+       "net/http"
+
+       "github.com/apache/airavata-custos/pkg/identity"
+       "github.com/apache/airavata-custos/pkg/models"
+)
+
+// withTestCaller installs a *identity.Caller plus a privilege set onto the
+// request context for handler tests that run without the auth middleware.
+func withTestCaller(r *http.Request, userID string, privs 
...models.PrivilegeKey) *http.Request {
+       ctx := identity.WithCaller(r.Context(), &identity.Caller{UserID: 
userID})
+       ctx = identity.WithPrivilegesForTest(ctx, privs)
+       return r.WithContext(ctx)
+}
diff --git a/pkg/identity/identity.go b/pkg/identity/identity.go
index d50bfbef3..3122527ed 100644
--- a/pkg/identity/identity.go
+++ b/pkg/identity/identity.go
@@ -83,6 +83,20 @@ func HasPrivilege(ctx context.Context, privilege 
models.PrivilegeKey) bool {
        return ok
 }
 
+// PrivilegesFromContext returns the caller's effective privilege set. Returns
+// nil for both the absent and the empty-set cases.
+func PrivilegesFromContext(ctx context.Context) []models.PrivilegeKey {
+       set, _ := ctx.Value(privilegesKey).(privilegeSet)
+       if len(set) == 0 {
+               return nil
+       }
+       keys := make([]models.PrivilegeKey, 0, len(set))
+       for k := range set {
+               keys = append(keys, k)
+       }
+       return keys
+}
+
 // WithPrivilegesForTest is the test-only entry point to seed privileges on 
ctx.
 func WithPrivilegesForTest(ctx context.Context, keys []models.PrivilegeKey) 
context.Context {
        return withPrivileges(ctx, keys)


Reply via email to