Why: System, role, and project entry permissions need explicit actions instead of a single administrator flag or unrestricted project creation. Evidence: The full Backend test suite passed, including role APIs, action isolation, project.create denial/allow, storage migration coverage, and account write fences. Impact: Adds versioned role and grant endpoints, maps account actions to explicit permissions, and requires project.create authority. Refs: Focused role and permission system task 3
182 lines
6.0 KiB
Go
182 lines
6.0 KiB
Go
package server
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
authzsvc "agent-system-backend/internal/authz"
|
|
"agent-system-backend/internal/store"
|
|
)
|
|
|
|
func (s *Server) requireSystemAction(w http.ResponseWriter, r *http.Request, actionID string) (string, bool) {
|
|
actor := s.owner(r)
|
|
if s.Auth == nil || !s.Auth.Enabled {
|
|
return actor, true
|
|
}
|
|
decision := s.authorizer.AuthorizeSystem(actor, actionID)
|
|
if !decision.Allowed {
|
|
http.Error(w, "system action denied: "+decision.Reason, http.StatusForbidden)
|
|
return "", false
|
|
}
|
|
return actor, true
|
|
}
|
|
|
|
func (s *Server) listPermissionCatalog(w http.ResponseWriter, r *http.Request) {
|
|
if _, ok := s.requireSystemAction(w, r, "system.permissions.view"); !ok {
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"permissions": authzsvc.Catalog()})
|
|
}
|
|
|
|
func (s *Server) listRoles(w http.ResponseWriter, r *http.Request) {
|
|
if _, ok := s.requireSystemAction(w, r, "system.roles.view"); !ok {
|
|
return
|
|
}
|
|
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
|
roles, next, err := s.st.ListRoles(r.URL.Query().Get("query"), r.URL.Query().Get("mode"),
|
|
r.URL.Query().Get("status"), r.URL.Query().Get("action"), r.URL.Query().Get("cursor"), limit)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"roles": roles, "nextCursor": next})
|
|
}
|
|
|
|
func (s *Server) createRole(w http.ResponseWriter, r *http.Request) {
|
|
actor, ok := s.requireSystemAction(w, r, "system.roles.manage")
|
|
if !ok {
|
|
return
|
|
}
|
|
var req struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
}
|
|
if err := decodeStrictJSON(r, &req); err != nil {
|
|
http.Error(w, "invalid role", http.StatusBadRequest)
|
|
return
|
|
}
|
|
role, err := s.st.CreateRole(actor, store.RoleCreate{Name: req.Name, Description: req.Description})
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, store.RoleAdminView{RoleRecord: role, Permissions: []store.RolePermissionRecord{}})
|
|
}
|
|
|
|
func (s *Server) getRole(w http.ResponseWriter, r *http.Request) {
|
|
if _, ok := s.requireSystemAction(w, r, "system.roles.view"); !ok {
|
|
return
|
|
}
|
|
role, err := s.st.RoleAdmin(r.PathValue("roleId"))
|
|
writeJSONOr(w, role, err)
|
|
}
|
|
|
|
func (s *Server) patchRole(w http.ResponseWriter, r *http.Request) {
|
|
actor, ok := s.requireSystemAction(w, r, "system.roles.manage")
|
|
if !ok {
|
|
return
|
|
}
|
|
var req struct {
|
|
ExpectedRoleVersion int64 `json:"expectedRoleVersion"`
|
|
Name *string `json:"name"`
|
|
Description *string `json:"description"`
|
|
Enabled *bool `json:"enabled"`
|
|
}
|
|
if err := decodeStrictJSON(r, &req); err != nil || req.ExpectedRoleVersion < 1 {
|
|
http.Error(w, "invalid role patch", http.StatusBadRequest)
|
|
return
|
|
}
|
|
role, err := s.st.PatchRole(actor, r.PathValue("roleId"), req.ExpectedRoleVersion,
|
|
store.RolePatch{Name: req.Name, Description: req.Description, Enabled: req.Enabled})
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
view, err := s.st.RoleAdmin(role.RoleID)
|
|
writeJSONOr(w, view, err)
|
|
}
|
|
|
|
func (s *Server) replaceRolePermissions(w http.ResponseWriter, r *http.Request) {
|
|
actor, ok := s.requireSystemAction(w, r, "system.permissions.manage")
|
|
if !ok {
|
|
return
|
|
}
|
|
var req struct {
|
|
ExpectedRoleVersion int64 `json:"expectedRoleVersion"`
|
|
IdempotencyKey string `json:"idempotencyKey"`
|
|
Changes []store.RolePermissionChange `json:"changes"`
|
|
}
|
|
if err := decodeStrictJSON(r, &req); err != nil || req.ExpectedRoleVersion < 1 || req.IdempotencyKey == "" {
|
|
http.Error(w, "invalid role permission batch", http.StatusBadRequest)
|
|
return
|
|
}
|
|
receipt, err := s.st.ReplaceRolePermissions(actor, r.PathValue("roleId"), req.ExpectedRoleVersion, req.Changes, req.IdempotencyKey)
|
|
writeJSONOr(w, receipt, err)
|
|
}
|
|
|
|
func (s *Server) getUserRole(w http.ResponseWriter, r *http.Request) {
|
|
if _, ok := s.requireSystemAction(w, r, "system.roles.view"); !ok {
|
|
return
|
|
}
|
|
view, err := s.st.RoleForUser(r.PathValue("userId"))
|
|
writeJSONOr(w, view, err)
|
|
}
|
|
|
|
func (s *Server) assignUserRole(w http.ResponseWriter, r *http.Request) {
|
|
actor, ok := s.requireSystemAction(w, r, "system.roles.assign")
|
|
if !ok {
|
|
return
|
|
}
|
|
var req struct {
|
|
RoleID string `json:"roleId"`
|
|
ExpectedStateVersion int64 `json:"expectedStateVersion"`
|
|
IdempotencyKey string `json:"idempotencyKey"`
|
|
}
|
|
if err := decodeStrictJSON(r, &req); err != nil || req.RoleID == "" || req.ExpectedStateVersion < 1 || req.IdempotencyKey == "" {
|
|
http.Error(w, "invalid role assignment", http.StatusBadRequest)
|
|
return
|
|
}
|
|
binding, err := s.st.AssignUserRole(actor, r.PathValue("userId"), req.RoleID, req.ExpectedStateVersion, req.IdempotencyKey)
|
|
writeJSONOr(w, binding, err)
|
|
}
|
|
|
|
func (s *Server) getSystemGrants(w http.ResponseWriter, r *http.Request) {
|
|
if _, ok := s.requireSystemAction(w, r, "system.permissions.view"); !ok {
|
|
return
|
|
}
|
|
actions, epoch, err := s.st.SystemGrantsForUser(r.PathValue("userId"))
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"actions": actions, "authorityEpoch": epoch})
|
|
}
|
|
|
|
func (s *Server) replaceSystemGrants(w http.ResponseWriter, r *http.Request) {
|
|
actor, ok := s.requireSystemAction(w, r, "system.permissions.manage")
|
|
if !ok {
|
|
return
|
|
}
|
|
var req struct {
|
|
ExpectedAuthorityEpoch int64 `json:"expectedAuthorityEpoch"`
|
|
IdempotencyKey string `json:"idempotencyKey"`
|
|
Actions []string `json:"actions"`
|
|
}
|
|
if err := decodeStrictJSON(r, &req); err != nil || req.ExpectedAuthorityEpoch < 1 || req.IdempotencyKey == "" {
|
|
http.Error(w, "invalid system grant batch", http.StatusBadRequest)
|
|
return
|
|
}
|
|
receipt, err := s.st.ReplaceSystemGrants(actor, r.PathValue("userId"), req.ExpectedAuthorityEpoch, req.Actions, req.IdempotencyKey)
|
|
writeJSONOr(w, receipt, err)
|
|
}
|
|
|
|
func (s *Server) explainSystemAuthorization(w http.ResponseWriter, r *http.Request) {
|
|
actionID := r.URL.Query().Get("action")
|
|
decision := s.authorizer.AuthorizeSystem(s.owner(r), actionID)
|
|
if actionID == "" {
|
|
http.Error(w, "action required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, decision)
|
|
}
|