All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 28s
CI / test-go-edu-search (push) Successful in 27s
CI / test-python-klausur (push) Successful in 1m45s
CI / test-python-agent-core (push) Successful in 16s
CI / test-nodejs-website (push) Successful in 21s
- edu-search-service von breakpilot-pwa nach breakpilot-lehrer kopiert (ohne vendor) - opensearch + edu-search-service in docker-compose.yml hinzugefuegt - voice-service aus docker-compose.yml entfernt (jetzt in breakpilot-core) - geo-service aus docker-compose.yml entfernt (nicht mehr benoetigt) - CI/CD: edu-search-service zu Gitea Actions und Woodpecker hinzugefuegt (Go lint, test mit go mod download, build, SBOM) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
701 lines
21 KiB
Go
701 lines
21 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/breakpilot/edu-search-service/internal/policy"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// PolicyHandler contains all policy-related HTTP handlers.
|
|
type PolicyHandler struct {
|
|
store *policy.Store
|
|
enforcer *policy.Enforcer
|
|
}
|
|
|
|
// policyHandler is the singleton instance
|
|
var policyHandler *PolicyHandler
|
|
|
|
// InitPolicyHandler initializes the policy handler with a database pool.
|
|
func InitPolicyHandler(store *policy.Store) {
|
|
policyHandler = &PolicyHandler{
|
|
store: store,
|
|
enforcer: policy.NewEnforcer(store),
|
|
}
|
|
}
|
|
|
|
// GetPolicyHandler returns the policy handler instance.
|
|
func GetPolicyHandler() *PolicyHandler {
|
|
return policyHandler
|
|
}
|
|
|
|
// =============================================================================
|
|
// POLICIES
|
|
// =============================================================================
|
|
|
|
// ListPolicies returns all source policies.
|
|
func (h *PolicyHandler) ListPolicies(c *gin.Context) {
|
|
var filter policy.PolicyListFilter
|
|
if err := c.ShouldBindQuery(&filter); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid query parameters", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Set defaults
|
|
if filter.Limit <= 0 || filter.Limit > 100 {
|
|
filter.Limit = 50
|
|
}
|
|
|
|
policies, total, err := h.store.ListPolicies(c.Request.Context(), &filter)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list policies", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"policies": policies,
|
|
"total": total,
|
|
"limit": filter.Limit,
|
|
"offset": filter.Offset,
|
|
})
|
|
}
|
|
|
|
// GetPolicy returns a single policy by ID.
|
|
func (h *PolicyHandler) GetPolicy(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid policy ID"})
|
|
return
|
|
}
|
|
|
|
p, err := h.store.GetPolicy(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get policy", "details": err.Error()})
|
|
return
|
|
}
|
|
if p == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Policy not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, p)
|
|
}
|
|
|
|
// CreatePolicy creates a new source policy.
|
|
func (h *PolicyHandler) CreatePolicy(c *gin.Context) {
|
|
var req policy.CreateSourcePolicyRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
p, err := h.store.CreatePolicy(c.Request.Context(), &req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create policy", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Log audit
|
|
userEmail := getUserEmail(c)
|
|
h.enforcer.LogChange(c.Request.Context(), policy.AuditActionCreate, policy.AuditEntitySourcePolicy, &p.ID, nil, p, userEmail)
|
|
|
|
c.JSON(http.StatusCreated, p)
|
|
}
|
|
|
|
// UpdatePolicy updates an existing policy.
|
|
func (h *PolicyHandler) UpdatePolicy(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid policy ID"})
|
|
return
|
|
}
|
|
|
|
// Get old value for audit
|
|
oldPolicy, err := h.store.GetPolicy(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get policy", "details": err.Error()})
|
|
return
|
|
}
|
|
if oldPolicy == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Policy not found"})
|
|
return
|
|
}
|
|
|
|
var req policy.UpdateSourcePolicyRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
p, err := h.store.UpdatePolicy(c.Request.Context(), id, &req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update policy", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Log audit
|
|
userEmail := getUserEmail(c)
|
|
h.enforcer.LogChange(c.Request.Context(), policy.AuditActionUpdate, policy.AuditEntitySourcePolicy, &p.ID, oldPolicy, p, userEmail)
|
|
|
|
c.JSON(http.StatusOK, p)
|
|
}
|
|
|
|
// =============================================================================
|
|
// SOURCES (WHITELIST)
|
|
// =============================================================================
|
|
|
|
// ListSources returns all allowed sources.
|
|
func (h *PolicyHandler) ListSources(c *gin.Context) {
|
|
var filter policy.SourceListFilter
|
|
if err := c.ShouldBindQuery(&filter); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid query parameters", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Set defaults
|
|
if filter.Limit <= 0 || filter.Limit > 100 {
|
|
filter.Limit = 50
|
|
}
|
|
|
|
sources, total, err := h.store.ListSources(c.Request.Context(), &filter)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list sources", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"sources": sources,
|
|
"total": total,
|
|
"limit": filter.Limit,
|
|
"offset": filter.Offset,
|
|
})
|
|
}
|
|
|
|
// GetSource returns a single source by ID.
|
|
func (h *PolicyHandler) GetSource(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid source ID"})
|
|
return
|
|
}
|
|
|
|
source, err := h.store.GetSource(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get source", "details": err.Error()})
|
|
return
|
|
}
|
|
if source == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Source not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, source)
|
|
}
|
|
|
|
// CreateSource creates a new allowed source.
|
|
func (h *PolicyHandler) CreateSource(c *gin.Context) {
|
|
var req policy.CreateAllowedSourceRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
source, err := h.store.CreateSource(c.Request.Context(), &req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create source", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Log audit
|
|
userEmail := getUserEmail(c)
|
|
h.enforcer.LogChange(c.Request.Context(), policy.AuditActionCreate, policy.AuditEntityAllowedSource, &source.ID, nil, source, userEmail)
|
|
|
|
c.JSON(http.StatusCreated, source)
|
|
}
|
|
|
|
// UpdateSource updates an existing source.
|
|
func (h *PolicyHandler) UpdateSource(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid source ID"})
|
|
return
|
|
}
|
|
|
|
// Get old value for audit
|
|
oldSource, err := h.store.GetSource(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get source", "details": err.Error()})
|
|
return
|
|
}
|
|
if oldSource == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Source not found"})
|
|
return
|
|
}
|
|
|
|
var req policy.UpdateAllowedSourceRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
source, err := h.store.UpdateSource(c.Request.Context(), id, &req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update source", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Log audit
|
|
userEmail := getUserEmail(c)
|
|
h.enforcer.LogChange(c.Request.Context(), policy.AuditActionUpdate, policy.AuditEntityAllowedSource, &source.ID, oldSource, source, userEmail)
|
|
|
|
c.JSON(http.StatusOK, source)
|
|
}
|
|
|
|
// DeleteSource deletes a source.
|
|
func (h *PolicyHandler) DeleteSource(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid source ID"})
|
|
return
|
|
}
|
|
|
|
// Get source for audit before deletion
|
|
source, err := h.store.GetSource(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get source", "details": err.Error()})
|
|
return
|
|
}
|
|
if source == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Source not found"})
|
|
return
|
|
}
|
|
|
|
if err := h.store.DeleteSource(c.Request.Context(), id); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete source", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Log audit
|
|
userEmail := getUserEmail(c)
|
|
h.enforcer.LogChange(c.Request.Context(), policy.AuditActionDelete, policy.AuditEntityAllowedSource, &id, source, nil, userEmail)
|
|
|
|
c.JSON(http.StatusOK, gin.H{"deleted": true, "id": id})
|
|
}
|
|
|
|
// =============================================================================
|
|
// OPERATIONS MATRIX
|
|
// =============================================================================
|
|
|
|
// GetOperationsMatrix returns all sources with their operation permissions.
|
|
func (h *PolicyHandler) GetOperationsMatrix(c *gin.Context) {
|
|
sources, err := h.store.GetOperationsMatrix(c.Request.Context())
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get operations matrix", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"sources": sources,
|
|
"operations": []string{
|
|
string(policy.OperationLookup),
|
|
string(policy.OperationRAG),
|
|
string(policy.OperationTraining),
|
|
string(policy.OperationExport),
|
|
},
|
|
})
|
|
}
|
|
|
|
// UpdateOperationPermission updates a single operation permission.
|
|
func (h *PolicyHandler) UpdateOperationPermission(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid operation permission ID"})
|
|
return
|
|
}
|
|
|
|
var req policy.UpdateOperationPermissionRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
// SECURITY: Prevent enabling training
|
|
if req.IsAllowed != nil && *req.IsAllowed {
|
|
// Check if this is a training operation by querying
|
|
ops, _ := h.store.GetOperationsBySourceID(c.Request.Context(), id)
|
|
for _, op := range ops {
|
|
if op.ID == id && op.Operation == policy.OperationTraining {
|
|
c.JSON(http.StatusForbidden, gin.H{
|
|
"error": "Training operations cannot be enabled",
|
|
"message": "Training with external data is FORBIDDEN by policy",
|
|
})
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
op, err := h.store.UpdateOperationPermission(c.Request.Context(), id, &req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update operation permission", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Log audit
|
|
userEmail := getUserEmail(c)
|
|
h.enforcer.LogChange(c.Request.Context(), policy.AuditActionUpdate, policy.AuditEntityOperationPermission, &op.ID, nil, op, userEmail)
|
|
|
|
c.JSON(http.StatusOK, op)
|
|
}
|
|
|
|
// =============================================================================
|
|
// PII RULES
|
|
// =============================================================================
|
|
|
|
// ListPIIRules returns all PII detection rules.
|
|
func (h *PolicyHandler) ListPIIRules(c *gin.Context) {
|
|
activeOnly := c.Query("active_only") == "true"
|
|
|
|
rules, err := h.store.ListPIIRules(c.Request.Context(), activeOnly)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list PII rules", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"rules": rules,
|
|
"total": len(rules),
|
|
})
|
|
}
|
|
|
|
// GetPIIRule returns a single PII rule by ID.
|
|
func (h *PolicyHandler) GetPIIRule(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid PII rule ID"})
|
|
return
|
|
}
|
|
|
|
rule, err := h.store.GetPIIRule(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get PII rule", "details": err.Error()})
|
|
return
|
|
}
|
|
if rule == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "PII rule not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, rule)
|
|
}
|
|
|
|
// CreatePIIRule creates a new PII detection rule.
|
|
func (h *PolicyHandler) CreatePIIRule(c *gin.Context) {
|
|
var req policy.CreatePIIRuleRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
rule, err := h.store.CreatePIIRule(c.Request.Context(), &req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create PII rule", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Log audit
|
|
userEmail := getUserEmail(c)
|
|
h.enforcer.LogChange(c.Request.Context(), policy.AuditActionCreate, policy.AuditEntityPIIRule, &rule.ID, nil, rule, userEmail)
|
|
|
|
c.JSON(http.StatusCreated, rule)
|
|
}
|
|
|
|
// UpdatePIIRule updates an existing PII rule.
|
|
func (h *PolicyHandler) UpdatePIIRule(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid PII rule ID"})
|
|
return
|
|
}
|
|
|
|
// Get old value for audit
|
|
oldRule, err := h.store.GetPIIRule(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get PII rule", "details": err.Error()})
|
|
return
|
|
}
|
|
if oldRule == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "PII rule not found"})
|
|
return
|
|
}
|
|
|
|
var req policy.UpdatePIIRuleRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
rule, err := h.store.UpdatePIIRule(c.Request.Context(), id, &req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update PII rule", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Log audit
|
|
userEmail := getUserEmail(c)
|
|
h.enforcer.LogChange(c.Request.Context(), policy.AuditActionUpdate, policy.AuditEntityPIIRule, &rule.ID, oldRule, rule, userEmail)
|
|
|
|
c.JSON(http.StatusOK, rule)
|
|
}
|
|
|
|
// DeletePIIRule deletes a PII rule.
|
|
func (h *PolicyHandler) DeletePIIRule(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid PII rule ID"})
|
|
return
|
|
}
|
|
|
|
// Get rule for audit before deletion
|
|
rule, err := h.store.GetPIIRule(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get PII rule", "details": err.Error()})
|
|
return
|
|
}
|
|
if rule == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "PII rule not found"})
|
|
return
|
|
}
|
|
|
|
if err := h.store.DeletePIIRule(c.Request.Context(), id); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete PII rule", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Log audit
|
|
userEmail := getUserEmail(c)
|
|
h.enforcer.LogChange(c.Request.Context(), policy.AuditActionDelete, policy.AuditEntityPIIRule, &id, rule, nil, userEmail)
|
|
|
|
c.JSON(http.StatusOK, gin.H{"deleted": true, "id": id})
|
|
}
|
|
|
|
// TestPIIRules tests PII detection against sample text.
|
|
func (h *PolicyHandler) TestPIIRules(c *gin.Context) {
|
|
var req policy.PIITestRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
response, err := h.enforcer.DetectPII(c.Request.Context(), req.Text)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to test PII detection", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
// =============================================================================
|
|
// AUDIT & COMPLIANCE
|
|
// =============================================================================
|
|
|
|
// ListAuditLogs returns audit log entries.
|
|
func (h *PolicyHandler) ListAuditLogs(c *gin.Context) {
|
|
var filter policy.AuditLogFilter
|
|
if err := c.ShouldBindQuery(&filter); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid query parameters", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Set defaults
|
|
if filter.Limit <= 0 || filter.Limit > 500 {
|
|
filter.Limit = 100
|
|
}
|
|
|
|
logs, total, err := h.store.ListAuditLogs(c.Request.Context(), &filter)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list audit logs", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"logs": logs,
|
|
"total": total,
|
|
"limit": filter.Limit,
|
|
"offset": filter.Offset,
|
|
})
|
|
}
|
|
|
|
// ListBlockedContent returns blocked content log entries.
|
|
func (h *PolicyHandler) ListBlockedContent(c *gin.Context) {
|
|
var filter policy.BlockedContentFilter
|
|
if err := c.ShouldBindQuery(&filter); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid query parameters", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Set defaults
|
|
if filter.Limit <= 0 || filter.Limit > 500 {
|
|
filter.Limit = 100
|
|
}
|
|
|
|
logs, total, err := h.store.ListBlockedContent(c.Request.Context(), &filter)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list blocked content", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"blocked": logs,
|
|
"total": total,
|
|
"limit": filter.Limit,
|
|
"offset": filter.Offset,
|
|
})
|
|
}
|
|
|
|
// CheckCompliance performs a compliance check for a URL.
|
|
func (h *PolicyHandler) CheckCompliance(c *gin.Context) {
|
|
var req policy.CheckComplianceRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
response, err := h.enforcer.CheckCompliance(c.Request.Context(), &req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check compliance", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
// GetPolicyStats returns aggregated statistics.
|
|
func (h *PolicyHandler) GetPolicyStats(c *gin.Context) {
|
|
stats, err := h.store.GetStats(c.Request.Context())
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get stats", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, stats)
|
|
}
|
|
|
|
// GenerateComplianceReport generates an audit report.
|
|
func (h *PolicyHandler) GenerateComplianceReport(c *gin.Context) {
|
|
var auditFilter policy.AuditLogFilter
|
|
var blockedFilter policy.BlockedContentFilter
|
|
|
|
// Parse date filters
|
|
fromStr := c.Query("from")
|
|
toStr := c.Query("to")
|
|
|
|
if fromStr != "" {
|
|
from, err := time.Parse("2006-01-02", fromStr)
|
|
if err == nil {
|
|
auditFilter.FromDate = &from
|
|
blockedFilter.FromDate = &from
|
|
}
|
|
}
|
|
|
|
if toStr != "" {
|
|
to, err := time.Parse("2006-01-02", toStr)
|
|
if err == nil {
|
|
// Add 1 day to include the end date
|
|
to = to.Add(24 * time.Hour)
|
|
auditFilter.ToDate = &to
|
|
blockedFilter.ToDate = &to
|
|
}
|
|
}
|
|
|
|
// No limit for report
|
|
auditFilter.Limit = 10000
|
|
blockedFilter.Limit = 10000
|
|
|
|
auditor := policy.NewAuditor(h.store)
|
|
report, err := auditor.GenerateAuditReport(c.Request.Context(), &auditFilter, &blockedFilter)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate report", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Set filename for download
|
|
format := c.Query("format")
|
|
if format == "download" {
|
|
filename := "compliance-report-" + time.Now().Format("2006-01-02") + ".json"
|
|
c.Header("Content-Disposition", "attachment; filename="+filename)
|
|
c.Header("Content-Type", "application/json")
|
|
}
|
|
|
|
c.JSON(http.StatusOK, report)
|
|
}
|
|
|
|
// =============================================================================
|
|
// HELPERS
|
|
// =============================================================================
|
|
|
|
// getUserEmail extracts user email from context or headers.
|
|
func getUserEmail(c *gin.Context) *string {
|
|
// Try to get from header (set by auth proxy)
|
|
email := c.GetHeader("X-User-Email")
|
|
if email != "" {
|
|
return &email
|
|
}
|
|
|
|
// Try to get from context (set by auth middleware)
|
|
if e, exists := c.Get("user_email"); exists {
|
|
if emailStr, ok := e.(string); ok {
|
|
return &emailStr
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// =============================================================================
|
|
// ROUTE SETUP
|
|
// =============================================================================
|
|
|
|
// SetupPolicyRoutes configures all policy-related routes.
|
|
func SetupPolicyRoutes(r *gin.RouterGroup) {
|
|
if policyHandler == nil {
|
|
return
|
|
}
|
|
|
|
h := policyHandler
|
|
|
|
// Policies
|
|
r.GET("/policies", h.ListPolicies)
|
|
r.GET("/policies/:id", h.GetPolicy)
|
|
r.POST("/policies", h.CreatePolicy)
|
|
r.PUT("/policies/:id", h.UpdatePolicy)
|
|
|
|
// Sources (Whitelist)
|
|
r.GET("/sources", h.ListSources)
|
|
r.GET("/sources/:id", h.GetSource)
|
|
r.POST("/sources", h.CreateSource)
|
|
r.PUT("/sources/:id", h.UpdateSource)
|
|
r.DELETE("/sources/:id", h.DeleteSource)
|
|
|
|
// Operations Matrix
|
|
r.GET("/operations-matrix", h.GetOperationsMatrix)
|
|
r.PUT("/operations/:id", h.UpdateOperationPermission)
|
|
|
|
// PII Rules
|
|
r.GET("/pii-rules", h.ListPIIRules)
|
|
r.GET("/pii-rules/:id", h.GetPIIRule)
|
|
r.POST("/pii-rules", h.CreatePIIRule)
|
|
r.PUT("/pii-rules/:id", h.UpdatePIIRule)
|
|
r.DELETE("/pii-rules/:id", h.DeletePIIRule)
|
|
r.POST("/pii-rules/test", h.TestPIIRules)
|
|
|
|
// Audit & Compliance
|
|
r.GET("/policy-audit", h.ListAuditLogs)
|
|
r.GET("/blocked-content", h.ListBlockedContent)
|
|
r.POST("/check-compliance", h.CheckCompliance)
|
|
r.GET("/policy-stats", h.GetPolicyStats)
|
|
r.GET("/compliance-report", h.GenerateComplianceReport)
|
|
}
|