# SCIM provisioning

> The SCIM 2.0 endpoint an identity provider calls to provision people and groups in a Probara organization, with its filters, PATCH rules, role shapes, errors and limits.

Probara serves SCIM 2.0 (RFC 7643 and RFC 7644) at `https://app.probara.net/scim/v2`. An identity provider uses it to create, update, deactivate and delete the people of one organization, and to push that organization's groups. This page is the reference for the endpoint. To connect a provider in the app, read the [SCIM provisioning guide](https://docs.probara.net/en/guides/scim-provisioning/).

SCIM is an Enterprise feature. On any other plan, every request with a valid token answers `403` with the detail `SCIM requires the Enterprise plan.`

## Quick path

1. Create a token on the SCIM page of the Workspace and copy its secret. Probara shows it once.
2. Send `Authorization: Bearer <token>` with every request to `https://app.probara.net/scim/v2`.
3. Check the connection with `GET /ServiceProviderConfig`, which answers `200` with `application/scim+json`, and with `GET /Users?count=1`.

## Base URL and authentication

| Topic              | Behavior                                                                                                                                                                                                  |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Base URL           | `https://app.probara.net/scim/v2`, the same for every organization. The token alone decides the organization. No organization id in the URL or in a header is read.                                       |
| Token              | Starts with `probara_scim_`, followed by 43 base64url characters. Created on the SCIM page of the Workspace, shown once, and valid for at most 365 days. An organization holds at most two active tokens. |
| Header             | `Authorization: Bearer <token>`. Any other scheme is refused.                                                                                                                                             |
| Scope              | A SCIM token works only on `/scim/v2`. `/api/v1` refuses it.                                                                                                                                              |
| Refusal            | `401` with `WWW-Authenticate: Bearer`. The detail is the same for a missing, unknown, revoked or expired token, so it never says which check failed.                                                      |
| Organization state | While an organization is suspended or scheduled for deletion, every write answers `403`. Reads stay open.                                                                                                 |

## Endpoints

| Method and path                                              | Answer                                                                                                                       |
| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| `GET /ServiceProviderConfig`                                 | Capabilities. Patch and filter are supported, with up to 100 results per page. Bulk, password change, sort and ETag are not. |
| `GET /ResourceTypes`, `GET /ResourceTypes/{id}`              | The User and Group resource types. Any other id answers `404`.                                                               |
| `GET /Schemas`, `GET /Schemas/{urn}`                         | The User schema, its enterprise and Probara extensions, and the Group schema. Any other URN answers `404`.                   |
| `GET /Users`, `POST /Users`                                  | List or create people. A create answers `201` with a `Location` header.                                                      |
| `GET /Users/{id}`, `PUT /Users/{id}`, `PATCH /Users/{id}`    | Read, replace or patch one person.                                                                                           |
| `DELETE /Users/{id}`                                         | Remove the person's membership, if any, then the SCIM record. Answers `204` with no body.                                    |
| `GET /Groups`, `POST /Groups`                                | List or create groups. A create answers `201` with a `Location` header.                                                      |
| `GET /Groups/{id}`, `PUT /Groups/{id}`, `PATCH /Groups/{id}` | Read, replace or patch one group.                                                                                            |
| `DELETE /Groups/{id}`                                        | Delete the group and its access. Answers `204` with no body.                                                                 |
| `/Bulk`, `/Me`, `/.search`                                   | Answer `501`.                                                                                                                |

A trailing slash names the same path. A method the path does not accept answers `405` with an `Allow` header that lists the accepted methods. Any other path answers `404`.

## Filters

| Resource | Filter                                | Matching         |
| -------- | ------------------------------------- | ---------------- |
| Users    | `userName eq "…"`                     | Case-insensitive |
| Users    | `externalId eq "…"`                   | Exact            |
| Users    | `emails[type eq "work"].value eq "…"` | Case-insensitive |
| Groups   | `displayName eq "…"`                  | Case-insensitive |
| Groups   | `externalId eq "…"`                   | Exact            |

