blob: 7ecf25f71fe9a80d5a4f5ae3e80ddc1679998a5f [file]
// 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 (
"errors"
"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 BearerAuth
// @Produce json
// @Success 200 {object} object{privileges=[]models.PrivilegeKey} "Effective privilege set"
// @Failure 401 {object} object{error=string} "Missing authenticated caller"
// @Router /user/privileges [get]
func (s *Server) getCallerPrivileges(w http.ResponseWriter, r *http.Request) {
if requireCaller(w, r) == nil {
return
}
keys := identity.PrivilegesFromContext(r.Context())
if keys == nil {
keys = []models.PrivilegeKey{}
}
common.WriteJSON(w, http.StatusOK, map[string]any{"privileges": keys})
}
// CallerProfileResponse bundles the caller's user row with the effective
// privilege set.
type CallerProfileResponse struct {
User *models.User `json:"user"`
Privileges []models.PrivilegeKey `json:"privileges"`
}
// @Summary Get the caller's user record and effective privileges
// @Tags Caller
// @Security BearerAuth
// @Produce json
// @Success 200 {object} CallerProfileResponse
// @Failure 401 {object} object{error=string} "Missing authenticated caller"
// @Router /me [get]
func (s *Server) getCallerProfile(w http.ResponseWriter, r *http.Request) {
caller := requireCaller(w, r)
if caller == nil {
return
}
user, err := s.svc.GetUser(r.Context(), caller.UserID)
if err != nil {
common.WriteServiceError(w, err)
return
}
keys := identity.PrivilegesFromContext(r.Context())
if keys == nil {
keys = []models.PrivilegeKey{}
}
common.WriteJSON(w, http.StatusOK, CallerProfileResponse{User: user, Privileges: keys})
}
// @Summary List the declared privilege catalog
// @Tags Privileges
// @Security BearerAuth
// @Produce json
// @Success 200 {array} models.PrivilegeKey
// @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) {
common.WriteJSON(w, http.StatusOK, s.svc.PrivilegeCatalog())
}
// @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 BearerAuth
// @Produce json
// @Param id path string true "User ID"
// @Success 200 {array} models.UserPrivilege
// @Failure 400 {object} object{error=string}
// @Failure 403 {object} object{error=string} "Caller lacks privileges:grant"
// @Router /users/{id}/privileges [get]
func (s *Server) listUserPrivileges(w http.ResponseWriter, r *http.Request) {
userID := r.PathValue("id")
if userID == "" {
common.WriteError(w, http.StatusBadRequest, errors.New("user id is required"))
return
}
rows, err := s.svc.ListUserPrivileges(r.Context(), userID)
if err != nil {
common.WriteServiceError(w, err)
return
}
common.WriteJSON(w, http.StatusOK, rows)
}
// @Summary List direct holders of a privilege
// @Description Role-derived holders are NOT listed here.
// @Tags Privileges
// @Security BearerAuth
// @Produce json
// @Param key path models.PrivilegeKey true "Privilege key"
// @Success 200 {array} models.UserPrivilege
// @Failure 400 {object} object{error=string} "Unknown privilege key"
// @Router /privileges/{key}/holders [get]
func (s *Server) listPrivilegeHolders(w http.ResponseWriter, r *http.Request) {
key := models.PrivilegeKey(r.PathValue("key"))
if !models.IsKnownPrivilege(key) {
common.WriteError(w, http.StatusBadRequest, errors.New("unknown privilege key"))
return
}
rows, err := s.svc.ListPrivilegeHolders(r.Context(), key)
if err != nil {
common.WriteServiceError(w, err)
return
}
common.WriteJSON(w, http.StatusOK, rows)
}
type grantPrivilegeRequest struct {
Privilege models.PrivilegeKey `json:"privilege"`
Reason string `json:"reason"`
}
// @Summary Grant a direct privilege to a user
// @Tags Privileges
// @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 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]
func (s *Server) grantPrivilege(w http.ResponseWriter, r *http.Request) {
userID := r.PathValue("id")
if userID == "" {
common.WriteError(w, http.StatusBadRequest, errors.New("user id is required"))
return
}
caller := requireCaller(w, r)
if caller == nil {
return
}
var req grantPrivilegeRequest
if err := common.DecodeJSON(r, &req); err != nil {
common.WriteError(w, http.StatusBadRequest, err)
return
}
grant, err := s.svc.GrantPrivilege(r.Context(), userID, req.Privilege, caller.UserID, req.Reason)
if err != nil {
common.WriteServiceError(w, err)
return
}
s.svc.InvalidateAllIdentities()
common.WriteJSON(w, http.StatusCreated, grant)
}
type revokePrivilegeRequest struct {
Reason string `json:"reason"`
}
// @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 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 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]
func (s *Server) revokePrivilege(w http.ResponseWriter, r *http.Request) {
userID := r.PathValue("id")
key := models.PrivilegeKey(r.PathValue("key"))
if userID == "" || key == "" {
common.WriteError(w, http.StatusBadRequest, errors.New("user id and privilege key are required"))
return
}
caller := requireCaller(w, r)
if caller == nil {
return
}
var req revokePrivilegeRequest
_ = common.DecodeJSON(r, &req)
if err := s.svc.RevokePrivilege(r.Context(), userID, key, caller.UserID, req.Reason); err != nil {
common.WriteServiceError(w, err)
return
}
s.svc.InvalidateAllIdentities()
w.WriteHeader(http.StatusNoContent)
}