# Custom roles

> Named sets of permissions, assigned to people and to brand groups.

Source: https://developer.mueslimail.com/docs/roles/

Every person in an organization holds one of the three predefined roles: `owner`, `admin` or `member`. A custom role is a named set of permissions added on top of one of them, so one person can hold exactly the one thing their job needs without becoming an admin. A custom role is a set of the same permission names an [API key](https://developer.mueslimail.com/docs/authentication.md#scoped-keys) holds, and it is assigned to a person or to a brand group.

## What a request may do

The effective permissions of a request are the union of the predefined role and every custom role the person holds, directly or through a group. Nothing is cached: the roles are read with the session on every request, so a role added or removed takes effect on the next call, not at the next sign in.

```bash
curl -s -X POST https://api.mueslimail.com/v0/roles \
  -H "Authorization: Bearer ***" \
  -H "Content-Type: application/json" \
  -d '{"name": "Support lead", "description": "Reads the team screen", "permissions": ["member_read"]}'
```

## What a request can see

A custom role grants permissions, never visibility. What a person can open is decided by the grants on an inbox or on a domain, and a role does not touch them: a role listing `message_read` for somebody with no grant answers `200` with an empty list, not somebody else's mail. This is the same intersection a scoped API key is subject to, and it holds by construction rather than by a check somebody can forget.

> Note: A key never inherits a custom role. A person's key stays bounded by its own permission list, and it can only be minted with permissions its creator holds, so a custom role is the ceiling a new key starts under and never something a key gains later.

## Assigning to a person or to a group

A `PUT` replaces the whole set for one subject, so the request says what the person should hold rather than what to add: a role left out of `role_ids` is revoked. Both halves are audited, as `role.assign` and `role.revoke`.

```bash
# One person
curl -s -X PUT https://api.mueslimail.com/v0/members/USER_ID/roles \
  -H "Authorization: Bearer ***" \
  -H "Content-Type: application/json" \
  -d '{"role_ids": ["ROLE_ID"]}'

# One brand group
curl -s -X PUT https://api.mueslimail.com/v0/groups/GROUP_ID/roles \
  -H "Authorization: Bearer ***" \
  -H "Content-Type: application/json" \
  -d '{"role_ids": ["ROLE_ID"]}'
```

A group is a brand. Its members are the people who can already see mail in it: those holding a grant on one of its domains or on one of its inboxes. Membership is read from the grants rather than copied into the assignment, so granting somebody a domain of the group is what makes them a member, and withdrawing the grant takes the role away with it. Deleting the group, deleting the role, or removing the person all revoke the assignments in the same transaction, and each is audited with the count.

## Sensitive permissions are never implicit

`data_delete` and `message_release` are not effects of full access, and a custom role is not a way around that: a role that lists one of them can only be created or assigned by somebody who holds it too. The refusal is `403 permission_escalation`, the same code a key gets when it tries to issue a wider key. In practice an owner builds the erasing role and hands it out; an admin cannot.

## Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 422 | `unknown_permission` | A name in `permissions` is not one of the permission names; the message lists the valid ones. |
| 403 | `permission_escalation` | The role, or one of the roles being assigned, grants a permission the caller does not hold. |
| 409 | `already_exists` | Another role of the organization already uses that name, compared ignoring case. |
| 409 | `limit_exceeded` | The organization holds 50 custom roles, which is the limit. |
| 404 | `not_found` | The role, the member or the group is not in this organization. |

## Endpoints

### GET /v0/roles

List custom roles (requires member_read)

Responses:

* `200` OK: CustomRoleList
* `403` Error envelope: Error

### POST /v0/roles

Create a custom role (requires member_write, audited)

Body (CustomRoleRequest):

| Name | Type | Notes |
| --- | --- | --- |
| `name` | string | max length 60 |
| `description` | string | max length 200 |
| `permissions` | Permission[] | Required on create. An unknown name is 422 unknown_permission; a name the caller does not hold is 403 permission_escalation. |

Responses:

* `201` Created: CustomRole
* `400` Error envelope: Error
* `403` Error envelope: Error
* `409` Error envelope: Error
* `422` Error envelope: Error

### PATCH /v0/roles/{role}

Change a role's name, description or permission set (requires member_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `role` | string (uuid) | in path, required |

Body (CustomRoleRequest):

| Name | Type | Notes |
| --- | --- | --- |
| `name` | string | max length 60 |
| `description` | string | max length 200 |
| `permissions` | Permission[] | Required on create. An unknown name is 422 unknown_permission; a name the caller does not hold is 403 permission_escalation. |

Responses:

* `200` OK: CustomRole
* `400` Error envelope: Error
* `403` Error envelope: Error
* `404` Error envelope: Error
* `409` Error envelope: Error
* `422` Error envelope: Error

### DELETE /v0/roles/{role}

Delete a custom role (requires member_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `role` | string (uuid) | in path, required |

Responses:

* `204` Deleted, assignments revoked
* `403` Error envelope: Error
* `404` Error envelope: Error

### PUT /v0/members/{user}/roles

Replace the custom roles of a person (requires member_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `user` | string (uuid) | in path, required |

Body (RoleAssignmentRequest):

| Name | Type | Notes |
| --- | --- | --- |
| `role_ids` | string (uuid)[] | required, The whole set the subject should hold. An empty array revokes every custom role from the subject; that is a revocation, not a mistake., at most 20 items |

Responses:

* `200` OK: RoleAssignmentChange
* `400` Error envelope: Error
* `403` Error envelope: Error
* `404` Error envelope: Error

### PUT /v0/groups/{group}/roles

Replace the custom roles of a group (requires member_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `group` | string (uuid) | in path, required |

Body (RoleAssignmentRequest):

| Name | Type | Notes |
| --- | --- | --- |
| `role_ids` | string (uuid)[] | required, The whole set the subject should hold. An empty array revokes every custom role from the subject; that is a revocation, not a mistake., at most 20 items |

Responses:

* `200` OK: RoleAssignmentChange
* `400` Error envelope: Error
* `403` Error envelope: Error
* `404` Error envelope: Error

### CustomRole object

| Name | Type | Notes |
| --- | --- | --- |
| `role_id` | string (uuid) |  |
| `name` | string | 1 to 60 characters, unique in the organization ignoring case |
| `description` | string | At most 200 characters; empty when none was given |
| `permissions` | Permission[] | Names an API key could hold. A custom role that lists nothing is refused on the write path. |
| `member_count` | integer | How many people the role reaches now, counting a person once even when several of their groups carry it |
| `assignments` | RoleAssignment[] | The people and the groups that hold it |
| `created_at` | string (date-time) |  |
| `updated_at` | string (date-time) |  |

### CustomRoleRequest object

| Name | Type | Notes |
| --- | --- | --- |
| `name` | string | max length 60 |
| `description` | string | max length 200 |
| `permissions` | Permission[] | Required on create. An unknown name is 422 unknown_permission; a name the caller does not hold is 403 permission_escalation. |

### RoleAssignment object

| Name | Type | Notes |
| --- | --- | --- |
| `role_id` | string (uuid) |  |
| `user_id` | string (uuid) | Set when the role is assigned to a person |
| `email` | string (email) | The address of that person, so the console does not resolve ids |
| `group_id` | string (uuid) | Set when the role is assigned to a group |
| `group_name` | string | The brand's display name |
| `assigned_at` | string (date-time) |  |

### RoleAssignmentChange object

| Name | Type | Notes |
| --- | --- | --- |
| `assignments` | RoleAssignment[] |  |
| `added` | string (uuid)[] | Role ids this call gave |
| `removed` | string (uuid)[] | Role ids this call took away |
