Files
breakpilot-compliance/ai-compliance-sdk/internal/rbac/store.go
Sharang Parnerkar 13f57c4519 refactor(go): split obligations, portfolio, rbac, whistleblower handlers and stores, roadmap parser
Split 7 files exceeding the 500 LOC hard cap into 16 files, all under 500 LOC.
No exported symbols renamed; zero behavior changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 10:00:15 +02:00

280 lines
7.6 KiB
Go

package rbac
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
// Store provides database operations for RBAC entities
type Store struct {
pool *pgxpool.Pool
}
// NewStore creates a new RBAC store
func NewStore(pool *pgxpool.Pool) *Store {
return &Store{pool: pool}
}
// ============================================================================
// Tenant Operations
// ============================================================================
// CreateTenant creates a new tenant
func (s *Store) CreateTenant(ctx context.Context, tenant *Tenant) error {
tenant.ID = uuid.New()
tenant.CreatedAt = time.Now().UTC()
tenant.UpdatedAt = tenant.CreatedAt
if tenant.Status == "" {
tenant.Status = TenantStatusActive
}
if tenant.Settings == nil {
tenant.Settings = make(map[string]any)
}
settingsJSON, err := json.Marshal(tenant.Settings)
if err != nil {
return fmt.Errorf("failed to marshal settings: %w", err)
}
_, err = s.pool.Exec(ctx, `
INSERT INTO compliance_tenants (id, name, slug, settings, max_users, llm_quota_monthly, status, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
`, tenant.ID, tenant.Name, tenant.Slug, settingsJSON, tenant.MaxUsers, tenant.LLMQuotaMonthly, tenant.Status, tenant.CreatedAt, tenant.UpdatedAt)
return err
}
// GetTenant retrieves a tenant by ID
func (s *Store) GetTenant(ctx context.Context, id uuid.UUID) (*Tenant, error) {
var tenant Tenant
var settingsJSON []byte
err := s.pool.QueryRow(ctx, `
SELECT id, name, slug, settings, max_users, llm_quota_monthly, status, created_at, updated_at
FROM compliance_tenants
WHERE id = $1
`, id).Scan(
&tenant.ID, &tenant.Name, &tenant.Slug, &settingsJSON,
&tenant.MaxUsers, &tenant.LLMQuotaMonthly, &tenant.Status,
&tenant.CreatedAt, &tenant.UpdatedAt,
)
if err != nil {
return nil, err
}
if err := json.Unmarshal(settingsJSON, &tenant.Settings); err != nil {
tenant.Settings = make(map[string]any)
}
return &tenant, nil
}
// GetTenantBySlug retrieves a tenant by slug
func (s *Store) GetTenantBySlug(ctx context.Context, slug string) (*Tenant, error) {
var tenant Tenant
var settingsJSON []byte
err := s.pool.QueryRow(ctx, `
SELECT id, name, slug, settings, max_users, llm_quota_monthly, status, created_at, updated_at
FROM compliance_tenants
WHERE slug = $1
`, slug).Scan(
&tenant.ID, &tenant.Name, &tenant.Slug, &settingsJSON,
&tenant.MaxUsers, &tenant.LLMQuotaMonthly, &tenant.Status,
&tenant.CreatedAt, &tenant.UpdatedAt,
)
if err != nil {
return nil, err
}
if err := json.Unmarshal(settingsJSON, &tenant.Settings); err != nil {
tenant.Settings = make(map[string]any)
}
return &tenant, nil
}
// ListTenants lists all tenants
func (s *Store) ListTenants(ctx context.Context) ([]*Tenant, error) {
rows, err := s.pool.Query(ctx, `
SELECT id, name, slug, settings, max_users, llm_quota_monthly, status, created_at, updated_at
FROM compliance_tenants
ORDER BY name
`)
if err != nil {
return nil, err
}
defer rows.Close()
var tenants []*Tenant
for rows.Next() {
var tenant Tenant
var settingsJSON []byte
err := rows.Scan(
&tenant.ID, &tenant.Name, &tenant.Slug, &settingsJSON,
&tenant.MaxUsers, &tenant.LLMQuotaMonthly, &tenant.Status,
&tenant.CreatedAt, &tenant.UpdatedAt,
)
if err != nil {
continue
}
if err := json.Unmarshal(settingsJSON, &tenant.Settings); err != nil {
tenant.Settings = make(map[string]any)
}
tenants = append(tenants, &tenant)
}
return tenants, nil
}
// UpdateTenant updates a tenant
func (s *Store) UpdateTenant(ctx context.Context, tenant *Tenant) error {
tenant.UpdatedAt = time.Now().UTC()
settingsJSON, err := json.Marshal(tenant.Settings)
if err != nil {
return fmt.Errorf("failed to marshal settings: %w", err)
}
_, err = s.pool.Exec(ctx, `
UPDATE compliance_tenants
SET name = $2, slug = $3, settings = $4, max_users = $5, llm_quota_monthly = $6, status = $7, updated_at = $8
WHERE id = $1
`, tenant.ID, tenant.Name, tenant.Slug, settingsJSON, tenant.MaxUsers, tenant.LLMQuotaMonthly, tenant.Status, tenant.UpdatedAt)
return err
}
// ============================================================================
// Namespace Operations
// ============================================================================
// CreateNamespace creates a new namespace
func (s *Store) CreateNamespace(ctx context.Context, ns *Namespace) error {
ns.ID = uuid.New()
ns.CreatedAt = time.Now().UTC()
ns.UpdatedAt = ns.CreatedAt
if ns.IsolationLevel == "" {
ns.IsolationLevel = IsolationStrict
}
if ns.DataClassification == "" {
ns.DataClassification = ClassificationInternal
}
if ns.Metadata == nil {
ns.Metadata = make(map[string]any)
}
metadataJSON, err := json.Marshal(ns.Metadata)
if err != nil {
return fmt.Errorf("failed to marshal metadata: %w", err)
}
_, err = s.pool.Exec(ctx, `
INSERT INTO compliance_namespaces (id, tenant_id, name, slug, parent_namespace_id, isolation_level, data_classification, metadata, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
`, ns.ID, ns.TenantID, ns.Name, ns.Slug, ns.ParentNamespaceID, ns.IsolationLevel, ns.DataClassification, metadataJSON, ns.CreatedAt, ns.UpdatedAt)
return err
}
// GetNamespace retrieves a namespace by ID
func (s *Store) GetNamespace(ctx context.Context, id uuid.UUID) (*Namespace, error) {
var ns Namespace
var metadataJSON []byte
err := s.pool.QueryRow(ctx, `
SELECT id, tenant_id, name, slug, parent_namespace_id, isolation_level, data_classification, metadata, created_at, updated_at
FROM compliance_namespaces
WHERE id = $1
`, id).Scan(
&ns.ID, &ns.TenantID, &ns.Name, &ns.Slug, &ns.ParentNamespaceID,
&ns.IsolationLevel, &ns.DataClassification, &metadataJSON,
&ns.CreatedAt, &ns.UpdatedAt,
)
if err != nil {
return nil, err
}
if err := json.Unmarshal(metadataJSON, &ns.Metadata); err != nil {
ns.Metadata = make(map[string]any)
}
return &ns, nil
}
// GetNamespaceBySlug retrieves a namespace by tenant and slug
func (s *Store) GetNamespaceBySlug(ctx context.Context, tenantID uuid.UUID, slug string) (*Namespace, error) {
var ns Namespace
var metadataJSON []byte
err := s.pool.QueryRow(ctx, `
SELECT id, tenant_id, name, slug, parent_namespace_id, isolation_level, data_classification, metadata, created_at, updated_at
FROM compliance_namespaces
WHERE tenant_id = $1 AND slug = $2
`, tenantID, slug).Scan(
&ns.ID, &ns.TenantID, &ns.Name, &ns.Slug, &ns.ParentNamespaceID,
&ns.IsolationLevel, &ns.DataClassification, &metadataJSON,
&ns.CreatedAt, &ns.UpdatedAt,
)
if err != nil {
return nil, err
}
if err := json.Unmarshal(metadataJSON, &ns.Metadata); err != nil {
ns.Metadata = make(map[string]any)
}
return &ns, nil
}
// ListNamespaces lists namespaces for a tenant
func (s *Store) ListNamespaces(ctx context.Context, tenantID uuid.UUID) ([]*Namespace, error) {
rows, err := s.pool.Query(ctx, `
SELECT id, tenant_id, name, slug, parent_namespace_id, isolation_level, data_classification, metadata, created_at, updated_at
FROM compliance_namespaces
WHERE tenant_id = $1
ORDER BY name
`, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
var namespaces []*Namespace
for rows.Next() {
var ns Namespace
var metadataJSON []byte
err := rows.Scan(
&ns.ID, &ns.TenantID, &ns.Name, &ns.Slug, &ns.ParentNamespaceID,
&ns.IsolationLevel, &ns.DataClassification, &metadataJSON,
&ns.CreatedAt, &ns.UpdatedAt,
)
if err != nil {
continue
}
if err := json.Unmarshal(metadataJSON, &ns.Metadata); err != nil {
ns.Metadata = make(map[string]any)
}
namespaces = append(namespaces, &ns)
}
return namespaces, nil
}