Conditions join with `and`. Attribute names and `eq` are case-insensitive. Inside a value, `\"` stands for a quote and `\\` for a backslash. Any other filter answers `400` with `scimType` `invalidFilter`. A filter that matches nothing answers `200` with `totalResults` set to `0` and an empty `Resources` array, never `404`.

## Paging and attributes

| Parameter            | Behavior                                                                                                                                                                |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `startIndex`         | One-based. A value below 1 reads as 1. A value that is not a whole number answers `400` with `scimType` `invalidValue`.                                                 |
| `count`              | Up to 100, and 100 when omitted. A larger value reads as 100, and a negative one as 0. A value that is not a whole number answers `400` with `scimType` `invalidValue`. |
| Order                | Oldest first, by creation time and then by id. The order is the same on every page.                                                                                     |
| `attributes`         | Returns only the named attributes.                                                                                                                                      |
| `excludedAttributes` | Drops the named attributes. Probara supports `excludedAttributes=members` on groups.                                                                                    |

`schemas`, `id` and `meta` are always returned. A URN with a sub-attribute, such as `urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department`, selects only that sub-attribute. `totalResults` counts every match, and `itemsPerPage` counts the resources on the page.

## Users

### Attributes

| Attribute                                                                                   | Behavior                                                                                                                                            |
| ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `userName`                                                                                  | Required on create and replace. Unique per organization, compared case-insensitively.                                                               |
| `emails` (type `work`)                                                                      | The work address, which must be an email address. When there is no work address, `userName` must be an email address, and it serves as the address. |
| `active`                                                                                    | A boolean, or the string `"True"` or `"False"` in any case. Defaults to `true` on create.                                                           |
| `externalId`, `displayName`, `title`, `name.givenName`, `name.familyName`, `name.formatted` | Stored per organization. The person's global profile is never changed.                                                                              |
| Enterprise `department` and `employeeNumber`                                                | Stored per organization.                                                                                                                            |
| Probara `role` and Entra `roles`                                                            | Role values. See [Roles](#roles).                                                                                                                   |
| `password`                                                                                  | Ignored. Probara does not sync passwords.                                                                                                           |
| `groups`                                                                                    | Ignored in a user body. Group membership is pushed through `/Groups`.                                                                               |

### Create and replace

`POST` and `PUT` take the whole user. `PUT` clears every attribute the body leaves out.

A `POST` for an address that is already a member of the organization adopts that membership into the new record, so nobody is added twice. A `POST` whose `userName` or work address another record of the organization already holds answers `409` with `scimType` `uniqueness`.

A person on one of the organization's verified domains gets an account and a membership in the same change, in the role that [Roles](#roles) gives. A person elsewhere receives an invitation, and their record stays unlinked until they accept it.

### Patch

`PATCH` takes an `Operations` array. `op` is `add`, `replace` or `remove`, in any case. The operations apply in order, and one refused operation refuses the whole request.

| Path                                                                                                    | Behavior                                                                                                  |
| ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `active`                                                                                                | Sets the state. Deactivation and reactivation are described below.                                        |
| `userName`, `externalId`, `displayName`, `title`, `name.givenName`, `name.familyName`, `name.formatted` | Set the value. `remove` clears it, except `userName`, which answers `400` with `scimType` `invalidValue`. |
| `emails[type eq "work"].value`, `emails`                                                                | Set the work address.                                                                                     |
| `department`, `employeeNumber`, and their enterprise URN forms                                          | Set the value.                                                                                            |
| The role paths described in [Roles](#roles)                                                             | Set the role.                                                                                             |
| `emails[type eq "home"]`, `password`, `groups`                                                          | Ignored.                                                                                                  |
| Any other path                                                                                          | Answers `400` with `scimType` `invalidPath`.                                                              |

A `replace` without a `path` takes an object of attributes, the Okta form. Its keys are read as attribute paths. If the object carries the resource itself, its `id`, `schemas` and `meta` are skipped and change nothing, on users and groups alike.

### Address changes

The address of a person is the work email, or `userName` when no work email is set. Changing it is allowed only when the old and the new address are both on the organization's verified domains and the account belongs to no other organization. Otherwise the answer is `400` with `scimType` `mutability` and a reason. A change to `userName` alone, while a work email holds the address, only changes the label.

### Deactivation, reactivation and delete

| Change                                     | Effect                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `active` set to `false` (`PATCH` or `PUT`) | The membership is kept, deactivated: the person's next request to the organization is refused, and the seat is freed. Their role, project access and groups stay. Sessions created through the organization's single sign-on connection end. Personal API tokens and OAuth and MCP grants for the organization are revoked. Open run assignments and pending review requests are unassigned. The SCIM record stays, inactive, and is still returned by `GET` and by filters. |
| `active` set to `true`                     | The same `id` returns. The membership is active again with its role, which the groups, the attribute or the default role give, after a seat check. Under **Managed in Probara** the role stays. Remembered groups are restored. Revoked tokens and grants are not restored.                                                                                                                                                                                                  |
| `DELETE`                                   | The membership is removed, so the seat is freed, the person's access ends, and their groups and open work go with it. Then the SCIM record is removed. The account stays. The `id` answers `404` afterwards.                                                                                                                                                                                                                                                                 |

In every case the account stays. Organization CI app tokens are not affected. Cases, suites, runs, results, defects, comments and reviews the person created stay, and they still name the person.

The owner of an organization cannot be deprovisioned. A deactivation or a `DELETE` of the owner answers `400` with the detail `The organization owner can't be deprovisioned by SCIM. Transfer ownership first.` and writes nothing.

## Groups

Each group the identity provider sends becomes a Probara user group that the provider manages.

- **Name and members** come from the identity provider. In Probara, the description and the project, dashboard and saved query grants of the group stay editable. Renaming, deleting or changing the members of the group in Probara answers `409`.
- **Members** are applied only when they have a linked account and are active in the organization. Other members are remembered, and they join the Probara group when they become active.
- **Name clashes.** A group name that matches a group someone created in Probara, ignoring case, answers `409` with `scimType` `uniqueness`. Nothing is written.
- **`PUT`** replaces the name, the `externalId` and the whole member list.
- **`PATCH`** with `add` or `remove` changes only the people it names. A `replace` on `members` sets the whole list. Removing someone who is not a member does nothing.

| Member operation       | Form                                                                              |
| ---------------------- | --------------------------------------------------------------------------------- |
| Remove one member      | `remove` with the path `members[value eq "<id>"]`                                 |
| Remove several members | `remove` with the path `members` and a value array of `{"value": "<id>"}` objects |
| Remove every member    | `remove` with the path `members` and no value                                     |
| Replace the list       | `replace` with the path `members` and a value array                               |

A member value that is not a Users `id` of this organization answers `400` with `scimType` `invalidValue`.

`DELETE` removes the Probara group and its project, dashboard and saved query grants. Group-to-role mapping is not part of this resource. It is set in the SCIM settings of the Workspace.

## Roles

The role source of the organization decides which of the identity provider's data sets a person's role.

| Role source        | The role comes from                                                                                                                                           | When a person has no value      |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
| `groups` (default) | The mapped group with the lowest priority number. Among groups that share it, the role that grants more permissions.                                          | The organization's default role |
| `attribute`        | The Probara `role` value, or the Entra `roles` values. Several values resolve to the role that grants the most permissions, and a tie goes to the lower slug. | The organization's default role |
| `probara`          | Nothing. SCIM sets the role when the person is provisioned, and later updates do not change it.                                                               | Not applicable                  |

The default role is the one the organization's single sign-on policy gives new members. It is Viewer until you change it.

### Role shapes

Under the `attribute` source, a role comes in one of these shapes, on `POST`, `PUT` and `PATCH`:

```json
{
  "schemas": [
    "urn:ietf:params:scim:schemas:core:2.0:User",
    "urn:ietf:params:scim:schemas:extension:probara:2.0:User"
  ],
  "userName": "ada@example.com",
  "urn:ietf:params:scim:schemas:extension:probara:2.0:User": { "role": "member" }
}
```

```json
{ "roles": [{ "value": "member" }] }
```

Entra can also send `roles` as a JSON-encoded app role object, as a JSON-encoded string of the array, or as a plain string. Each form resolves to the role `value` it carries.

| Rule          | Behavior                                                                                     |
| ------------- | -------------------------------------------------------------------------------------------- |
| Value         | A role slug of the organization: `admin`, `member`, `viewer`, or a custom role's identifier. |
| `owner`       | Never assigned. Under `attribute` it answers `400` with `scimType` `invalidValue`.           |
| Unknown slug  | Under `attribute` it answers `400` with `scimType` `invalidValue`.                           |
| Other sources | Under `groups` and `probara`, a role value changes no role and never refuses the request.    |

On `PATCH`, the Probara path is `urn:ietf:params:scim:schemas:extension:probara:2.0:User:role`, and `roles` is the Entra path. `add` adds to the stored role, `remove` clears it, and `replace` sets it. A value filter on `roles` answers `400` with `scimType` `invalidPath`.

## Errors

Every error uses the SCIM error schema, with `status` as a string:

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

The `detail` is written for the administrator, and an identity provider can show it in its log. `scimType` appears only where the table names one.

| Status | When                                                                                                        | `scimType`                                                                                                            |
| ------ | ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `400`  | A malformed body, filter, path or value, an owner refusal, or a refused address change                      | `invalidSyntax`, `invalidFilter`, `invalidPath`, `invalidValue`, `noTarget` or `mutability`, when the table names one |
| `401`  | A missing, unknown, revoked or expired token                                                                | None                                                                                                                  |
| `403`  | The Enterprise plan is missing, the organization is suspended or scheduled for deletion, or no seat is free | None                                                                                                                  |
| `404`  | An unknown id, resource type, schema or path                                                                | None                                                                                                                  |
| `405`  | A method the path does not accept. The `Allow` header lists the accepted methods.                           | None                                                                                                                  |
| `409`  | A duplicate `userName`, email or group name, or a group name that a group created in Probara holds          | `uniqueness`                                                                                                          |
| `413`  | A body larger than 1 MB                                                                                     | None                                                                                                                  |
| `415`  | A body sent with a media type other than `application/scim+json` or `application/json`                      | None                                                                                                                  |
| `429`  | The token reached its request limit. The `Retry-After` header gives the wait in seconds.                    | None                                                                                                                  |
| `500`  | An unexpected fault. Retry the request.                                                                     | None                                                                                                                  |
| `501`  | `/Bulk`, `/Me` or `/.search`                                                                                | None                                                                                                                  |

Responses use `application/scim+json; charset=utf-8`. A request with a body must use `application/scim+json` or `application/json`, with or without parameters.

## Limits

| Limit              | Value                                                                                               |
| ------------------ | --------------------------------------------------------------------------------------------------- |
| Request body       | 1 MB. A larger body answers `413`.                                                                  |
| Requests per token | 250 per 10 seconds, which is 25 per second. Above that, the answer is `429` with `Retry-After: 10`. |
| Page size          | Up to 100 resources per page                                                                        |
| Token expiry       | Required, at most 365 days                                                                          |
| Active tokens      | Two per organization                                                                                |

## Checklist

- [ ] `GET /ServiceProviderConfig` answers `200` with your token.
- [ ] `POST /Users` answers `201` with a `Location` header, and a second create with the same `userName` answers `409`.
- [ ] A `PATCH` that sets `active` to `false` answers `200` and keeps the record in `GET /Users`.
- [ ] A request without a valid token answers `401`.

## Next step

Read the [SCIM provisioning guide](https://docs.probara.net/en/guides/scim-provisioning/) for the setup steps for Okta, Microsoft Entra ID and generic SCIM 2.0 clients.
