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)