Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/platform/security/recordscope.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/platform/security/recordscope.go |
| Original SHA-256 | 0eb87a48cd76268020f82e5c1e3c3f79fc946930f35fe37d44c4e4115b977db9 |
| Git blob SHA | 2c89bd5997fe7ad5d4ba83330279b71654ad5008 |
| Reading-copy SHA-256 | 0eb87a48cd76268020f82e5c1e3c3f79fc946930f35fe37d44c4e4115b977db9 |
Recovered source transcript
Download complete source text ↓82 lines · 3.6 KB
package security
// RECORD SCOPE, the third access axis.
//
// A capability says what actions you may take. Field sensitivity says which fields you may see on a record
// you can already read. Record scope says WHICH RECORDS you may read at all: only your own, your team's, or
// the whole organization. Every established product in this market has it, and this one did not.
//
// LIKE SENSITIVITY, IT ONLY EVER NARROWS. Scope cannot grant a record to somebody whose capability did not
// already allow reading that kind of record, and the tenant boundary is unaffected: row level security still
// decides which organization's rows exist at all, and this decides which of the caller's own organization's
// rows they are shown. A scope bug is a visibility bug inside one customer, never a cross tenant one.
//
// PURE, NO DATABASE, NO CONTEXT. Every combination of role and setting is provable in a unit test rather than
// only reachable by logging in as four different people.
// RecordScope is how wide a caller's view of records is.
type RecordScope string
const (
// ScopeOwn is records this person owns, plus unowned records when the organization allows that.
ScopeOwn RecordScope = "own"
// ScopeTeam is records owned by anyone sharing a team with this person. A person on no team sees their
// own, which is why the peer resolver includes the caller.
ScopeTeam RecordScope = "team"
// ScopeOrganization is everything in the tenant. This is the DEFAULT, because it is the behaviour that
// existed before scoping, and a change that silently hid records from people who could see them
// yesterday would be an incident rather than a feature.
ScopeOrganization RecordScope = "organization"
)
func ValidRecordScope(s RecordScope) bool {
switch s {
case ScopeOwn, ScopeTeam, ScopeOrganization:
return true
}
return false
}
// ValidManagerScope is the narrower set a manager may be held to. A manager restricted to their OWN records
// could not run a desk, so "own" is not offered at that tier: the setting exists to decide whether a manager
// sees their team or the whole floor.
func ValidManagerScope(s RecordScope) bool {
return s == ScopeTeam || s == ScopeOrganization
}
// ResolveRecordScope is the whole rule.
//
// OWNER AND ADMIN ARE ALWAYS ORGANIZATION WIDE, and that is deliberately not configurable. Somebody has to be
// able to see everything in an account, and it is the people accountable for it. Letting an administrator
// scope themselves down is how a customer locks themselves out of their own data and opens a support ticket
// nobody can close from inside the product.
//
// AN UNRECOGNISED SETTING FALLS BACK TO THE NARROWEST scope, not the widest. If a value is corrupt, partially
// migrated, or from a newer version of this software, the failure is that somebody sees too little and says
// so, not that everybody sees everything and nobody notices.
func ResolveRecordScope(role Role, staff, manager RecordScope) RecordScope {
rank, known := roleRank[role]
if !known {
return ScopeOwn
}
switch {
case rank >= roleRank[RoleAdmin]:
return ScopeOrganization
case rank >= roleRank[RoleManager]:
if !ValidManagerScope(manager) {
return ScopeTeam
}
return manager
default:
if !ValidRecordScope(staff) {
return ScopeOwn
}
return staff
}
}
// Unrestricted reports whether a scope needs no owner predicate at all. Callers use it to skip building a
// list of user identifiers when the answer is "everything", which is both faster and clearer at the call site
// than passing a sentinel around.
func (s RecordScope) Unrestricted() bool { return s == ScopeOrganization }