Files
certctl/internal/domain/audit.go
T
shankar0123 19497eef87 auth-bundle-1 Phase 1: RBAC schema + domain types + repository layer
Bundle 1 / Phase 1: ships the RBAC primitive as schema + domain types + repo layer. Service-layer wiring lands in Phase 2; middleware integration in Phase 3.

Schema (migrations/000029_rbac.up.sql, 272 lines, idempotent, transaction-wrapped):

tenants, roles, permissions, role_permissions, actor_roles. TEXT primary keys with prefixes (t-, r-, p-, ar-) per CLAUDE.md Architecture Decisions. TIMESTAMPTZ time columns. FK cascade explicit (tenant CASCADE, role RESTRICT, actor CASCADE). Three-value scope_type CHECK ('global', 'profile', 'issuer') matched 1:1 with internal/domain/auth.ScopeType. UNIQUE(tenant_id, name) on roles, UNIQUE(name) on permissions, UNIQUE(actor_id, actor_type, role_id, tenant_id) on actor_roles.

Seeds: t-default tenant, 7 default roles (admin, operator, viewer, agent, mcp, cli, auditor), 33-permission canonical catalogue (cert.* / profile.* / issuer.* / target.* / agent.* / audit.* / auth.role.* / auth.key.* / auth.bootstrap.use), full role->permission grant matrix at global scope. Demo-mode preservation: actor-demo-anon seeded with admin role unconditionally; Phase 3 wires the auth middleware to inject this actor into the context when CERTCTL_AUTH_TYPE=none. Reserved system actor; Phase 4 API rejects mutations / deletions targeting it with 409 Conflict.

Domain types (internal/domain/auth/{types,validate,validate_test}.go):

Tenant, Role, Permission, RolePermission, ActorRole structs with JSON tags. ScopeType enum (global/profile/issuer). ActorTypeValue mirrors internal/domain.ActorType to avoid an import cycle. CanonicalPermissions slice + DefaultRoles map are the single source of truth referenced by the migration; validate_test.go pins (a) no duplicate permissions, (b) every default-role permission is canonical, (c) admin holds the full catalogue, (d) seeded IDs carry the prefix convention, (e) ScopeType enum has exactly 3 values matching the CHECK constraint.

Extended internal/domain/audit.go: added ActorTypeAPIKey + ActorTypeAnonymous to the existing User/System/Agent enum so the audit trail can distinguish API-key requests from federated humans (Bundle 2 OIDC) and demo-mode (CERTCTL_AUTH_TYPE=none). Existing code that records actor_type=User keeps working; new APIKey value used by Bundle 1 Phase 3 middleware.

Repository layer (internal/repository/auth.go + internal/repository/postgres/auth.go):

TenantRepository (Get, List, EnsureDefault). RoleRepository (Get, GetByName, List, Create, Update, Delete with ErrAuthRoleInUse on FK RESTRICT, ListPermissions, AddPermission idempotent, RemovePermission). PermissionRepository (List, GetByName, IsCanonical for fail-fast catalog check). ActorRoleRepository (ListByActor, ListByRole, Grant idempotent, Revoke, EffectivePermissions which is the JOIN that auth.RequirePermission will use in Phase 3 — returns deduplicated (permission, scope) triples honouring the not-yet-expired predicate so future time-bound grants work without code change). Sentinel errors ErrAuthNotFound, ErrAuthDuplicateName, ErrAuthRoleInUse, ErrAuthReservedActor, ErrAuthUnknownPermission for handler-layer 404/409/400 mapping.

Verification: gofmt clean, go vet ./... clean, go test -short ./internal/domain/auth ./internal/repository/postgres pass. Integration tests against a live Postgres are gated by testing.Short() per repo convention; Phase 12 wires the testcontainers harness for full e2e coverage.

Branch: dev/auth-bundle-1. Phase 0 was 99a012e (extract internal/auth/). Phase 2 (service layer) is the next bundle.
2026-05-09 16:00:08 +00:00

55 lines
2.1 KiB
Go

package domain
import (
"encoding/json"
"time"
)
// AuditEvent records an action taken in the control plane.
type AuditEvent struct {
ID string `json:"id"`
Actor string `json:"actor"`
ActorType ActorType `json:"actor_type"`
Action string `json:"action"`
ResourceType string `json:"resource_type"`
ResourceID string `json:"resource_id"`
Details json.RawMessage `json:"details"`
Timestamp time.Time `json:"timestamp"`
}
// ActorType represents the entity performing an action.
type ActorType string
const (
// ActorTypeUser represents a federated human identity. Reserved by
// Bundle 2 (OIDC + sessions) for OIDC-authenticated humans. Bundle 1
// continues to set this for legacy callers; new code should use
// ActorTypeAPIKey for API-key-authenticated requests.
ActorTypeUser ActorType = "User"
// ActorTypeSystem represents background workers (scheduler loops, GC
// sweepers, migrations). System actors don't have a credential; the
// scheduler / startup code passes them directly to AuditService.
ActorTypeSystem ActorType = "System"
// ActorTypeAgent represents a certctl-agent identity. Agents poll the
// control plane outbound; the matched API key carries this actor type
// when the operator scopes the key to the agent role (Bundle 1
// Phase 1 ships the agent role with cert.read + agent.heartbeat +
// agent.job.* permissions).
ActorTypeAgent ActorType = "Agent"
// ActorTypeAPIKey represents an API-key-authenticated request whose
// scope was not narrowed to agent-only. Bundle 1 Phase 1 introduces
// this so the audit trail can distinguish a human-operator API key
// from a federated OIDC user (Bundle 2). System actors and agents
// keep their existing types.
ActorTypeAPIKey ActorType = "APIKey"
// ActorTypeAnonymous represents the synthetic actor used when
// CERTCTL_AUTH_TYPE=none is configured (the demo path). The audit
// row records "actor-demo-anon" with this type so operators can
// filter demo activity from real auth in audit reports.
ActorTypeAnonymous ActorType = "Anonymous"
)