# RoleChoice.HeldIn — where a role may be held: across the app, inside an organisation, or either

> Each role an app offers through its pages says where it may be held: across the whole app (a `RoleGrant`), only inside an organisation (the role a membership holds there), or either — the default. The invitation form and the user manager offer a role only where it may be held, and refuse to give it anywhere else, so a team's "Owner" is never handed out as an app-wide role.

<!-- id: security-roles-held-in · area: security · stability: preview · html: https://osysharp.com/reference/security/roles-held-in/ -->

## Summary        {#summary}

An app hands its roles out in two places. A **`RoleGrant`** holds a role across the whole app — an agent works every
ticket. A **membership's role** holds only inside one organisation ([[security-organizations-kit#roles]]) — a company
admin looks after that company's people and nobody else's. Some roles only make sense in one of the two, so each offered
role says which:

| `HeldIn` | the role may be held | offered and given by the kits |
|---|---|---|
| `RoleScope.Anywhere` (default) | across the app, or inside an organisation | in both places |
| `RoleScope.App` | across the app only — a `RoleGrant` | on an app-wide invitation, and as a person's app role |
| `RoleScope.Organization` | only inside an organisation | on an invitation into one, and as a role in one |

```osy title="a desk whose company admins exist only inside their company" test app=security-roles-held-in
app Desk {
  use Osysharp.Ui;
  use Osysharp.Workflow;
  use Osysharp.Identity@0;
  use Osysharp.Mail@0;
  use Osysharp.EmailTemplates@0;
  use Osysharp.UserAccounts@1;
  use Osysharp.Organizations@0;
  use Osysharp.Invitations@2;
  model "model/**/*.osy";
}

using Osysharp.Identity;
using Osysharp.UserAccounts;
using Osysharp.Organizations;
using Osysharp.Invitations;

[Role] enum DeskRole { Authenticator, Agent, Admin, CompanyAdmin }
policy IsAdmin => RoleGrant.Any(g => g.Member == user && g.Role == DeskRole.Admin);
policy MayInvite => IsAdmin;
policy ManagesOrganizations => IsAdmin;

app.Accounts = new AccountsSetup { AppName = "Desk", SignupOpen = false, FirstAccountRole = DeskRole.Admin };
app.Roles = new RolesSetup { Offered = [
  new RoleChoice { Role = DeskRole.Agent, Label = "Agent", Description = "Works tickets.", HeldIn = RoleScope.App },
  new RoleChoice { Role = DeskRole.Admin, Label = "Admin", Description = "Runs the desk.", HeldIn = RoleScope.App },
  new RoleChoice { Role = DeskRole.CompanyAdmin, Label = "Company admin",
                   Description = "Looks after their company's people.", HeldIn = RoleScope.Organization },
] };

entity Company : Organization {
  security {
    allow read, create, update when IsAdmin;
    allow read when IsAuthenticator;
    allow read when IsAuthenticated where IsMemberOf(this);
  }
}

// An app-wide invitation: the form offers Agent and Admin, never Company admin.
[Route("/people")] [Title("People")]
component PeoplePage() {
  render { Stack(gap: 5, p: 5, canSee: MayInvite) { Card("Invite someone") { InviteForm(canSend: MayInvite); } } }
}

// An invitation into one company: the form offers Company admin (and nothing held only across the app).
[Route("/companies/{id}")] [Title("Company")]
component CompanyPage(Guid id) {
  live var company = Company.FirstOrDefault(c => c.Id == id);
  render {
    if (company != null) {
      Stack(gap: 5, p: 5, canSee: MayInvite) { Card("Invite to " + company.Name) { InviteForm(into: company, canSend: MayInvite); } }
    }
  }
}

Invitation InviteCompanyAdmin(string email, Company company) {
  return Invite(email, DeskRole.CompanyAdmin, organization: company);   // accepted: held inside an organisation
}
```

## Signature      {#signature}

```osy syntax
public class RoleChoice { role? Role; string Label; string Description; RoleScope HeldIn = RoleScope.Anywhere; bool Grantable = true; }
public enum RoleScope { Anywhere, App, Organization }
List<RoleOption> OfferedRoles(RoleScope where = RoleScope.Anywhere);
bool IsOfferedRole(role r, RoleScope where = RoleScope.Anywhere);
```

## Description    {#description}

### Where is each role offered?   {#offered}

`OfferedRoles(where)` answers the roles that may be held **there**, in the app's order: `RoleScope.App` gives every
offered role whose `HeldIn` is `App` or `Anywhere`, `RoleScope.Organization` every one whose `HeldIn` is `Organization`
or `Anywhere`. With no argument — `RoleScope.Anywhere` — it lists every offered role. `IsOfferedRole(r, where)` asks the
same of one role. A role listed `Grantable = false` is in neither: it is only named — `RoleLabel(r)` — where somebody holds it.

