mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 14:01:36 +00:00
19497eef87
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.
55 lines
2.1 KiB
Go
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"
|
|
)
|