diff options
Diffstat (limited to 'cmd/mirum-server/actor.go')
| -rw-r--r-- | cmd/mirum-server/actor.go | 252 | +0 −252 |
1 files changed, 0 insertions, 252 deletions
diff --git a/cmd/mirum-server/actor.go b/cmd/mirum-server/actor.go deleted file mode 100644 --- a/cmd/mirum-server/actor.go +++ /dev/null @@ -1,252 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Nikolay Govorov -// SPDX-License-Identifier: AGPL-3.0-or-later - -package main - -import ( - "context" - "errors" - "slices" - - "dimidiumlabs/mirum/cmd/mirum-server/apipb" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5" -) - -var ( - ErrPermissionDenied = errors.New("database: permission denied") - ErrUnauthenticated = errors.New("database: authentication required") -) - -var anonPermissions = []apipb.Perm{ - apipb.Perm_PERM_ORG_READ, -} - -var userGlobalPermissions = []apipb.Perm{ - apipb.Perm_PERM_ORG_READ, - apipb.Perm_PERM_ORG_WRITE, - apipb.Perm_PERM_USER_READ, -} - -// rolePermissions is the single source of truth for role → perm bundles. -// RLS checks only tenancy (membership); action authz lives here. -var rolePermissions = map[string][]apipb.Perm{ - "owner": { - apipb.Perm_PERM_ORG_READ, - apipb.Perm_PERM_ORG_WRITE, - apipb.Perm_PERM_ORG_DELETE, - apipb.Perm_PERM_ORG_MEMBER_READ, - apipb.Perm_PERM_ORG_MEMBER_WRITE, - apipb.Perm_PERM_WORKER_READ, - apipb.Perm_PERM_WORKER_WRITE, - }, - "admin": { - apipb.Perm_PERM_ORG_READ, - apipb.Perm_PERM_ORG_WRITE, - apipb.Perm_PERM_ORG_MEMBER_READ, - apipb.Perm_PERM_ORG_MEMBER_WRITE, - apipb.Perm_PERM_WORKER_READ, - apipb.Perm_PERM_WORKER_WRITE, - }, - "member": { - apipb.Perm_PERM_ORG_READ, - apipb.Perm_PERM_ORG_MEMBER_READ, - apipb.Perm_PERM_WORKER_READ, - }, -} - -// Actor is the principal making a database request. It carries identity, -// display metadata, and coarse capability. Zero value is invalid: dbID -// panics, so a missing initialisation cannot silently grant privileges. -// -// Synthetic actors (System/Operator/Anon) live only as Go constants — -// they are not rows in the users table, so they cannot be logged in as -// even if somebody writes a password into the DB. -// -// Authorization is divided into two planes: -// - Tenancy: an actor can only see a subset of resources to which -// they have access (public or through organization membership). -// Any select statement will return only records accessible to the actor. -// - RBAC: what the actor can do with records (create/read/write) is implemented here. -// Any rights we grant here are a strict subset of the Tenancy rights. -// The list of perms can be either explicit (for tokens) or implied (for user roles). -type Actor struct { - kind actorKind - id uuid.UUID - email string - superuser bool -} - -type actorKind uint8 - -const ( - actorInvalid actorKind = iota - actorUser - actorOperator - actorSystem - actorAnon -) - -// ActorKind is the exported form of actorKind for audit sinks and logging. -type ActorKind uint8 - -const ( - KindInvalid ActorKind = iota - KindUser - KindOperator - KindSystem - KindAnon -) - -var ( - anonUUID = uuid.MustParse("ffffffff-ffff-ffff-ffff-ffffffffffff") - systemUUID = uuid.MustParse("00000000-0000-0000-0000-000000000001") - operatorUUID = uuid.MustParse("00000000-0000-0000-0000-000000000002") -) - -// UserActor identifies an authenticated user from a session or token. -func UserActor(id UserID, email string, superuser bool) Actor { - if id.IsZero() { - panic("database: UserActor with nil UUID") - } - if email == "" { - panic("database: UserActor with empty email") - } - return Actor{kind: actorUser, id: id.UUID(), email: email, superuser: superuser} -} - -// OperatorActor is the principal for externally invoked privileged -// operations (admin socket). Distinguishable from System in audit logs. -func OperatorActor() Actor { - return Actor{kind: actorOperator, id: operatorUUID, email: "operator@mirum.local", superuser: true} -} - -// SystemActor is the principal for internal machinery (mTLS handshake, -// session bootstrap, background jobs). Not an operator action. -func SystemActor() Actor { - return Actor{kind: actorSystem, id: systemUUID, email: "system@mirum.local", superuser: true} -} - -// AnonActor is the principal for unauthenticated public requests. -func AnonActor() Actor { - return Actor{kind: actorAnon, id: anonUUID, email: "anonymous@mirum.local"} -} - -func (a Actor) Kind() ActorKind { - switch a.kind { - case actorUser: - return KindUser - case actorOperator: - return KindOperator - case actorSystem: - return KindSystem - case actorAnon: - return KindAnon - } - return KindInvalid -} - -func (a Actor) UserID() UserID { return UserID(a.id) } -func (a Actor) Email() string { return a.email } -func (a Actor) IsSuperuser() bool { return a.superuser } - -// dbID returns the UUID to write into app.user_id. Panics on zero value. -func (a Actor) dbID() uuid.UUID { - if a.id == uuid.Nil { - panic("database: zero-value Actor; use UserActor/SystemActor/OperatorActor/AnonActor") - } - return a.id -} - -// kindString returns the string written into app.actor_kind. -// It must match the values tested by app_issuper() in the SQL migration. -func (a Actor) kindString() string { - switch a.kind { - case actorUser: - return "user" - case actorOperator: - return "operator" - case actorSystem: - return "system" - case actorAnon: - return "anon" - } - panic("database: zero-value Actor; use UserActor/SystemActor/OperatorActor/AnonActor") -} - -// checkGlobal checks a global-scope perm (no specific org). Pure, no DB. -func checkGlobal(actor Actor, perm apipb.Perm) error { - switch actor.kind { - case actorOperator, actorSystem: - return nil - case actorAnon: - if slices.Contains(anonPermissions, perm) { - return nil - } - return ErrUnauthenticated - case actorUser: - if actor.superuser { - return nil - } - if slices.Contains(userGlobalPermissions, perm) { - return nil - } - return ErrPermissionDenied - default: - return ErrPermissionDenied - } -} - -// checkPerm checks an org-scoped perm within an existing transaction. -func checkPerm(ctx context.Context, tx pgx.Tx, actor Actor, orgID OrgID, perm apipb.Perm) error { - switch actor.kind { - case actorOperator, actorSystem: - return nil - case actorAnon: - return ErrUnauthenticated - case actorUser: - if actor.superuser { - return nil - } - var role string - err := tx.QueryRow(ctx, - `SELECT role FROM org_members WHERE org_id = $1 AND user_id = $2`, - orgID, actor.id, - ).Scan(&role) - if err != nil { - return ErrPermissionDenied - } - if !slices.Contains(rolePermissions[role], perm) { - return ErrPermissionDenied - } - return nil - default: - return ErrPermissionDenied - } -} - -// checkSystem checks that actor is the internal system principal. -func checkSystem(actor Actor) error { - if actor.kind == actorSystem { - return nil - } - return ErrPermissionDenied -} - -// checkSelf checks that actor is the target user or superuser. -func checkSelf(actor Actor, targetID UserID) error { - switch actor.kind { - case actorOperator, actorSystem: - return nil - case actorAnon: - return ErrUnauthenticated - case actorUser: - if actor.superuser || targetID == actor.UserID() { - return nil - } - return ErrPermissionDenied - default: - return ErrPermissionDenied - } -} |
