NextMyDay/internal/core/server/roles.go
Claus Lohmar d11e14295b feat(core): add the store, identity plumbing and the door (SPEC-003/004)
- store: SQLite (WAL, foreign keys ON, CGO-free driver) + the embedded
  migration runner; each module's migrations are name-prefixed (D-012)
- audit: the append-only audit log; the actor threads via context (R-1)
- users: the user_id/GUID/email anchors (D-013)
- config: the YAML settings file (D-035) + validation
- httpx: cookies · loopback detection · trusted-proxy XFF · CSRF · security
  headers · redaction
- middleware: correlation id · audit · rate limiting
- mail: the invite/recovery mailer seam
- server: the core DOOR — the resolution switch, the dispatch registry
  (RegisterModule), the role gates, /setup · enroll/login · recovery flows,
  the reserved `static` workflow, and the loopback-only bootstrap (D-025)
2026-10-14 10:53:51 +00:00

50 lines
1.8 KiB
Go

package server
import "git.lohmar.co.uk/cclohmar/NextMyDay/internal/core/users"
// Tiers is the D-034 RoleGate: simple tiers, ONE role per user, and the
// assigned role RIPPLES DOWN (admin > director > manager > team) — a director
// may do everything a manager and a team member may. There are no tick-boxes
// and no ACL.
//
// A function carries a MINIMUM required role. With no explicit requirement the
// floor is team, so every authenticated user is admitted — the door still
// enforces session + identity before this gate runs (SPEC-003 §4 step 5d).
//
// The role-ABOVE rules that need the *other* party (creation authority,
// recovery approval) do not live here: they cannot be decided from
// (role, module, function) alone, so the users service enforces them
// (SPEC-007 AC-1/AC-2).
type Tiers struct {
required map[string]users.Role
floor users.Role
}
// NewTiers returns the tiered gate with the default floor (team).
func NewTiers() *Tiers {
return &Tiers{required: make(map[string]users.Role), floor: users.RoleTeam}
}
// Require records the minimum role a (module, function) needs and returns the
// receiver for chaining. An unknown (module, function) keeps the floor.
func (t *Tiers) Require(module, function string, min users.Role) *Tiers {
if t.required == nil {
t.required = make(map[string]users.Role)
}
t.required[gateKey(module, function)] = min
return t
}
// Allowed implements RoleGate. The ripple-down rule is simply rank >= minimum;
// an invalid role (rank 0) never passes — fail closed.
func (t *Tiers) Allowed(role users.Role, module, function string) bool {
min, ok := t.required[gateKey(module, function)]
if !ok {
min = t.floor
}
return role.Rank() >= min.Rank()
}
func gateKey(module, function string) string { return module + "/" + function }
var _ RoleGate = (*Tiers)(nil)