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 49beee1507b75baff1fd0938db3019db026ab995
Author: lahiruj <[email protected]>
AuthorDate: Tue Jun 16 19:25:42 2026 -0400

    Read caller identity from context, not the user-id header
---
 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

Reply via email to