Skip to content

Scopes and hierarchy

A scope is where a role applies. A scope has a type and an ID: organisation:acme, branch:lagos, workspace:w42.

wardn has no built-in hierarchy. Each product declares its own scope types and how they nest:

scopes:
organisation: {}
region: { parent: organisation }
branch: { parent: region }

Above every tree sits one implicit scope: global. A role assigned at global applies everywhere.

A role assigned at a scope applies in that scope and in every scope beneath it, never above it and never sideways:

global root → super_admin (applies everywhere)
├── organisation:acme ada → org_admin (acme and all below)
│ └── region:south seun → auditor (south and its branches)
│ ├── branch:lagos bayo → teller (lagos only)
│ └── branch:ibadan
└── organisation:globex (ada has nothing here)

The policy says a branch’s parent is a region. wardn must also learn that branch lagos sits in region south. There are two ways.

Store the links (default). Call Link when your application creates or moves an entity:

err := w.Link(ctx, wardn.NewScope("branch", "lagos"), wardn.NewScope("region", "south"))

Or resolve parents from your own data, so nothing is stored twice:

wardn.Config{
ParentOf: func(ctx context.Context, s wardn.Scope) (wardn.Scope, bool, error) {
if s.Type == "branch" {
region, err := branches.RegionOf(ctx, s.ID)
return wardn.NewScope("region", region), err == nil, err
}
// ... other types
return wardn.Scope{}, false, nil
},
}

Either way, wardn checks that the parent has the type the policy declares.

To move a branch to another region later, call w.Move(ctx, branch, newRegion). Link never changes an existing parent: linking to the same parent again does nothing, and linking to a different one fails with ErrAlreadyLinked.

A scope is identified by its type and ID alone, across all tenants. If two hospitals each call a department cardiology, wardn sees one scope, department:cardiology.

  • Use IDs that are unique across tenants: your database’s primary keys or UUIDs, or composite IDs such as lagos-general/cardiology.
  • wardn refuses the dangerous case. Linking a second hospital’s cardiology would move the first hospital’s department under the second hospital’s administrators, so it fails with ErrAlreadyLinked instead.

With composite IDs, give ScopeFrom a function that builds the ID from the URL:

departmentID := func(r *http.Request) string {
return r.PathValue("hospitalID") + "/" + r.PathValue("deptID")
}
r.Use(w.ScopeFrom("department", departmentID))
  • A scope whose parent is unknown inherits only from global. If a branch was never linked, an organisation admin has no power there. This fails safe.
  • IDs are compared exactly. o1 and O1 are different scopes; wardn never normalises IDs. IDs may be up to 256 bytes and may not contain control or invisible characters, or start or end with whitespace.
  • Lookups are bounded by the depth of your scope tree, so corrupt data can never cause an endless loop.
  • A check with no scope happens at global, where only global assignments apply. That is always the most restrictive answer, never the most permissive.

Questions about this page?

Ask below, point out something wrong or unclear, or suggest an improvement. Reading needs no account; to post, sign in with GitHub.