Add granular role grant/revoke endpoints to REST and gRPC APIs
- Add POST /v1/accounts/{id}/roles and DELETE /v1/accounts/{id}/roles/{role} REST endpoints
- Add GrantRole and RevokeRole RPCs to AccountService in gRPC API
- Update OpenAPI specification with new endpoints
- Add grant and revoke subcommands to mciasctl
- Add grant and revoke subcommands to mciasgrpcctl
- Regenerate proto files with new message types and RPCs
- Implement gRPC server methods for granular role management
- All existing tests pass; build verified with goimports
Security: Role changes are audited via EventRoleGranted and EventRoleRevoked events,
consistent with existing SetRoles implementation.
This commit is contained in:
@@ -130,6 +130,8 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.Handle("DELETE /v1/accounts/{id}", requireAdmin(http.HandlerFunc(s.handleDeleteAccount)))
|
||||
mux.Handle("GET /v1/accounts/{id}/roles", requireAdmin(http.HandlerFunc(s.handleGetRoles)))
|
||||
mux.Handle("PUT /v1/accounts/{id}/roles", requireAdmin(http.HandlerFunc(s.handleSetRoles)))
|
||||
mux.Handle("POST /v1/accounts/{id}/roles", requireAdmin(http.HandlerFunc(s.handleGrantRole)))
|
||||
mux.Handle("DELETE /v1/accounts/{id}/roles/{role}", requireAdmin(http.HandlerFunc(s.handleRevokeRole)))
|
||||
mux.Handle("GET /v1/accounts/{id}/pgcreds", requireAdmin(http.HandlerFunc(s.handleGetPGCreds)))
|
||||
mux.Handle("PUT /v1/accounts/{id}/pgcreds", requireAdmin(http.HandlerFunc(s.handleSetPGCreds)))
|
||||
mux.Handle("GET /v1/audit", requireAdmin(http.HandlerFunc(s.handleListAudit)))
|
||||
@@ -666,6 +668,10 @@ type setRolesRequest struct {
|
||||
Roles []string `json:"roles"`
|
||||
}
|
||||
|
||||
type grantRoleRequest struct {
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
func (s *Server) handleGetRoles(w http.ResponseWriter, r *http.Request) {
|
||||
acct, ok := s.loadAccount(w, r)
|
||||
if !ok {
|
||||
@@ -710,6 +716,68 @@ func (s *Server) handleSetRoles(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (s *Server) handleGrantRole(w http.ResponseWriter, r *http.Request) {
|
||||
acct, ok := s.loadAccount(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var req grantRoleRequest
|
||||
if !decodeJSON(w, r, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Role == "" {
|
||||
middleware.WriteError(w, http.StatusBadRequest, "role is required", "bad_request")
|
||||
return
|
||||
}
|
||||
|
||||
actor := middleware.ClaimsFromContext(r.Context())
|
||||
var grantedBy *int64
|
||||
if actor != nil {
|
||||
if a, err := s.db.GetAccountByUUID(actor.Subject); err == nil {
|
||||
grantedBy = &a.ID
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.db.GrantRole(acct.ID, req.Role, grantedBy); err != nil {
|
||||
middleware.WriteError(w, http.StatusBadRequest, "invalid role", "bad_request")
|
||||
return
|
||||
}
|
||||
|
||||
s.writeAudit(r, model.EventRoleGranted, grantedBy, &acct.ID, fmt.Sprintf(`{"role":"%s"}`, req.Role))
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (s *Server) handleRevokeRole(w http.ResponseWriter, r *http.Request) {
|
||||
acct, ok := s.loadAccount(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
role := r.PathValue("role")
|
||||
if role == "" {
|
||||
middleware.WriteError(w, http.StatusBadRequest, "role is required", "bad_request")
|
||||
return
|
||||
}
|
||||
|
||||
actor := middleware.ClaimsFromContext(r.Context())
|
||||
var revokedBy *int64
|
||||
if actor != nil {
|
||||
if a, err := s.db.GetAccountByUUID(actor.Subject); err == nil {
|
||||
revokedBy = &a.ID
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.db.RevokeRole(acct.ID, role); err != nil {
|
||||
middleware.WriteError(w, http.StatusInternalServerError, "internal error", "internal_error")
|
||||
return
|
||||
}
|
||||
|
||||
s.writeAudit(r, model.EventRoleRevoked, revokedBy, &acct.ID, fmt.Sprintf(`{"role":"%s"}`, role))
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ---- TOTP endpoints ----
|
||||
|
||||
type totpEnrollResponse struct {
|
||||
|
||||
Reference in New Issue
Block a user