A role not listed in `app.Roles` at all is offered nowhere, whatever it is — the sign-in flow's `Authenticator`, an
integration's role ([[security-identity-kit#offered]]).

### What do the kits do with it?   {#kits}

- **The invitation form** ([The Invitations kit — invite an address with a role, answered only by that address](https://osysharp.com/reference/security/invitations-kit/)) offers the roles held where the invitation leads:
  `InviteForm()` the app-wide ones, `InviteForm(into: company)` the ones held inside an organisation.
- **`Invite` refuses a role sent to the wrong place.** `Invite(email, DeskRole.CompanyAdmin)` with no organisation is
  refused with *"That role is held only inside an organisation — invite them to the organisation."*; an app-wide role
  sent with `organization:` is refused with *"That role is held across the app, not inside an organisation."*
- **The user manager** ([The UserManagement kit — the user manager: who is in the app, what each may do, and who changed it](https://osysharp.com/reference/security/people-kit/)) lists only app-wide roles as a person's app roles, and a new
  integration is given one of those. `GrantRole(person, DeskRole.CompanyAdmin, …)` is refused with *"That role is held
  only inside an organisation — give it from the organisation's people."*, and `GrantInOrganization(company, person,
  DeskRole.Agent, …)` with *"That role is held across the app, not inside an organisation."*

Both refusals come from the function, not the page, so an API call or a test is held to the same rule a form is.

### When should a role be `Anywhere`?   {#anywhere}

When it genuinely means the same thing in both places — a "Viewer" that may read the whole app, or one company's
records. Choosing `App` or `Organization` is what stops a person being given, in the wrong place, a role that carries
more than it should there: a company admin promoted across the app would look after every company's people.
`HeldIn` says where the kits may hand a role OUT; what a held role lets its holder do is still the app's own rules —
`RoleGrant.Any(…)` for an app role, `HoldsRoleIn(company, …)` for one held in an organisation.

### What does sign-up give a stranger?   {#signup}

`SignupRole` ([The UserAccounts kit — sign-in done properly, once](https://osysharp.com/reference/security/accounts-kit/)) is a `RoleGrant` — a role held **across the whole app** — and it is given to
everyone who fills in the sign-up form. So it must be a role that is safe in a stranger's hands: a customer, a member,
a reader. An organisation's owner is not one. Handed out at sign-up, an app-wide `Owner` that answers
`ManagesPeople`, `ManagesOrganizations` or `MayInvite`, or that writes the role grants or the organisations themselves,
lets anyone who signs up manage every organisation in the app.

The shape that keeps an owner to their own organisation is the one above: offer the role with
`HeldIn = RoleScope.Organization`, grant it per organisation, and ask for it in a `where` —
`allow update where HoldsRoleIn(this, AppRole.Owner);` on the organisation, and
`where HoldsRoleIn(Company, AppRole.Owner)` (or `IsMemberOf(Company)`) on every row that belongs to one. The app's own
staff can hold an app-wide role — as long as nobody gets it by signing up (`FirstAccountRole`, an invitation, or
`osy user add … --role Admin`).

Two MUST rules in `osy lint` (and printed by `osy validate`) say when this is wrong, and `osy explain` opens with the
same sentence:

- `security-signup-role-administers-everything` — sign-up is open and the role it gives satisfies a policy that manages
  people, organisations, invitations or role grants across the app: *"Anyone who signs up gets `Owner`, which manages
  every organisation in the app."*
- `security-tenant-rows-open-to-every-account` — a row that belongs to an organisation is granted, with no `where`, to
  a caller every account can be: signed in, holding the sign-up role, or a member of *some* organisation.

## See also       {#see-also}

- [The Identity kit — who signs in, and which of the app's roles they hold](https://osysharp.com/reference/security/identity-kit/) — `app.Roles`, `RoleChoice`, and the roles the app offers
- [The Invitations kit — invite an address with a role, answered only by that address](https://osysharp.com/reference/security/invitations-kit/) — the invite form and `Invite`, which offer and send a role only where it may be held
- [The UserManagement kit — the user manager: who is in the app, what each may do, and who changed it](https://osysharp.com/reference/security/people-kit/) — the user manager, which gives a role only where it may be held
- [The Organizations kit — companies, teams, and roles held within one](https://osysharp.com/reference/security/organizations-kit/) — memberships, and the role a membership holds inside one organisation
- [role grants (and the first admin)](https://osysharp.com/reference/security/role-grants/) — how an app-wide role is held
- [The UserAccounts kit — sign-in done properly, once](https://osysharp.com/reference/security/accounts-kit/) — `SignupRole` and `FirstAccountRole`, the roles a new account is given
