package security import "testing" // Written as the answers a customer would give if the setting names were read out loud, role by role, rather // than as a loop over ranks, because a loop over ranks passes even when the ranks are wrong. func TestResolveRecordScope(t *testing.T) { // Owner and admin are organization wide at EVERY combination of settings. This is the guarantee that a // customer cannot lock themselves out of their own account. for _, staff := range []RecordScope{ScopeOwn, ScopeTeam, ScopeOrganization, "", "nonsense"} { for _, mgr := range []RecordScope{ScopeTeam, ScopeOrganization, "", "nonsense"} { for _, r := range []Role{RoleOwner, RoleAdmin} { if got := ResolveRecordScope(r, staff, mgr); got != ScopeOrganization { t.Errorf("%s resolved to %q with staff=%q manager=%q, it must always be organization wide", r, got, staff, mgr) } } } } // A manager follows the manager setting and nothing else. if got := ResolveRecordScope(RoleManager, ScopeOwn, ScopeTeam); got != ScopeTeam { t.Errorf("a manager with manager scope team resolved to %q", got) } if got := ResolveRecordScope(RoleManager, ScopeOwn, ScopeOrganization); got != ScopeOrganization { t.Errorf("a manager with manager scope organization resolved to %q", got) } // A manager is NEVER narrowed to their own records, because a manager who cannot see their team's work // cannot run a desk. Even an explicit "own" is refused at that tier. if got := ResolveRecordScope(RoleManager, ScopeOwn, ScopeOwn); got != ScopeTeam { t.Errorf("a manager was narrowed to %q, own is not offered at the manager tier", got) } // Everyone below manager follows the staff setting. for _, r := range []Role{RoleRecruiter, RoleMember, RoleViewer} { for _, want := range []RecordScope{ScopeOwn, ScopeTeam, ScopeOrganization} { if got := ResolveRecordScope(r, want, ScopeOrganization); got != want { t.Errorf("%s with staff scope %q resolved to %q", r, want, got) } } } } // The default settings must reproduce today's behaviour exactly. A migration that narrowed anybody's view on // the morning it shipped would be an incident, not a feature. func TestDefaultSettingsChangeNothing(t *testing.T) { for _, r := range []Role{RoleOwner, RoleAdmin, RoleManager, RoleRecruiter, RoleMember, RoleViewer} { if got := ResolveRecordScope(r, ScopeOrganization, ScopeOrganization); got != ScopeOrganization { t.Errorf("%s sees %q under the shipped defaults, everyone must still see everything", r, got) } if !ResolveRecordScope(r, ScopeOrganization, ScopeOrganization).Unrestricted() { t.Errorf("%s is restricted under the shipped defaults", r) } } } // A corrupt or unknown value must narrow, never widen. This is the direction that makes a bad setting a // complaint instead of a disclosure. func TestUnknownValuesFailNarrow(t *testing.T) { for _, bad := range []RecordScope{"", "all", "everything", "ORGANIZATION", "Own", "global"} { if got := ResolveRecordScope(RoleRecruiter, bad, ScopeOrganization); got != ScopeOwn { t.Errorf("a recruiter with staff scope %q resolved to %q, an unknown value must narrow to own", bad, got) } if got := ResolveRecordScope(RoleManager, ScopeOrganization, bad); got != ScopeTeam { t.Errorf("a manager with manager scope %q resolved to %q, an unknown value must narrow to team", bad, got) } } // An unknown ROLE is the narrowest of all. It should be unreachable, and if it is ever reached the // answer must be their own records rather than the floor. if got := ResolveRecordScope("superadmin", ScopeOrganization, ScopeOrganization); got != ScopeOwn { t.Errorf("an unknown role resolved to %q, it must narrow to own", got) } if got := ResolveRecordScope("", ScopeOrganization, ScopeOrganization); got != ScopeOwn { t.Errorf("an empty role resolved to %q, it must narrow to own", got) } } func TestValidScopes(t *testing.T) { for _, s := range []RecordScope{ScopeOwn, ScopeTeam, ScopeOrganization} { if !ValidRecordScope(s) { t.Errorf("%q should be a valid staff scope", s) } } for _, s := range []RecordScope{"", "all", "ORGANIZATION"} { if ValidRecordScope(s) { t.Errorf("%q must not be a valid staff scope", s) } } // own is deliberately NOT a valid manager scope. if ValidManagerScope(ScopeOwn) { t.Error("own must not be offered at the manager tier") } for _, s := range []RecordScope{ScopeTeam, ScopeOrganization} { if !ValidManagerScope(s) { t.Errorf("%q should be a valid manager scope", s) } } } func TestUnrestricted(t *testing.T) { if !ScopeOrganization.Unrestricted() { t.Error("organization scope needs no owner predicate") } for _, s := range []RecordScope{ScopeOwn, ScopeTeam, "", "nonsense"} { if s.Unrestricted() { t.Errorf("%q must be treated as restricted", s) } } } // Scope narrows, it never widens: no combination may show records to somebody who cannot read that record // type in the first place. The capability matrix remains the outer bound. func TestScopeNeverWidensBeyondCapability(t *testing.T) { for _, r := range []Role{RoleOwner, RoleAdmin, RoleManager, RoleRecruiter, RoleMember, RoleViewer} { s := ResolveRecordScope(r, ScopeOrganization, ScopeOrganization) if s.Unrestricted() && !Has(r, CapCandidateRead) { t.Errorf("%s is unrestricted by scope but cannot read a candidate at all", r) } } }