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
