# SCIM provisioning

With SCIM, your identity provider creates, updates and deactivates the members of your
organization, and pushes groups. Okta, Microsoft Entra ID and any SCIM 2.0 client work. A group
mapping turns group membership into a workspace role, an org role and a guardrail.

In the console, open Team > Single sign-on for the SCIM base URL and token.

## Connect your identity provider

| Setting | Value |
|---|---|
| SCIM base URL | `https://demo-api.routerplus.com/scim/v2` |
| Authentication | HTTP header, Bearer token |
| Token | A management key of the organization (`tm_mk_...`) |
| Unique identifier for users | `userName` (the email address) |
| Supported actions | Create users, update user attributes, deactivate users, push groups |

- Create a management key with `POST /v1/management-keys`. See the
  [management API](api-management.md). Use the key for this connection only. To stop
  provisioning, disable the key.
- The token selects the organization. A user or group id of another organization returns `404`.
- A missing, wrong, disabled or expired token returns `401`.
- In Okta, set the user name format to the email address.
- In Entra ID, map `userPrincipalName` or `mail` to `userName`.

## Users

A SCIM user is a member of the organization.

| Request | What it does |
|---|---|
| `POST /Users` | Adds the member with the org role `viewer`, or links the existing member with that email. Returns `201`. |
| `GET /Users?filter=userName eq "a@b.com"` | Finds a user. `startIndex` (from 1) and `count` (at most 200) page through the list. |
| `GET /Users/:id` | Reads one user. |
| `PUT /Users/:id` | Replaces `externalId`, `displayName`, `name` and `active`. If you leave out `active`, it keeps its value. |
| `PATCH /Users/:id` | `add`, `replace` or `remove` operations, with or without a `path`. |
| `DELETE /Users/:id` | Deactivates the member and removes it from every group. Returns `204`. |

Rules:

- `userName` is the member's email. RouterPlus lowercases it and removes a `+tag`. You
  cannot change `userName`. For a new email, create a new user.
- `active: false` disables the member. The member loses access to the organization. Keys bound
  to the member stop working.
- After `DELETE`, the user id returns `404`. RouterPlus keeps the member's history. A later
  `POST` with the same email enables the member again, with a new id.
- SCIM can link the org owner. It never deactivates or deletes the owner, and never changes the
  owner's role. Those requests return `400` with `scimType: "mutability"`.
- `active` accepts `true`, `false`, `"True"` and `"False"`.
- RouterPlus accepts and ignores attributes other than `userName`, `externalId`, `displayName`,
  `name`, `active` and `emails`. `emails` must match `userName`.

## Groups

| Request | What it does |
|---|---|
| `POST /Groups` | `{displayName, externalId?, members?: [{value: <user id>}]}`. Returns `201`. |
| `GET /Groups?filter=displayName eq "Engineering"` | Finds a group. `excludedAttributes=members` leaves out the members. |
| `GET /Groups/:id` | Reads one group. |
| `PUT /Groups/:id` | Replaces `displayName`, `externalId` and the member list. |
| `PATCH /Groups/:id` | Adds or removes members, or replaces `displayName`. |
| `DELETE /Groups/:id` | Deletes the group and its mapping. Returns `204`. |

These PATCH forms work:

```json
{"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
 "Operations": [
   {"op": "add", "path": "members", "value": [{"value": "<user id>"}]},
   {"op": "remove", "path": "members[value eq \"<user id>\"]"},
   {"op": "remove", "path": "members", "value": [{"value": "<user id>"}]},
   {"op": "replace", "value": {"displayName": "Platform"}}
 ]}
```

Each member must be a user of the same organization. Otherwise the request returns `400` with
`scimType: "invalidValue"`.

## Filters

Filters use `eq`, joined by `and`.

| Resource | Attributes |
|---|---|
| Users | `userName`, `externalId`, `id`, `displayName`, `active`, `emails.value`, `emails[type eq "work"].value` |
| Groups | `displayName` (case is ignored), `externalId`, `id`, `members[value eq "<user id>"]` |

Other operators, `or` and `not` return `400` with `scimType: "invalidFilter"`.

## Group mappings

A mapping gives each member of a group these values:

- `workspace_id` and `workspace_role` (`admin`, `member` or `viewer`): the member's role in
  that workspace. Give both or neither.
- `org_role` (`admin` or `viewer`): the member's org role.
- `guardrail_id`: a guardrail assigned to the member.

A mapping needs at least one of the three.

| Route | What it does |
|---|---|
| `GET /v1/scim/groups` | Lists the SCIM groups with their member count and mapping. |
| `GET /v1/scim/groups/:id/mapping` | Reads a group's mapping. Returns `404` when the group has none. |
| `PUT /v1/scim/groups/:id/mapping` | `{workspace_id?, workspace_role?, org_role?, guardrail_id?}`. Sets the mapping and applies it to the members. |
| `DELETE /v1/scim/groups/:id/mapping` | Removes the mapping and applies the change to the members. |

These routes use [management API](api-management.md) authentication. They are served
under `/v1/...` and `/api/v1/...`.

How mappings apply:

- Every change applies immediately. Changes include a member that joins or leaves a group, a
  changed mapping, and a deleted group or user.
- For a member of several mapped groups, the strongest workspace role wins in each workspace.
  For the org role, `admin` wins over `viewer`.
- When several of a member's groups name a guardrail, the group with the lowest id wins. Ids
  compare as lowercase text.
- When no group grants a value any more, the member gets back the value from before SCIM set it.
  If someone changed the value by hand after that, SCIM does not change it.
- A member that SCIM added has the org role `viewer` when no group grants an org role.
- A mapping never changes the org owner.
- A deleted guardrail never applies.

## Sign-in audit

`GET /v1/audit/sign-ins` lists the sign-ins and sign-outs of the organization's members,
newest first.

| Query | What it does |
|---|---|
| `since`, `until` | ISO 8601 times. `since` is inclusive, `until` is exclusive. |
| `email` | Only this member. |
| `limit` | 1 to 500. Default 100. |

Each row is `{at, email, event, source}`.

| `event` | Meaning |
|---|---|
| `sign_in` | The member signed in to the console. |
| `sign_out` | The member signed out. |
| `session_revoked` | The session ended for another reason, for example the identity provider ended it. |

| `source` | Meaning |
|---|---|
| `clerk` | Single sign-on. All three events are recorded. |
| `email_link` | An email sign-in link. Only `sign_in` is recorded. |

Each event has one row for each organization that the person is a member of. Disabled
memberships are included.

## Errors

Errors use the SCIM error form, with `content-type: application/scim+json`:

```json
{"schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"], "status": "409", "scimType": "uniqueness", "detail": "a user with this userName exists"}
```

## Discovery

`GET /ServiceProviderConfig`, `GET /ResourceTypes` and `GET /Schemas` need no token.
RouterPlus supports PATCH and filters. It does not support bulk, sort, ETags or password
changes.
