feat: Add Academy, Whistleblower, Incidents, Vendor, DSB, SSO, Reporting, Multi-Tenant and Industry backends
Go handlers, models, stores and migrations for all SDK modules. Updates developer portal navigation and BYOEH page. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
148
ai-compliance-sdk/internal/multitenant/store.go
Normal file
148
ai-compliance-sdk/internal/multitenant/store.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package multitenant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/reporting"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// Store provides aggregated multi-tenant views by combining data from the
|
||||
// existing RBAC store, reporting store, and direct SQL queries for module highlights.
|
||||
type Store struct {
|
||||
pool *pgxpool.Pool
|
||||
rbacStore *rbac.Store
|
||||
reportingStore *reporting.Store
|
||||
}
|
||||
|
||||
// NewStore creates a new multi-tenant store.
|
||||
func NewStore(pool *pgxpool.Pool, rbacStore *rbac.Store, reportingStore *reporting.Store) *Store {
|
||||
return &Store{
|
||||
pool: pool,
|
||||
rbacStore: rbacStore,
|
||||
reportingStore: reportingStore,
|
||||
}
|
||||
}
|
||||
|
||||
// GetOverview retrieves all tenants with their compliance scores and module highlights.
|
||||
// It aggregates data from the RBAC tenant list, the reporting compliance score,
|
||||
// and direct SQL counts for namespaces, incidents, reports, DSRs, training, and vendors.
|
||||
// Individual query failures are tolerated and result in zero-value defaults.
|
||||
func (s *Store) GetOverview(ctx context.Context) (*MultiTenantOverviewResponse, error) {
|
||||
tenants, err := s.rbacStore.ListTenants(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list tenants: %w", err)
|
||||
}
|
||||
|
||||
overviews := make([]TenantOverview, 0, len(tenants))
|
||||
totalScore := 0
|
||||
|
||||
for _, tenant := range tenants {
|
||||
overview := s.buildTenantOverview(ctx, tenant)
|
||||
totalScore += overview.ComplianceScore
|
||||
overviews = append(overviews, overview)
|
||||
}
|
||||
|
||||
averageScore := 0
|
||||
if len(overviews) > 0 {
|
||||
averageScore = totalScore / len(overviews)
|
||||
}
|
||||
|
||||
return &MultiTenantOverviewResponse{
|
||||
Tenants: overviews,
|
||||
Total: len(overviews),
|
||||
AverageScore: averageScore,
|
||||
GeneratedAt: time.Now().UTC(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetTenantDetail returns detailed compliance info for a specific tenant.
|
||||
func (s *Store) GetTenantDetail(ctx context.Context, tenantID uuid.UUID) (*TenantOverview, error) {
|
||||
tenant, err := s.rbacStore.GetTenant(ctx, tenantID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get tenant: %w", err)
|
||||
}
|
||||
|
||||
overview := s.buildTenantOverview(ctx, tenant)
|
||||
return &overview, nil
|
||||
}
|
||||
|
||||
// buildTenantOverview constructs a TenantOverview by fetching compliance scores
|
||||
// and module highlights for a single tenant. Errors are logged but do not
|
||||
// propagate -- missing data defaults to zero values.
|
||||
func (s *Store) buildTenantOverview(ctx context.Context, tenant *rbac.Tenant) TenantOverview {
|
||||
overview := TenantOverview{
|
||||
ID: tenant.ID,
|
||||
Name: tenant.Name,
|
||||
Slug: tenant.Slug,
|
||||
Status: string(tenant.Status),
|
||||
MaxUsers: tenant.MaxUsers,
|
||||
LLMQuotaMonthly: tenant.LLMQuotaMonthly,
|
||||
CreatedAt: tenant.CreatedAt,
|
||||
UpdatedAt: tenant.UpdatedAt,
|
||||
}
|
||||
|
||||
// Compliance score and risk level derived from an executive report.
|
||||
// GenerateReport computes the compliance score and risk overview internally.
|
||||
report, err := s.reportingStore.GenerateReport(ctx, tenant.ID)
|
||||
if err != nil {
|
||||
log.Printf("multitenant: failed to generate report for tenant %s: %v", tenant.ID, err)
|
||||
} else {
|
||||
overview.ComplianceScore = report.ComplianceScore
|
||||
overview.RiskLevel = report.RiskOverview.OverallLevel
|
||||
}
|
||||
|
||||
// Namespace count
|
||||
overview.NamespaceCount = s.countSafe(ctx, tenant.ID,
|
||||
"SELECT COUNT(*) FROM compliance_namespaces WHERE tenant_id = $1")
|
||||
|
||||
// Open incidents
|
||||
overview.OpenIncidents = s.countSafe(ctx, tenant.ID,
|
||||
"SELECT COUNT(*) FROM incidents WHERE tenant_id = $1 AND status IN ('new', 'investigating', 'containment')")
|
||||
|
||||
// Open whistleblower reports
|
||||
overview.OpenReports = s.countSafe(ctx, tenant.ID,
|
||||
"SELECT COUNT(*) FROM whistleblower_reports WHERE tenant_id = $1 AND status IN ('new', 'acknowledged', 'investigating')")
|
||||
|
||||
// Pending DSR requests
|
||||
overview.PendingDSRs = s.countSafe(ctx, tenant.ID,
|
||||
"SELECT COUNT(*) FROM dsr_requests WHERE tenant_id = $1 AND status IN ('new', 'in_progress')")
|
||||
|
||||
// Training completion rate (average progress, 0-100)
|
||||
overview.TrainingRate = s.avgSafe(ctx, tenant.ID,
|
||||
"SELECT COALESCE(AVG(CASE WHEN status = 'completed' THEN 100.0 ELSE progress END), 0) FROM academy_enrollments WHERE tenant_id = $1")
|
||||
|
||||
// High-risk vendors
|
||||
overview.VendorRiskHigh = s.countSafe(ctx, tenant.ID,
|
||||
"SELECT COUNT(*) FROM vendors WHERE tenant_id = $1 AND risk_level = 'high'")
|
||||
|
||||
return overview
|
||||
}
|
||||
|
||||
// countSafe executes a COUNT(*) query that takes a single tenant_id parameter.
|
||||
// If the query fails for any reason (e.g. table does not exist), it returns 0.
|
||||
func (s *Store) countSafe(ctx context.Context, tenantID uuid.UUID, query string) int {
|
||||
var count int
|
||||
err := s.pool.QueryRow(ctx, query, tenantID).Scan(&count)
|
||||
if err != nil {
|
||||
// Tolerate errors -- table may not exist or query may fail
|
||||
return 0
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// avgSafe executes an AVG query that takes a single tenant_id parameter.
|
||||
// If the query fails for any reason, it returns 0.
|
||||
func (s *Store) avgSafe(ctx context.Context, tenantID uuid.UUID, query string) float64 {
|
||||
var avg float64
|
||||
err := s.pool.QueryRow(ctx, query, tenantID).Scan(&avg)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return avg
|
||||
}
|
||||
Reference in New Issue
Block a user