# A membership role

Use a role when people get access by belonging to something, such as the
editors of a company. A role is stored in a record of its own: a
membership entity, whose `membership:` section names three of its fields:

- `scope`: the record the role is held in, such as the company;
- `member`: who holds it, a reference to the user entity;
- `role`: the field that holds the role, typed with an enum.

The role enum lives beside the membership in the same domain, and other
domains use it by its full name. There is no global roles file.

```jadpo
// domains/accounts/types/company_role.jadpo
type CompanyRole = Enum {
    owner
    editor
    viewer
}

// domains/accounts/entities/company_membership.jadpo
entity CompanyMembership {
    id: Uuid
    company_id: Company.id {
        on_delete: OnDelete.cascade
    }
    user_id: User.id {
        on_delete: OnDelete.cascade
    }
    role: CompanyRole

    membership: {
        scope: company_id
        member: user_id
        role: role
    }

    policy: {
        create: [CompanyRole.owner]
        read: [CompanyRole.owner, user_id]
        update: [CompanyRole.owner]
        delete: [CompanyRole.owner, user_id]
    }
}
```

Here only a company's owners may add, change or remove members. Each
member may also read their own membership, and delete it to leave.

## Using a role on other records

A role applies to another entity through that entity's reference to the
scope, the same reference [tenancy](/docs/tenancy.md) uses. A `Project` with a
`company_id` takes its grants from the roles held in its company:

```jadpo
// domains/projects/entities/project.jadpo
entity Project {
    id: Uuid
    company_id: Company.id {
        on_delete: OnDelete.cascade
    }
    name: ProjectName

    policy: {
        create: [CompanyRole.editor]
        read: [CompanyRole.viewer, CompanyRole.editor]
        update: [CompanyRole.editor]
        delete: [CompanyRole.owner]
    }
}
```

## How roles combine

- Roles have no ranking. A role lets in only where a list names it, so an
  owner who may also edit is listed under both.
- Only current memberships count. A membership its lifecycle hides grants
  nothing, and removing one takes effect on the next request.
- A role held across the whole application, such as staff, has a
  membership with `scope: MembershipScope.application`.
- An entity can have two references to the scope, such as a handover with
  a `from_team_id` and a `to_team_id`. Then each grant names the reference
  it means: `TeamRole.lead(from_team_id)` is the lead of the from-team.

## Roles that need a way in

Staff often sign in through their company's provider, such as Google or
Microsoft, so that switching their account off there ends their access
everywhere. If they could also sign in by magic link and keep their
roles, it wouldn't. A membership's `requires:` says, for each role, how
its holder must have signed in for the role to count:

```jadpo
// domains/desk/entities/staff_member.jadpo
entity StaffMember {
    id: Uuid
    patron_id: Patron.id {
        on_delete: OnDelete.cascade
    }
    role: StaffRole

    membership: {
        scope: MembershipScope.application
        member: patron_id
        role: role
        requires: {
            StaffRole.admin: {
                sign_in: [SignInMethod.google]
                strength: AuthenticationStrength.multi_factor
            }
            StaffRole.front_desk: {
                sign_in: [SignInMethod.google]
            }
        }
    }
}
```

- `sign_in:` lists the ways in, any of which will do: a provider that
  `sign_in: { providers: }` names ([Providers](/docs/checking-the-person-sign-in.md#providers)),
  or `SignInMethod.external_token`. A magic link or a passkey proves an
  address or a device, not that the company still vouches for the
  person, so neither can be listed.
- `strength: AuthenticationStrength.multi_factor` asks for a second
  factor: a passkey, or a provider whose ID token says the person used
  one ([How strongly a person signed in](/docs/starting-and-ending-a-session.md#how-strongly-a-person-signed-in)).
- `StaffRole.front_desk: {}` needs nothing. Once one role has a line,
  every value of the enum has one, so a role added later is decided
  rather than open by default.

A role counts on a request only when the caller's current session meets
every line of its block. Otherwise the person is just themselves,
without that role: Ann, signed in by link, is a customer like any other.
Nothing is refused when she signs in. The requirement applies wherever
the role is named: grants, a field's own `policy:`, lines under
`actions:`, listed queries and what the proofs count on. You write it
once.

A membership whose role doesn't count doesn't make its holder a member
of its tenant for that request either, so tenancy fails closed
([Tenancy](/docs/tenancy.md)).

A session started through a provider that any role's requirement names
lasts a day, not 30 days, so a person the company switches off loses the
role within a day ([How a session started](/docs/starting-and-ending-a-session.md#how-a-session-started)).

The permissions report lists each role's requirement, and
`access.jadpo_record` records it. Any change to a role's requirement,
tighter or looser, and adding or removing a role's line, needs a person's
acceptance, `PolicyChange.role_requirements_changed`
([Acceptances](/docs/acceptances.md)).

What the compiler checks:

- A role or a way in the enum doesn't have is
  [`type.unknown_enum_variant`](/docs/errors/type.unknown_enum_variant.md).
- A role left out once another has a line is
  [`policy.requirements_incomplete`](/docs/errors/policy.requirements_incomplete.md).
- Anything else a role's line can't hold, an empty `sign_in:`,
  `strength: AuthenticationStrength.primary`, a way Jadpo's own sign-in
  gives, or a role given twice is
  [`policy.requirement_invalid`](/docs/errors/policy.requirement_invalid.md).
- A line no way the app declares could ever meet, such as
  `SignInMethod.external_token` without `external_token:`, is
  [`policy.requirement_unsatisfiable`](/docs/errors/policy.requirement_unsatisfiable.md).
- A route whose API can't carry the sign-in that every role letting
  anyone in needs is [`policy.effect_ungranted`](/docs/errors/policy.effect_ungranted.md),
  which names the requirement. A provider's sign-in comes as a browser
  session and `external_token`'s as its token; an API token or a service
  key carries neither.

A route test says how its user signed in with `sign_in:` and `strength:`
beside `as:`; without them, `as:` is someone who signed in by magic link
with one factor ([Signing in for a route test](/docs/acting-as-a-user.md#signing-in-for-a-route-test)).
