You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by la...@apache.org on 2026/06/17 20:30:07 UTC
(airavata-custos) 07/15: Read caller identity from context, not the user-id header
This is an automated email from the ASF dual-hosted git repository.
lahirujayathilake pushed a commit to branch auth-endpoints
in repository https://gitbox.apache.org/repos/asf/airavata-custos.git
commit 8535531c9209ceea997f72d6f20bb95db34d39cb
Author: lahiruj <la...@gmail.com>
AuthorDate: Tue Jun 16 19:25:42 2026 -0400
Read caller identity from context, not the user-id header
Co-Authored-By: Claude Opus 4.7 <no...@anthropic.com>
---
internal/server/auth.go | 24 +++++++++++++++---------
internal/server/privilege.go | 21 +++++++++------------
internal/server/role.go | 43 ++++++++++++++++++-------------------------
3 files changed, 42 insertions(+), 46 deletions(-)
diff --git a/internal/server/auth.go b/internal/server/auth.go
index b6aaf2a0d..44f51f855 100644
--- a/internal/server/auth.go
+++ b/internal/server/auth.go
@@ -24,14 +24,21 @@ import (
"sync"
"time"
+ "github.com/apache/airavata-custos/pkg/identity"
"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"
+// callerOrUnauthorized returns the verified caller's user ID, writing a 401
+// response if the request is unauthenticated. Returns "" on rejection;
+// the caller must return immediately when it sees an empty string.
+func callerOrUnauthorized(w http.ResponseWriter, r *http.Request) string {
+ caller := identity.CallerFromContext(r.Context())
+ if caller == nil {
+ writeError(w, http.StatusUnauthorized, errors.New("unauthenticated"))
+ return ""
+ }
+ return caller.UserID
+}
// authProfileTTL bounds how long the middleware will trust a cached
// privilege set before re-reading the DB.
@@ -110,19 +117,18 @@ func (c *authProfileCache) invalidateAll() {
}
// requirePrivilege returns a middleware that admits the request only if the
-// caller (identified by callerHeader) holds the named active privilege.
+// verified caller holds the named active privilege.
//
// Responses:
-// - 401 Unauthorized - no caller header
+// - 401 Unauthorized - caller not on context (middleware should have set it)
// - 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)
+ callerID := callerOrUnauthorized(w, r)
if callerID == "" {
- writeError(w, http.StatusUnauthorized, errors.New("missing "+callerHeader+" header"))
return
}
profile, err := s.lookupAuthProfile(r.Context(), callerID)
diff --git a/internal/server/privilege.go b/internal/server/privilege.go
index e33b1acb1..1f8839dc6 100644
--- a/internal/server/privilege.go
+++ b/internal/server/privilege.go
@@ -27,16 +27,15 @@ import (
// @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 401 {object} object{error=string} "Unauthenticated"
// @Failure 503 {object} object{error=string} "Auth lookup failed"
// @Router /user/privileges [get]
func (s *Server) getCallerPrivileges(w http.ResponseWriter, r *http.Request) {
- callerID := r.Header.Get(callerHeader)
+ callerID := callerOrUnauthorized(w, r)
if callerID == "" {
- writeError(w, http.StatusUnauthorized, errors.New("missing "+callerHeader+" header"))
return
}
profile, err := s.lookupAuthProfile(r.Context(), callerID)
@@ -117,14 +116,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} "Unauthenticated"
// @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]
@@ -134,9 +133,8 @@ func (s *Server) grantPrivilege(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, errors.New("user id is required"))
return
}
- granterID := r.Header.Get(callerHeader)
+ granterID := callerOrUnauthorized(w, r)
if granterID == "" {
- writeError(w, http.StatusUnauthorized, errors.New("missing "+callerHeader+" header"))
return
}
var req grantPrivilegeRequest
@@ -160,14 +158,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} "Unauthenticated"
// @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]
@@ -178,9 +176,8 @@ func (s *Server) revokePrivilege(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, errors.New("user id and privilege key are required"))
return
}
- revokerID := r.Header.Get(callerHeader)
+ revokerID := callerOrUnauthorized(w, r)
if revokerID == "" {
- writeError(w, http.StatusUnauthorized, errors.New("missing "+callerHeader+" header"))
return
}
var req revokePrivilegeRequest
diff --git a/internal/server/role.go b/internal/server/role.go
index ee91ea7b7..c7a2afd68 100644
--- a/internal/server/role.go
+++ b/internal/server/role.go
@@ -26,7 +26,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}
@@ -43,7 +43,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}
@@ -79,7 +79,7 @@ type createRoleRequest struct {
// @Summary Create a role
// @Tags Roles
-// @Security CustosUserHeader
+// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body createRoleRequest true "Role payload"
@@ -89,9 +89,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)
+ actorID := callerOrUnauthorized(w, r)
if actorID == "" {
- writeError(w, http.StatusUnauthorized, errors.New("missing "+callerHeader+" header"))
return
}
var req createRoleRequest
@@ -115,7 +114,7 @@ type updateRoleRequest struct {
// @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"
@@ -130,9 +129,8 @@ func (s *Server) updateRole(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, errors.New("role id is required"))
return
}
- actorID := r.Header.Get(callerHeader)
+ actorID := callerOrUnauthorized(w, r)
if actorID == "" {
- writeError(w, http.StatusUnauthorized, errors.New("missing "+callerHeader+" header"))
return
}
var req updateRoleRequest
@@ -152,7 +150,7 @@ func (s *Server) updateRole(w http.ResponseWriter, r *http.Request) {
// @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"
@@ -164,9 +162,8 @@ func (s *Server) deleteRole(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, errors.New("role id is required"))
return
}
- actorID := r.Header.Get(callerHeader)
+ actorID := callerOrUnauthorized(w, r)
if actorID == "" {
- writeError(w, http.StatusUnauthorized, errors.New("missing "+callerHeader+" header"))
return
}
if err := s.svc.DeleteRole(r.Context(), roleID, actorID); err != nil {
@@ -184,7 +181,7 @@ type rolePrivilegeRequest struct {
// @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"
@@ -199,9 +196,8 @@ func (s *Server) addRolePrivilege(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, errors.New("role id is required"))
return
}
- actorID := r.Header.Get(callerHeader)
+ actorID := callerOrUnauthorized(w, r)
if actorID == "" {
- writeError(w, http.StatusUnauthorized, errors.New("missing "+callerHeader+" header"))
return
}
var req rolePrivilegeRequest
@@ -220,7 +216,7 @@ func (s *Server) addRolePrivilege(w http.ResponseWriter, r *http.Request) {
// @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"
@@ -235,9 +231,8 @@ func (s *Server) removeRolePrivilege(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, errors.New("role id and privilege key are required"))
return
}
- actorID := r.Header.Get(callerHeader)
+ actorID := callerOrUnauthorized(w, r)
if actorID == "" {
- writeError(w, http.StatusUnauthorized, errors.New("missing "+callerHeader+" header"))
return
}
if err := s.svc.RemovePrivilegeFromRole(r.Context(), roleID, key, actorID); err != nil {
@@ -250,7 +245,7 @@ func (s *Server) removeRolePrivilege(w http.ResponseWriter, r *http.Request) {
// @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
@@ -272,7 +267,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
@@ -299,7 +294,7 @@ type grantRoleRequest struct {
// @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"
@@ -315,9 +310,8 @@ func (s *Server) grantRoleToUser(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, errors.New("user id is required"))
return
}
- granterID := r.Header.Get(callerHeader)
+ granterID := callerOrUnauthorized(w, r)
if granterID == "" {
- writeError(w, http.StatusUnauthorized, errors.New("missing "+callerHeader+" header"))
return
}
var req grantRoleRequest
@@ -341,7 +335,7 @@ type revokeRoleRequest struct {
// @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"
@@ -358,9 +352,8 @@ func (s *Server) revokeRoleFromUser(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, errors.New("user id and role id are required"))
return
}
- revokerID := r.Header.Get(callerHeader)
+ revokerID := callerOrUnauthorized(w, r)
if revokerID == "" {
- writeError(w, http.StatusUnauthorized, errors.New("missing "+callerHeader+" header"))
return
}
var req revokeRoleRequest