A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
549 lines
15 KiB
Go
549 lines
15 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// RBACHandlers handles RBAC-related API endpoints
|
|
type RBACHandlers struct {
|
|
store *rbac.Store
|
|
service *rbac.Service
|
|
policyEngine *rbac.PolicyEngine
|
|
}
|
|
|
|
// NewRBACHandlers creates new RBAC handlers
|
|
func NewRBACHandlers(store *rbac.Store, service *rbac.Service, policyEngine *rbac.PolicyEngine) *RBACHandlers {
|
|
return &RBACHandlers{
|
|
store: store,
|
|
service: service,
|
|
policyEngine: policyEngine,
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Tenant Endpoints
|
|
// ============================================================================
|
|
|
|
// ListTenants returns all tenants
|
|
func (h *RBACHandlers) ListTenants(c *gin.Context) {
|
|
tenants, err := h.store.ListTenants(c.Request.Context())
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"tenants": tenants})
|
|
}
|
|
|
|
// GetTenant returns a tenant by ID
|
|
func (h *RBACHandlers) GetTenant(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid tenant ID"})
|
|
return
|
|
}
|
|
|
|
tenant, err := h.store.GetTenant(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "tenant not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, tenant)
|
|
}
|
|
|
|
// CreateTenant creates a new tenant
|
|
func (h *RBACHandlers) CreateTenant(c *gin.Context) {
|
|
var tenant rbac.Tenant
|
|
if err := c.ShouldBindJSON(&tenant); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if err := h.store.CreateTenant(c.Request.Context(), &tenant); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, tenant)
|
|
}
|
|
|
|
// UpdateTenant updates a tenant
|
|
func (h *RBACHandlers) UpdateTenant(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid tenant ID"})
|
|
return
|
|
}
|
|
|
|
var tenant rbac.Tenant
|
|
if err := c.ShouldBindJSON(&tenant); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
tenant.ID = id
|
|
if err := h.store.UpdateTenant(c.Request.Context(), &tenant); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, tenant)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Namespace Endpoints
|
|
// ============================================================================
|
|
|
|
// ListNamespaces returns namespaces for a tenant
|
|
func (h *RBACHandlers) ListNamespaces(c *gin.Context) {
|
|
tenantID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
tenantID = rbac.GetTenantID(c)
|
|
}
|
|
|
|
if tenantID == uuid.Nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
|
|
return
|
|
}
|
|
|
|
namespaces, err := h.store.ListNamespaces(c.Request.Context(), tenantID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"namespaces": namespaces})
|
|
}
|
|
|
|
// GetNamespace returns a namespace by ID
|
|
func (h *RBACHandlers) GetNamespace(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid namespace ID"})
|
|
return
|
|
}
|
|
|
|
namespace, err := h.store.GetNamespace(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "namespace not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, namespace)
|
|
}
|
|
|
|
// CreateNamespace creates a new namespace
|
|
func (h *RBACHandlers) CreateNamespace(c *gin.Context) {
|
|
tenantID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
tenantID = rbac.GetTenantID(c)
|
|
}
|
|
|
|
if tenantID == uuid.Nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
|
|
return
|
|
}
|
|
|
|
var namespace rbac.Namespace
|
|
if err := c.ShouldBindJSON(&namespace); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
namespace.TenantID = tenantID
|
|
if err := h.store.CreateNamespace(c.Request.Context(), &namespace); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, namespace)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Role Endpoints
|
|
// ============================================================================
|
|
|
|
// ListRoles returns roles for a tenant (including system roles)
|
|
func (h *RBACHandlers) ListRoles(c *gin.Context) {
|
|
tenantID := rbac.GetTenantID(c)
|
|
var tenantIDPtr *uuid.UUID
|
|
if tenantID != uuid.Nil {
|
|
tenantIDPtr = &tenantID
|
|
}
|
|
|
|
roles, err := h.store.ListRoles(c.Request.Context(), tenantIDPtr)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"roles": roles})
|
|
}
|
|
|
|
// ListSystemRoles returns all system roles
|
|
func (h *RBACHandlers) ListSystemRoles(c *gin.Context) {
|
|
roles, err := h.store.ListSystemRoles(c.Request.Context())
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"roles": roles})
|
|
}
|
|
|
|
// GetRole returns a role by ID
|
|
func (h *RBACHandlers) GetRole(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid role ID"})
|
|
return
|
|
}
|
|
|
|
role, err := h.store.GetRole(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "role not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, role)
|
|
}
|
|
|
|
// CreateRole creates a new role
|
|
func (h *RBACHandlers) CreateRole(c *gin.Context) {
|
|
var role rbac.Role
|
|
if err := c.ShouldBindJSON(&role); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
tenantID := rbac.GetTenantID(c)
|
|
if tenantID != uuid.Nil {
|
|
role.TenantID = &tenantID
|
|
}
|
|
|
|
if err := h.store.CreateRole(c.Request.Context(), &role); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, role)
|
|
}
|
|
|
|
// ============================================================================
|
|
// User Role Endpoints
|
|
// ============================================================================
|
|
|
|
// AssignRoleRequest represents a role assignment request
|
|
type AssignRoleRequest struct {
|
|
UserID string `json:"user_id" binding:"required"`
|
|
RoleID string `json:"role_id" binding:"required"`
|
|
NamespaceID *string `json:"namespace_id"`
|
|
ExpiresAt *string `json:"expires_at"` // RFC3339 format
|
|
}
|
|
|
|
// AssignRole assigns a role to a user
|
|
func (h *RBACHandlers) AssignRole(c *gin.Context) {
|
|
var req AssignRoleRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
userID, err := uuid.Parse(req.UserID)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user ID"})
|
|
return
|
|
}
|
|
|
|
roleID, err := uuid.Parse(req.RoleID)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid role ID"})
|
|
return
|
|
}
|
|
|
|
tenantID := rbac.GetTenantID(c)
|
|
if tenantID == uuid.Nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
|
|
return
|
|
}
|
|
|
|
grantorID := rbac.GetUserID(c)
|
|
if grantorID == uuid.Nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "authentication required"})
|
|
return
|
|
}
|
|
|
|
userRole := &rbac.UserRole{
|
|
UserID: userID,
|
|
RoleID: roleID,
|
|
TenantID: tenantID,
|
|
}
|
|
|
|
if req.NamespaceID != nil {
|
|
nsID, err := uuid.Parse(*req.NamespaceID)
|
|
if err == nil {
|
|
userRole.NamespaceID = &nsID
|
|
}
|
|
}
|
|
|
|
if err := h.service.AssignRoleToUser(c.Request.Context(), userRole, grantorID); err != nil {
|
|
if err == rbac.ErrPermissionDenied {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "permission denied"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "role assigned successfully"})
|
|
}
|
|
|
|
// RevokeRole revokes a role from a user
|
|
func (h *RBACHandlers) RevokeRole(c *gin.Context) {
|
|
userIDStr := c.Param("userId")
|
|
roleIDStr := c.Param("roleId")
|
|
|
|
userID, err := uuid.Parse(userIDStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user ID"})
|
|
return
|
|
}
|
|
|
|
roleID, err := uuid.Parse(roleIDStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid role ID"})
|
|
return
|
|
}
|
|
|
|
tenantID := rbac.GetTenantID(c)
|
|
if tenantID == uuid.Nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
|
|
return
|
|
}
|
|
|
|
revokerID := rbac.GetUserID(c)
|
|
if revokerID == uuid.Nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "authentication required"})
|
|
return
|
|
}
|
|
|
|
var namespaceID *uuid.UUID
|
|
if nsIDStr := c.Query("namespace_id"); nsIDStr != "" {
|
|
if nsID, err := uuid.Parse(nsIDStr); err == nil {
|
|
namespaceID = &nsID
|
|
}
|
|
}
|
|
|
|
if err := h.service.RevokeRoleFromUser(c.Request.Context(), userID, roleID, tenantID, namespaceID, revokerID); err != nil {
|
|
if err == rbac.ErrPermissionDenied {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "permission denied"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "role revoked successfully"})
|
|
}
|
|
|
|
// GetUserRoles returns all roles for a user
|
|
func (h *RBACHandlers) GetUserRoles(c *gin.Context) {
|
|
userIDStr := c.Param("userId")
|
|
userID, err := uuid.Parse(userIDStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user ID"})
|
|
return
|
|
}
|
|
|
|
tenantID := rbac.GetTenantID(c)
|
|
if tenantID == uuid.Nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
|
|
return
|
|
}
|
|
|
|
roles, err := h.store.GetUserRoles(c.Request.Context(), userID, tenantID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"roles": roles})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Permission Endpoints
|
|
// ============================================================================
|
|
|
|
// GetEffectivePermissions returns effective permissions for the current user
|
|
func (h *RBACHandlers) GetEffectivePermissions(c *gin.Context) {
|
|
userID := rbac.GetUserID(c)
|
|
tenantID := rbac.GetTenantID(c)
|
|
namespaceID := rbac.GetNamespaceID(c)
|
|
|
|
if userID == uuid.Nil || tenantID == uuid.Nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "authentication required"})
|
|
return
|
|
}
|
|
|
|
perms, err := h.service.GetEffectivePermissions(c.Request.Context(), userID, tenantID, namespaceID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, perms)
|
|
}
|
|
|
|
// GetUserContext returns complete context for the current user
|
|
func (h *RBACHandlers) GetUserContext(c *gin.Context) {
|
|
userID := rbac.GetUserID(c)
|
|
tenantID := rbac.GetTenantID(c)
|
|
|
|
if userID == uuid.Nil || tenantID == uuid.Nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "authentication required"})
|
|
return
|
|
}
|
|
|
|
ctx, err := h.policyEngine.GetUserContext(c.Request.Context(), userID, tenantID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, ctx)
|
|
}
|
|
|
|
// CheckPermission checks if user has a specific permission
|
|
func (h *RBACHandlers) CheckPermission(c *gin.Context) {
|
|
permission := c.Query("permission")
|
|
if permission == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "permission parameter required"})
|
|
return
|
|
}
|
|
|
|
userID := rbac.GetUserID(c)
|
|
tenantID := rbac.GetTenantID(c)
|
|
namespaceID := rbac.GetNamespaceID(c)
|
|
|
|
if userID == uuid.Nil || tenantID == uuid.Nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "authentication required"})
|
|
return
|
|
}
|
|
|
|
hasPermission, err := h.service.HasPermission(c.Request.Context(), userID, tenantID, namespaceID, permission)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"permission": permission,
|
|
"has_permission": hasPermission,
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// LLM Policy Endpoints
|
|
// ============================================================================
|
|
|
|
// ListLLMPolicies returns LLM policies for a tenant
|
|
func (h *RBACHandlers) ListLLMPolicies(c *gin.Context) {
|
|
tenantID := rbac.GetTenantID(c)
|
|
if tenantID == uuid.Nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
|
|
return
|
|
}
|
|
|
|
policies, err := h.store.ListLLMPolicies(c.Request.Context(), tenantID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"policies": policies})
|
|
}
|
|
|
|
// GetLLMPolicy returns an LLM policy by ID
|
|
func (h *RBACHandlers) GetLLMPolicy(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid policy ID"})
|
|
return
|
|
}
|
|
|
|
policy, err := h.store.GetLLMPolicy(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "policy not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, policy)
|
|
}
|
|
|
|
// CreateLLMPolicy creates a new LLM policy
|
|
func (h *RBACHandlers) CreateLLMPolicy(c *gin.Context) {
|
|
var policy rbac.LLMPolicy
|
|
if err := c.ShouldBindJSON(&policy); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
tenantID := rbac.GetTenantID(c)
|
|
if tenantID == uuid.Nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
|
|
return
|
|
}
|
|
|
|
policy.TenantID = tenantID
|
|
if err := h.store.CreateLLMPolicy(c.Request.Context(), &policy); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, policy)
|
|
}
|
|
|
|
// UpdateLLMPolicy updates an LLM policy
|
|
func (h *RBACHandlers) UpdateLLMPolicy(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid policy ID"})
|
|
return
|
|
}
|
|
|
|
var policy rbac.LLMPolicy
|
|
if err := c.ShouldBindJSON(&policy); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
policy.ID = id
|
|
if err := h.store.UpdateLLMPolicy(c.Request.Context(), &policy); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, policy)
|
|
}
|
|
|
|
// DeleteLLMPolicy deletes an LLM policy
|
|
func (h *RBACHandlers) DeleteLLMPolicy(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid policy ID"})
|
|
return
|
|
}
|
|
|
|
if err := h.store.DeleteLLMPolicy(c.Request.Context(), id); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "policy deleted"})
|
|
}
|