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)