Pulse/internal/api/access_admin_handlers.go
2026-03-18 16:06:30 +00:00

120 lines
3.5 KiB
Go

package api
import (
"encoding/json"
"net/http"
"strings"
)
// HandleRBACIntegrityCheck returns RBAC data integrity status for an org.
// GET /api/admin/rbac/integrity?org_id=default
func (h *RBACHandlers) HandleRBACIntegrityCheck(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
if h.rbacProvider == nil {
writeErrorResponse(w, http.StatusNotImplemented, "rbac_unavailable", "RBAC management is not available", nil)
return
}
contextOrgID := strings.TrimSpace(GetOrgID(r.Context()))
if contextOrgID == "" {
contextOrgID = "default"
}
orgID := strings.TrimSpace(r.URL.Query().Get("org_id"))
if orgID == "" {
orgID = contextOrgID
} else {
if !isValidOrganizationID(orgID) {
writeErrorResponse(w, http.StatusBadRequest, "invalid_org_id", "Invalid organization ID", nil)
return
}
if orgID != contextOrgID {
writeErrorResponse(w, http.StatusForbidden, "access_denied", "Token is not authorized for this organization", nil)
return
}
}
result := VerifyRBACIntegrity(h.rbacProvider, orgID)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(result)
}
// HandleRBACAdminReset performs break-glass admin role reset.
// POST /api/admin/rbac/reset-admin
// Requires a valid recovery token for security.
func (h *RBACHandlers) HandleRBACAdminReset(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
if h.rbacProvider == nil {
writeErrorResponse(w, http.StatusNotImplemented, "rbac_unavailable", "RBAC management is not available", nil)
return
}
// Limit request body size.
r.Body = http.MaxBytesReader(w, r.Body, 4*1024) // 4KB max
var req struct {
OrgID string `json:"org_id"`
Username string `json:"username"`
RecoveryToken string `json:"recovery_token"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErrorResponse(w, http.StatusBadRequest, "invalid_json", "Invalid request body", nil)
return
}
if req.Username == "" {
writeErrorResponse(w, http.StatusBadRequest, "missing_username", "Username is required", nil)
return
}
if req.RecoveryToken == "" {
writeErrorResponse(w, http.StatusBadRequest, "missing_token", "Recovery token is required", nil)
return
}
contextOrgID := strings.TrimSpace(GetOrgID(r.Context()))
if contextOrgID == "" {
contextOrgID = "default"
}
req.OrgID = strings.TrimSpace(req.OrgID)
if req.OrgID == "" {
req.OrgID = contextOrgID
} else {
if !isValidOrganizationID(req.OrgID) {
writeErrorResponse(w, http.StatusBadRequest, "invalid_org_id", "Invalid organization ID", nil)
return
}
if req.OrgID != contextOrgID {
writeErrorResponse(w, http.StatusForbidden, "access_denied", "Token is not authorized for this organization", nil)
return
}
}
store := GetRecoveryTokenStore()
if !store.ValidateRecoveryTokenConstantTime(req.RecoveryToken, GetClientIP(r)) {
writeErrorResponse(w, http.StatusForbidden, "invalid_token", "Invalid or expired recovery token", nil)
return
}
if err := ResetAdminRole(h.rbacProvider, req.OrgID, req.Username); err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "reset_failed", "Failed to reset admin role", nil)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{
"status": "ok",
"org_id": req.OrgID,
"username": req.Username,
"message": "Admin role reset successfully",
})
}