Membership
Membership is a package for access to one thing through the people on it: a board with its members, a case file with its team. This page explains what Membership is for, what it covers, and its classes. The tutorial then adds it to the Harbor application of the Tenancy tutorial.
What it is for
Tenancy decides what a seat may do at a unit: a permission granted at North counts at North and every unit below it. Many applications also need access to one thing:
- Rhea opens a board, Crane schedule, and adds Leo to it as an editor.
- Leo can edit that board. He gets no permissions at North, and none on other boards.
- Rhea owns the board. She decides who is on it, and nobody can remove her from it.
Membership stores the members and their roles, changes them through two managers, and checks each request against them before your handler runs. Your application keeps the board itself, its names, and which permission changes the members.
What it does
- Members: who is on a resource. Each membership starts at a set time, and ends at a set time or never.
- Roles on a resource: a member holds roles there, each for its own period. A role counts only on that resource.
- One owner: the owner is always a member. The owner stays on the resource until somebody else becomes owner.
- Rules for the member list: a person is on the list once, holds each role once, and the owner cannot be removed.
- Two managers per resource, written by the generator.
BoardMemberManageropens a board, adds and removes members, gives and takes roles, names an owner, and lists who is on a board.BoardRoleManagermakes and changes the roles a tenant keeps. Each manager checks the caller, loads, changes and saves in one call. - The check before a handler: does the caller hold this permission on this board? If not, the handler does not run.
- Lists in one query:
db.Boards.WhereIHold(BoardPermissions.View)makes the boards a caller holds a permission on a subquery of your own query. Your code does not fetch a list of ids first. - Questions in a handler:
MembershipAccessManager<Board>returns how the caller holds a permission on a board, and which permissions the caller holds on several boards. Use it for a rule of your own or for the buttons of a screen. - Reach from above, with an organization: a permission held at the unit where a board hangs, or above it, counts on the board.
- On Postgres: functions that your row level security (RLS) policies call, and a lock on the member rows.
What it does not do
- Choose the permission that changes the members: you mark one of yours, and the managers require it.
- Store the resource: the board is your aggregate, with your fields, in your context.
- Store an organization: use Tenancy, or your own. Membership also works without one.
- Write your RLS policies: your row access rules say what a caller reads and changes of a board.
- Groups, or access through another resource: a member is one id.
With Tenancy, or without
- Without Tenancy: a member is one of your users, identified by the user id in its token.
- With Tenancy: a member is a seat. A board then belongs to one tenant, and a suspended seat loses access to every board. The generator writes the code that connects the two packages into your project. Neither package references the other.
- Everything else works the same. The tutorial uses Tenancy, because Harbor has it.
How the classes connect
The classes of a board with members, and how they connect. The board (purple) is your aggregate. The two managers (green) are generated for it, and your handlers take them. The access manager is the package's service, which your handlers take too:
- a board has many members;
- a member holds roles, each for a period;
- each role is one of the tenant's board roles;
- the member manager changes the members through the board's member list;
- the role manager reads and changes the board roles, and what the generator writes makes them from your starter roles;
- the rules read your permissions;
- the access manager answers questions about the caller and boards, by the rules.
| Class | What it is | In Harbor |
|---|---|---|
| Resource | The one thing access is about: an aggregate of yours that has members. | A board: Crane schedule |
Member (BoardMember) | Someone on the resource, for a period. With Tenancy, a seat. You declare it in one line, from the package's template. | Leo's seat, on Crane schedule |
Member list (Members) | The rules for the members, over your collection: each person once, and the owner stays. The generator adds it to your aggregate. | Members.Open(now), in the board's constructor |
Member manager (BoardMemberManager) | The class your handlers change members through. It checks the caller, loads the resource, calls your hooks and the member list, and saves. The generator writes it, named after the member class. | AddAsync(board, leo, editor, until: null) |
| Hook | A partial method the generator declares on your aggregate. The member manager calls it, for a guard of yours before a change or an event of yours after one. | none: Harbor needs no hook |
Owner ([Owner]) | One per resource, and always a member. The owner holds no role. The owner holds every permission marked for the owner. | Rhea |
| Role | A named set of permissions, given to a member on one resource. | editor |
Role hold (MemberRole) | One role held by one member. It starts at a set time, and ends at a set time or never. It counts only while the membership lasts. | Leo holds editor on Crane schedule |
Custom role (BoardRole) | A role stored as a row of the tenant. The tenant can rename it and change its permissions. A starter role is one every tenant gets a copy of, and the copy follows it. | The tenant's editor, made from the starter role editor |
Role manager (BoardRoleManager) | The class your handlers make and change custom roles through. It requires the permission the rules name in managedWith. The generator writes it, named after the role class. | ListAsync(), for the roles of Harbor Works |
| Permission and its level | One thing a caller may do, as a string, with the level where the check asks for it: on one resource ([On<Board>]), at a unit ([AtUnit]), or for the whole tenant ([AtTenant]). | boards.edit on a board, boards.create at North |
Rules (MembershipRules) | One declaration per kind of resource: where the roles come from, and whether a permission held above reaches the resource. The C# check and the database functions read the same rules. | BoardMembership.Rules |
Access manager (MembershipAccessManager<Board>) | The service your handlers ask what the current caller holds on one kind of resource. Each question takes the resource, or its id. | PermissionsOnAsync([board], ...), for the buttons of a board's screen |
| Reach from above | A permission held at the unit where a resource hangs ([At]), or above it, counts on the resource for a caller who is not a member. | Ada's Administrator role at the root reaches every board |
| The permission that manages the members | The permission you mark ManagesMembers. The member manager requires it for every change to the members. A permission marked NamesOwner does the same for handing a resource on. | boards.members.manage |
| Lock | A database check: only a caller who holds the permission you mark can change a member row or the owner column. | Only a caller with boards.members.manage can write Crane schedule's member rows |
Who holds a permission on a board
A caller holds a permission on a board in five cases. One query checks all five:
| The caller | Holds | In Harbor |
|---|---|---|
| is the owner | every permission marked for the owner (all of them, except one marked Owner = false) | Rhea, on Crane schedule |
| is a member now | the one permission marked EveryMember, whatever roles the member holds | Leo sees Crane schedule |
| holds a role on the board now | what the role gives, limited to the permissions a role may give (InRoles) | Leo edits it, as editor |
| holds the permission at the board's unit, or above it | that permission, with reach from above | Ada, from the root |
| is system work in the module's scope | every permission | seed data written as system work in boards |
A caller who holds none of these does not see the board, and gets boards.not-found, the same answer as for a
board that does not exist. A caller who sees the board but lacks the permission gets boards.not-permitted.
Loading the explorer…
Reach from above: a permission held at the board's unit
With reach from above, a permission held in the organization counts on the boards below it. At the end of the tutorial, Harbor has two boards. Units are square and show the roles granted there. Boards are purple and rounded, with a dotted line to the unit where they hang, and show their owner and their members with their roles:
| Person | Sees | Why |
|---|---|---|
| Leo | Crane schedule | He is a member. He holds no permission in the organization. |
| Rhea | Crane schedule and Quay plan | She owns Crane schedule. Her boards.view at North reaches Quay plan at North Coast. |
| Ada | both boards | Her role at the root reaches every board in the tenant. She is also an editor on Crane schedule. |
What your application decides
Most choices have a default. The smallest application decides only where the roles come from.
| Choice | Default | What you can do instead |
|---|---|---|
| Where the roles come from | No default: the rules must say it. | Roles.Fixed([...]): roles in code, the same for every tenant. Roles.Custom(starters: [...]): rows the tenant keeps and changes, as in the tutorial. Roles.External(...): roles stored elsewhere, such as Tenancy's (Beside an organization). |
| Who may add a member, give a role or name an owner | Only system work, because no permission is marked for it. | ManagesMembers = true and NamesOwner = true on a permission on the board, such as the tutorial's boards.members.manage. Write further rules in the handler (Who may give a role). |
| Who may change the custom roles | Only system work. | Roles.Custom(managedWith: ...) names the permission. With Tenancy, the caller holds it for the whole tenant (The role manager). |
| What the owner holds | Every permission on the board. | Mark a permission Owner = false to keep it from the owner. |
| What every member holds | Nothing. | Mark one permission EveryMember = true, usually the one that shows the board. Reach from above needs it. |
| What a role may give | Every permission on the board. | InRoles = false keeps a permission out of every role. otherPermissions lets permissions of other modules in. |
| The lock on the member rows and the owner | Off. Any caller your row rules let change a board can write them, and the start-up check warns. | The same two marks. On Postgres they also lock the member rows and the owner column (Who writes the member rows). |
| Reach from above | Off. Only the members and the owner have access. | reachFromAbove: true, and [At] on the property that holds the board's unit. |
| How the caller's member id is found | From the member id's type: the caller's seat for Tenancy's seat id, the user id for an id over a Guid. | members: MemberSource.Claim(...) reads a claim of the token. MemberSource.Resolved(...) uses an id you resolve. |
| System work | System work in the module's scope, boards, holds every permission on every board. | systemScopes: [...] names other scopes, [] none (System work). |
| The name in refusal codes and functions | The class name in the plural: boards.not-found, boards_i_see. | [Members("...")] on the class (Names). |
| A save tied to its check | Off. The check, the load by id, the version, the board's rules and RLS protect a change. | UseMemberHolds on the context (The expert hold). |
| How the tables are mapped | By convention. A context with AddDDDToolkitConventions() maps a resource's members and a custom role class. A role class with [OfTenant] also gets a unique index on the tenant and the name, and one on the tenant and the starter role. | HasMembers(...) and IsCustomRole() in OnModelCreating, for table names of your own (Several kinds of resource). |
| With or without Tenancy | Works without: members are your users. | Add Tenancy's Entity Framework package to the project: members become seats, and the connecting code is generated (With Tenancy). |
Where to go next
- Build it: the tutorial, step by step, on the Harbor application.
- Look something up: one page per topic, with every detail:
- Resources and members: the packages, a resource without Tenancy, the member list, the managers and their hooks, names, several kinds of resource.
- Permissions and roles: the settings of a permission, who holds one, who may give a role, roles a tenant makes.
- Checks and saves: combined requirements, from the check to the save, the access manager and your own queries, system work, the expert hold.
- Postgres: the functions, the lock on the member rows, the start-up check.
- Beside an organization: with Tenancy, or with an organization of your own.
- Design and limits: why you write each piece yourself, and what Membership does not do.
- Every class and method: the API reference.
- The organization around it: Tenancy.