# The Organizations kit — companies, teams, and roles held within one

> `Osysharp.Organizations` holds the groups people belong to — the companies an app's customers work for and the teams its own people are in: memberships that remember where they came from, roles held WITHIN one organisation (the app's own roles, scoped), and mail domains that place a person with a PROVEN address in the right company. The service a customer organisation gets — its tier, entitlements, account team and VIPs — is its add-on, Osysharp.Organizations.ServicePlans.

<!-- id: security-organizations-kit · area: security · stability: preview · html: https://osysharp.com/reference/security/organizations-kit/ -->

## Summary        {#summary}
`use Osysharp.Organizations@0;` gives an app `Organization` to derive its companies and teams from, `Membership` for a
person's place in one, `MembershipRole` for a role held there and nowhere else, and `OrganizationDomain` so that a person
whose address on `acme.com` is proven joins Acme when they sign in. The app says one thing — who looks after them:

```osy syntax
policy ManagesOrganizations => IsStaff;
```

## Signature      {#signature}
```osy syntax
policy ManagesOrganizations;                                  // the app supplies it
[Authority(Extensible)] public entity Organization {
  string Name; Organization? Parent; bool IsActive = true;
  string? ExternalId; IdentitySource Source;
}
[Authority(Extensible)] public entity Membership {             // one per person and organisation
  Organization Organization; Principal Member;
  MembershipSource Source;   // Local | Domain | Invitation | Idp | Scim
  string? ExternalId; bool IsActive = true; DateTime JoinedAt;
}
[Authority(Extensible)] public entity MembershipRole { Membership Membership; role Role; }
public entity OrganizationDomain { Organization Organization; string Domain; bool JoinsAutomatically = true; role? JoinRole; }

policy IsMemberOf(Organization o);          policy HoldsRoleIn(Organization o, role r);
Membership Join(Organization org, Principal person, MembershipSource source = MembershipSource.Local);
void Leave(Organization org, Principal person);                       // the bare step — unrecorded
MembershipRemoval RemoveMember(Organization org, Principal person);   // recorded, mailed, the app told; or leaving
public entity MembershipRemoval { Organization Organization; Principal Member; Principal RemovedBy; MembershipSource Source;
                                  DateTime At; MembershipRemovalState State; string? Refusal; }
MembershipRole GrantIn(Organization org, Principal person, role r);   void RevokeIn(Organization org, Principal person, role r);
bool HoldsIn(Principal person, Organization org, role r);            List<Organization> OrganizationsOf(Principal person);
OrganizationDomain AddDomain(Organization org, string domain, role? joinRole = null);
Organization? OrganizationByDomain(string email);   Membership? JoinByDomain(Principal person);
```

## Description    {#description}

### Companies and teams                        {#organizations}
Derive the kinds the app has — `Company : Organization` for customers, `Team : Organization` for its own staff — or use
`Organization` as it is. `Parent` makes a group of companies or a department of teams. An organisation that is not
active is kept with its people and history. A derived type states its own `security { }` grants (they are never
inherited), and `this` in a rule is the row itself, so "its own people see it" is `allow read when IsAuthenticated
where IsMemberOf(this);`.

### Memberships, ready for a directory         {#memberships}
A `Membership` is one person in one organisation, and is never re-pointed at another of either. `Source` says what made
it and `ExternalId` holds the provider's id, so single sign-on or a SCIM feed later adds, suspends (`IsActive = false`)
and removes only the memberships it made. A suspended membership holds nothing: no policy of the kit reads it.

