Files
Agent-System-Backend/internal/server/roles.go
T
weichen 4d1ca60067 Expose role and system permission APIs
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
2026-10-14 17:02:11 +00:00

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)
}