Release prep: 54 engines, self-hosted signatures, i18n, dashboard updates

This commit is contained in:
DmitrL-dev 2026-03-23 16:45:40 +10:00
parent 694e32be26
commit 41cbfd6e0a
178 changed files with 36008 additions and 399 deletions

View file

@ -0,0 +1,278 @@
package shadow_ai
import (
"fmt"
"log/slog"
"sync"
"time"
)
// --- Tiered Approval Workflow ---
// Implements §6 of the ТЗ: data classification → approval tier → SLA tracking.
// ApprovalStatus tracks the state of an approval request.
type ApprovalStatus string
const (
ApprovalPending ApprovalStatus = "pending"
ApprovalApproved ApprovalStatus = "approved"
ApprovalDenied ApprovalStatus = "denied"
ApprovalExpired ApprovalStatus = "expired"
ApprovalAutoApproved ApprovalStatus = "auto_approved"
)
// DefaultApprovalTiers defines the approval requirements per data classification.
func DefaultApprovalTiers() []ApprovalTier {
return []ApprovalTier{
{
Name: "Tier 1: Public Data",
DataClass: DataPublic,
ApprovalNeeded: nil, // Auto-approve
SLA: 0,
AutoApprove: true,
},
{
Name: "Tier 2: Internal Data",
DataClass: DataInternal,
ApprovalNeeded: []string{"manager"},
SLA: 4 * time.Hour,
AutoApprove: false,
},
{
Name: "Tier 3: Confidential Data",
DataClass: DataConfidential,
ApprovalNeeded: []string{"manager", "soc"},
SLA: 24 * time.Hour,
AutoApprove: false,
},
{
Name: "Tier 4: Critical Data",
DataClass: DataCritical,
ApprovalNeeded: []string{"ciso"},
SLA: 0, // Manual only, no auto-expire
AutoApprove: false,
},
}
}
// ApprovalEngine manages the tiered approval workflow.
type ApprovalEngine struct {
mu sync.RWMutex
tiers []ApprovalTier
requests map[string]*ApprovalRequest
logger *slog.Logger
}
// NewApprovalEngine creates an engine with default tiers.
func NewApprovalEngine() *ApprovalEngine {
return &ApprovalEngine{
tiers: DefaultApprovalTiers(),
requests: make(map[string]*ApprovalRequest),
logger: slog.Default().With("component", "shadow-ai-approvals"),
}
}
// SubmitRequest creates a new approval request based on data classification.
// Returns the request or auto-approves if the tier allows it.
func (ae *ApprovalEngine) SubmitRequest(userID, docID string, dataClass DataClassification) *ApprovalRequest {
ae.mu.Lock()
defer ae.mu.Unlock()
tier := ae.findTier(dataClass)
req := &ApprovalRequest{
ID: genApprovalID(),
DocID: docID,
UserID: userID,
Tier: tier.Name,
DataClass: dataClass,
Status: string(ApprovalPending),
CreatedAt: time.Now(),
}
// Set expiry based on SLA.
if tier.SLA > 0 {
req.ExpiresAt = req.CreatedAt.Add(tier.SLA)
}
// Auto-approve for public data.
if tier.AutoApprove {
req.Status = string(ApprovalAutoApproved)
req.ApprovedBy = "system"
req.ResolvedAt = time.Now()
ae.logger.Info("auto-approved",
"request_id", req.ID,
"user", userID,
"data_class", dataClass,
)
} else {
ae.logger.Info("approval required",
"request_id", req.ID,
"user", userID,
"data_class", dataClass,
"tier", tier.Name,
"approvers", tier.ApprovalNeeded,
)
}
ae.requests[req.ID] = req
return req
}
// Approve approves a pending request.
func (ae *ApprovalEngine) Approve(requestID, approvedBy string) error {
ae.mu.Lock()
defer ae.mu.Unlock()
req, ok := ae.requests[requestID]
if !ok {
return fmt.Errorf("request %s not found", requestID)
}
if req.Status != string(ApprovalPending) {
return fmt.Errorf("request %s is not pending (status: %s)", requestID, req.Status)
}
req.Status = string(ApprovalApproved)
req.ApprovedBy = approvedBy
req.ResolvedAt = time.Now()
ae.logger.Info("approved",
"request_id", requestID,
"approved_by", approvedBy,
)
return nil
}
// Deny denies a pending request.
func (ae *ApprovalEngine) Deny(requestID, deniedBy, reason string) error {
ae.mu.Lock()
defer ae.mu.Unlock()
req, ok := ae.requests[requestID]
if !ok {
return fmt.Errorf("request %s not found", requestID)
}
if req.Status != string(ApprovalPending) {
return fmt.Errorf("request %s is not pending (status: %s)", requestID, req.Status)
}
req.Status = string(ApprovalDenied)
req.DeniedBy = deniedBy
req.Reason = reason
req.ResolvedAt = time.Now()
ae.logger.Info("denied",
"request_id", requestID,
"denied_by", deniedBy,
"reason", reason,
)
return nil
}
// GetRequest returns an approval request by ID.
func (ae *ApprovalEngine) GetRequest(requestID string) (*ApprovalRequest, bool) {
ae.mu.RLock()
defer ae.mu.RUnlock()
req, ok := ae.requests[requestID]
if !ok {
return nil, false
}
cp := *req
return &cp, true
}
// PendingRequests returns all pending approval requests.
func (ae *ApprovalEngine) PendingRequests() []ApprovalRequest {
ae.mu.RLock()
defer ae.mu.RUnlock()
var result []ApprovalRequest
for _, req := range ae.requests {
if req.Status == string(ApprovalPending) {
result = append(result, *req)
}
}
return result
}
// ExpireOverdue marks overdue pending requests as expired.
// Returns the number of expired requests.
func (ae *ApprovalEngine) ExpireOverdue() int {
ae.mu.Lock()
defer ae.mu.Unlock()
now := time.Now()
expired := 0
for _, req := range ae.requests {
if req.Status == string(ApprovalPending) && !req.ExpiresAt.IsZero() && now.After(req.ExpiresAt) {
req.Status = string(ApprovalExpired)
req.ResolvedAt = now
expired++
ae.logger.Warn("request expired",
"request_id", req.ID,
"user", req.UserID,
"expired_at", req.ExpiresAt,
)
}
}
return expired
}
// Stats returns approval workflow statistics.
func (ae *ApprovalEngine) Stats() map[string]int {
ae.mu.RLock()
defer ae.mu.RUnlock()
stats := map[string]int{
"total": len(ae.requests),
"pending": 0,
"approved": 0,
"denied": 0,
"expired": 0,
"auto_approved": 0,
}
for _, req := range ae.requests {
switch ApprovalStatus(req.Status) {
case ApprovalPending:
stats["pending"]++
case ApprovalApproved:
stats["approved"]++
case ApprovalDenied:
stats["denied"]++
case ApprovalExpired:
stats["expired"]++
case ApprovalAutoApproved:
stats["auto_approved"]++
}
}
return stats
}
// Tiers returns the approval tier configuration.
func (ae *ApprovalEngine) Tiers() []ApprovalTier {
return ae.tiers
}
func (ae *ApprovalEngine) findTier(dataClass DataClassification) ApprovalTier {
for _, t := range ae.tiers {
if t.DataClass == dataClass {
return t
}
}
// Default to most restrictive.
return ae.tiers[len(ae.tiers)-1]
}
var approvalCounter uint64
var approvalCounterMu sync.Mutex
func genApprovalID() string {
approvalCounterMu.Lock()
approvalCounter++
id := approvalCounter
approvalCounterMu.Unlock()
return fmt.Sprintf("apr-%d-%d", time.Now().UnixMilli(), id)
}