### A role held in one organisation            {#roles}
`GrantIn(company, person, AppRole.Buyer)` makes a `MembershipRole` — a member of the app's own `[Role]` enum
([`role` — a package's name for the app's own roles](https://osysharp.com/reference/security/role-type/)), held at that company and nowhere else. Ask for it in a rule with
`HoldsRoleIn(Company, AppRole.Buyer)`.

- **It is never an app-wide role.** It is a row of its own table, never a `RoleGrant`, so no `RoleGrant.Any(…)` policy
  can read a role somebody holds at Acme as one they hold everywhere.
- **Nobody grants themself one** — not an organisation's own admin, not the app's staff — and `Authenticator`, the sign-in
  flow's role, is never one. A role row is made or removed, never edited into another.

### Mail domains                               {#domains}
`AddDomain(company, "acme.com", AppRole.Viewer)` says `acme.com` belongs to that company. `JoinByDomain(person)`, called
from the app's sign-in, places a person whose address is on it — holding the domain's role there, if it names one.

- **Only a proven address joins.** An address typed at sign-up proves nothing; a membership made by a domain is refused
  for an account whose `EmailVerifiedAt` is not set, whoever tries to make it.
- **Which domain is whose is the app's to say** — its organisation managers', never an organisation's own people's. One
  domain belongs to one organisation, and a public mail service's domain (`gmail.com`, `outlook.com`, …) is refused.
- **The sign-in flow must be able to read the organisation.** `JoinByDomain` runs as the sign-in flow, so a derived type
  needs `allow read when IsAuthenticator;` — without it the company is invisible to the flow and nobody joins (the kit
  logs a warning naming the type).

### Taking a person out, or leaving            {#removal}
`RemoveMember(company, person)` takes a person out of an organisation — or, when `person` is the caller, is them
leaving it. It records a `MembershipRemoval` (whom, by whom, when, and what kind of membership it ended), and that
record's run, in the same commit, deletes their roles there and their membership, calls the app's
`MembershipEvents.Removed` (where the app ends what it hangs on the membership — a seat, access to the organisation's
own things) and mails them who removed them and that their account remains.

```osy syntax
app.Organizations = new OrganizationsSetup {
  KeepOneOf = AppRole.Owner, KeepOneOfCalled = "owner",   // "Ada is the last owner of Acme — make someone else its owner first."
  Events = new DeskMemberships(),                         // override Removed(MembershipRemoval removal)
};
```

- **Who may do it is the removal's own `create` rule**: the app's organisation managers, the person themself, and whoever
  the app grants (`partial entity MembershipRemoval { security { allow create where HoldsRoleIn(Organization, AppRole.Owner); } }`).
  The recorded remover is always the caller.
- **Removal is not erasure.** The account and everything else the person belongs to stay. The membership is deleted, not
  suspended: `IsActive` belongs to the identity provider or directory that made a membership, so one made by single
  sign-on or SCIM is refused here, with the sentence saying to remove them there.
- **Their access ends at once** — the next request they make reads nothing the membership gave them — and a person taken
  out does not walk back in through the organisation's mail domain; an invitation, or `Join`, brings them back.
- `Leave(org, person)` is the bare step — unrecorded, nobody told — for whatever keeps memberships in step with an outside
  source.

### The service a customer gets              {#service}
The tier an organisation you serve is on, what it is entitled to, its account team and its VIP people are the add-on
`Osysharp.Organizations.ServicePlans` ([The Accounts add-on — the tier, entitlements, account team and VIPs of a customer organisation](https://osysharp.com/reference/security/organizations-accounts-kit/)), which uses this kit: derive a customer from
its `CustomerAccount` (itself an `Organization`) and keep your own teams plain organisations.

## Examples       {#examples}

### A supplier portal: buyers at their own company   {#example-portal}
```osy title="companies on the kit, a buyer role held at one company, and a mail domain" test app=security-organizations-kit
app SupplierPortal {
  use Osysharp.Identity;
  use Osysharp.Organizations;
  model "model/**/*.osy";
}

using Osysharp.Identity;
using Osysharp.Organizations;

[Role] enum PortalRole { Authenticator, Staff, Buyer, Viewer }
policy IsStaff => RoleGrant.Any(g => g.Member == user && g.Role == PortalRole.Staff);
policy ManagesOrganizations => IsStaff;

entity Person : Principal {
  [MaxLength(200)] string CodeHash = "";
  security {
    allow read when IsAuthenticated || IsAuthenticator;
    allow create when IsAuthenticator;
    allow update when IsAuthenticated || IsAuthenticator where Id == user.Id;
    allow update when IsAuthenticator;
    deny read CodeHash when !IsAuthenticator;
  }
}

entity Company : Organization {
  [MaxLength(40)] string AccountNumber = "";
  security {
    allow read, create, update when IsStaff;
    allow read when IsAuthenticator;
    allow read when IsAuthenticated where IsMemberOf(this);
  }
}

entity PurchaseOrder {
  [Required("Say which company the order is for.")] Company Company;
  [MaxLength(80)] string Reference = "";
  security {
    allow read when IsStaff;
    allow read where IsMemberOf(Company);
    allow create where HoldsRoleIn(Company, PortalRole.Buyer);
  }
}

Company OpenAccount(string name, string domain) {
  var company = new Company { Name = name };
  AddDomain(company, domain, PortalRole.Viewer);
  return company;
}

[AuthMethod]
string SignInWithCode(string email, string code) {
  var p = Person.FirstOrDefault(x => x.Email == NormalisedEmail(email));
  if (p == null || !p.IsActive) { Security.VerifyPassword(code); return ""; }
  if (!Security.VerifyPassword(code, p.CodeHash)) { return ""; }
  JoinByDomain(p);
  return Security.IssueJwt(p.Id, p.Email);
}

app.AuthBootstrap = new AuthBootstrap { Role = PortalRole.Authenticator, Login = SignInWithCode };
app.Auth = new PasswordAuth { LoginField = Email, PasswordField = CodeHash };
```

## See also       {#see-also}
- [The Accounts add-on — the tier, entitlements, account team and VIPs of a customer organisation](https://osysharp.com/reference/security/organizations-accounts-kit/) — the add-on: the tier, entitlements, account team and VIPs of a customer
- [The Identity kit — who signs in, and which of the app's roles they hold](https://osysharp.com/reference/security/identity-kit/) — `Principal` and the app's own roles
- [The Invitations kit — invite an address with a role, answered only by that address](https://osysharp.com/reference/security/invitations-kit/) — inviting a person into an organisation, with a role there
- [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 people list that shows and changes memberships
- [`role` — a package's name for the app's own roles](https://osysharp.com/reference/security/role-type/) — how a kit names the app's roles
- [entity Sub : Base](https://osysharp.com/reference/entity/inheritance/) — deriving `Company` from `Organization`, and the floor it stands on
