diff --git a/doc/api/saml.md b/doc/api/saml.md new file mode 100644 index 0000000000000000000000000000000000000000..810ed382d4937ee9f33497ba9e78cda8be708171 --- /dev/null +++ b/doc/api/saml.md @@ -0,0 +1,78 @@ +--- +stage: Manage +group: Authentication and Authorization +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments +--- + +# SAML API **(PREMIUM SAAS)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/227841) in GitLab 15.5. + +API for accessing SAML features. + +## Get SAML identities for a group + +```plaintext +GET /groups/:id/saml/identities +``` + +Fetch SAML identities for a group. + +Supported attributes: + +| Attribute | Type | Required | Description | +|:------------------|:--------|:---------|:----------------------| +| `id` | integer | Yes | Group ID for the group to return SAML identities. | + +If successful, returns [`200`](index.md#status-codes) and the following +response attributes: + +| Attribute | Type | Description | +| ------------ | ------ | ------------------------- | +| `extern_uid` | string | External UID for the user | +| `user_id` | string | ID for the user | + +Example request: + +```shell +curl --location --request GET "https://gdk.test:3443/api/v4/groups/33/saml/identities" \ +--header "" \ +--form "extern_uid=" \ +``` + +Example response: + +```json +[ + { + "extern_uid": "4", + "user_id": 48 + } +] +``` + +## Update `extern_uid` field for a SAML identity + +Update `extern_uid` field for a SAML identity. Field that can be updated are: + +| SAML IdP attribute | GitLab field | +| ------------------ | ------------ | +| `id/externalId` | `extern_uid` | + +```plaintext +PATCH groups/:groups_id/saml/:uid +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ------ | -------- | ------------------------- | +| `uid` | string | yes | External UID of the user. | + +Example request: + +```shell +curl --location --request PATCH "https://gdk.test:3443/api/v4/groups/33/saml/sydney_jones" \ +--header "" \ +--form "extern_uid=sydney_jones_new" \ +``` diff --git a/doc/api/scim.md b/doc/api/scim.md index 9a745776f65290bf1c0ad49e4f63ae46f196b3ca..b1763a44fc4883b1e7a5b29b9953f9d548df104b 100644 --- a/doc/api/scim.md +++ b/doc/api/scim.md @@ -4,251 +4,80 @@ stage: Manage group: Authentication and Authorization info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- +# SCIM API **(PREMIUM SAAS)** -# SCIM API (SYSTEM ONLY) **(PREMIUM SAAS)** - -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/9388) in GitLab 11.10. - -The SCIM API implements the [RFC7644 protocol](https://www.rfc-editor.org/rfc/rfc7644). As this API is for -**system** use for SCIM provider integration, it is subject to change without notice. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/98354) in GitLab 15.5. To use this API, [Group SSO](../user/group/saml_sso/index.md) must be enabled for the group. This API is only in use where [SCIM for Group SSO](../user/group/saml_sso/scim_setup.md) is enabled. It's a prerequisite to the creation of SCIM identities. -## Get a list of SCIM provisioned users - -This endpoint is used as part of the SCIM syncing mechanism. It only returns -a single user based on a unique ID which should match the `extern_uid` of the user. - -```plaintext -GET /api/scim/v2/groups/:group_path/Users -``` - -Parameters: - -| Attribute | Type | Required | Description | -|:----------|:--------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------| -| `filter` | string | no | A [filter](#available-filters) expression. | -| `group_path` | string | yes | Full path to the group. | -| `startIndex` | integer | no | The 1-based index indicating where to start returning results from. A value of less than one will be interpreted as 1. | -| `count` | integer | no | Desired maximum number of query results. | - -NOTE: -Pagination follows the [SCIM spec](https://www.rfc-editor.org/rfc/rfc7644#section-3.4.2.4) rather than GitLab pagination as used elsewhere. If records change between requests it is possible for a page to either be missing records that have moved to a different page or repeat records from a previous request. - -Example request: - -```shell -curl "https://gitlab.example.com/api/scim/v2/groups/test_group/Users?filter=id%20eq%20%220b1d561c-21ff-4092-beab-8154b17f82f2%22" \ - --header "Authorization: Bearer " \ - --header "Content-Type: application/scim+json" -``` - -Example response: +Not to be confused with the [internal SCIM API](../development/internal_api/index.md#scim-api). -```json -{ - "schemas": [ - "urn:ietf:params:scim:api:messages:2.0:ListResponse" - ], - "totalResults": 1, - "itemsPerPage": 20, - "startIndex": 1, - "Resources": [ - { - "schemas": [ - "urn:ietf:params:scim:schemas:core:2.0:User" - ], - "id": "0b1d561c-21ff-4092-beab-8154b17f82f2", - "active": true, - "name.formatted": "Test User", - "userName": "username", - "meta": { "resourceType":"User" }, - "emails": [ - { - "type": "work", - "value": "name@example.com", - "primary": true - } - ] - } - ] -} -``` +## Get SCIM identities for a group -## Get a single SCIM provisioned user +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/227841) in GitLab 15.5. ```plaintext -GET /api/scim/v2/groups/:group_path/Users/:id +GET /groups/:id/scim/identities ``` -Parameters: +Supported attributes: -| Attribute | Type | Required | Description | -|:----------|:--------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------| -| `id` | string | yes | External UID of the user. | -| `group_path` | string | yes | Full path to the group. | +| Attribute | Type | Required | Description | +|:------------------|:--------|:---------|:----------------------| +| `id` | integer | Yes | Return SAML identities for the given group ID. | -Example request: +If successful, returns [`200`](index.md#status-codes) and the following +response attributes: -```shell -curl "https://gitlab.example.com/api/scim/v2/groups/test_group/Users/f0b1d561c-21ff-4092-beab-8154b17f82f2" \ - --header "Authorization: Bearer " --header "Content-Type: application/scim+json" -``` +| Attribute | Type | Description | +| ------------ | ------ | ------------------------- | +| `extern_uid` | string | External UID for the user | +| `user_id` | string | ID for the user | Example response: ```json -{ - "schemas": [ - "urn:ietf:params:scim:schemas:core:2.0:User" - ], - "id": "0b1d561c-21ff-4092-beab-8154b17f82f2", - "active": true, - "name.formatted": "Test User", - "userName": "username", - "meta": { "resourceType":"User" }, - "emails": [ +[ { - "type": "work", - "value": "name@example.com", - "primary": true + "extern_uid": "4", + "user_id": 48 } - ] -} +] ``` -## Create a SCIM provisioned user - -```plaintext -POST /api/scim/v2/groups/:group_path/Users/ -``` - -Parameters: - -| Attribute | Type | Required | Description | -|:---------------|:----------|:----|:--------------------------| -| `externalId` | string | yes | External UID of the user. | -| `userName` | string | yes | Username of the user. | -| `emails` | JSON string | yes | Work email. | -| `name` | JSON string | yes | Name of the user. | -| `meta` | string | no | Resource type (`User`). | - Example request: ```shell -curl --verbose --request POST "https://gitlab.example.com/api/scim/v2/groups/test_group/Users" \ - --data '{"externalId":"test_uid","active":null,"userName":"username","emails":[{"primary":true,"type":"work","value":"name@example.com"}],"name":{"formatted":"Test User","familyName":"User","givenName":"Test"},"schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],"meta":{"resourceType":"User"}}' \ - --header "Authorization: Bearer " --header "Content-Type: application/scim+json" -``` - -Example response: - -```json -{ - "schemas": [ - "urn:ietf:params:scim:schemas:core:2.0:User" - ], - "id": "0b1d561c-21ff-4092-beab-8154b17f82f2", - "active": true, - "name.formatted": "Test User", - "userName": "username", - "meta": { "resourceType":"User" }, - "emails": [ - { - "type": "work", - "value": "name@example.com", - "primary": true - } - ] -} +curl --location --request GET "https://gdk.test:3443/api/v4/groups/33/scim/identities" \ +--header "" \ +--form "extern_uid=" \ ``` -Returns a `201` status code if successful. +## Update extern_uid field for a SCIM identity -## Update a single SCIM provisioned user +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/227841) in GitLab 15.5. Fields that can be updated are: -| SCIM/IdP field | GitLab field | -|:---------------------------------|:-----------------------------------------------------------------------------| -| `id/externalId` | `extern_uid` | -| `name.formatted` | `name` ([Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/363058)) | -| `emails\[type eq "work"\].value` | `email` ([Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/363058)) | -| `active` | Identity removal if `active` = `false` | -| `userName` | `username` ([Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/363058)) | - -```plaintext -PATCH /api/scim/v2/groups/:group_path/Users/:id -``` - -Parameters: - -| Attribute | Type | Required | Description | -|:----------|:--------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------| -| `id` | string | yes | External UID of the user. | -| `group_path` | string | yes | Full path to the group. | -| `Operations` | JSON string | yes | An [operations](#available-operations) expression. | - -Example request: - -```shell -curl --verbose --request PATCH "https://gitlab.example.com/api/scim/v2/groups/test_group/Users/f0b1d561c-21ff-4092-beab-8154b17f82f2" \ - --data '{ "Operations": [{"op":"Add","path":"name.formatted","value":"New Name"}] }' \ - --header "Authorization: Bearer " --header "Content-Type: application/scim+json" -``` - -Returns an empty response with a `204` status code if successful. - -## Remove a single SCIM provisioned user - -Removes the user's SSO identity and group membership. +| SCIM/IdP field | GitLab field | +| --------------- | ------------ | +| `id/externalId` | `extern_uid` | ```plaintext -DELETE /api/scim/v2/groups/:group_path/Users/:id +PATCH groups/:groups_id/scim/:uid ``` Parameters: -| Attribute | Type | Required | Description | -|:----------|:--------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------| -| `id` | string | yes | External UID of the user. | -| `group_path` | string | yes | Full path to the group. | +| Attribute | Type | Required | Description | +| --------- | ------ | -------- | ------------------------- | +| `uid` | string | yes | External UID of the user. | Example request: ```shell -curl --verbose --request DELETE "https://gitlab.example.com/api/scim/v2/groups/test_group/Users/f0b1d561c-21ff-4092-beab-8154b17f82f2" \ - --header "Authorization: Bearer " --header "Content-Type: application/scim+json" -``` - -Returns an empty response with a `204` status code if successful. - -## Available filters - -They match an expression as specified in [the RFC7644 filtering section](https://www.rfc-editor.org/rfc/rfc7644#section-3.4.2.2). - -| Filter | Description | -| ----- | ----------- | -| `eq` | The attribute matches exactly the specified value. | - -Example: - -```plaintext -id eq a-b-c-d -``` - -## Available operations - -They perform an operation as specified in [the RFC7644 update section](https://www.rfc-editor.org/rfc/rfc7644#section-3.5.2). - -| Operator | Description | -| ----- | ----------- | -| `Replace` | The attribute's value is updated. | -| `Add` | The attribute has a new value. | - -Example: - -```json -{ "op": "Add", "path": "name.formatted", "value": "New Name" } +curl --location --request PATCH "https://gdk.test:3443/api/v4/groups/33/scim/sydney_jones" \ +--header "" \ +--form "extern_uid=sydney_jones_new" \ ``` diff --git a/doc/development/internal_api/index.md b/doc/development/internal_api/index.md index e7dfcd58af601c88474a2b7a8d1f0057658c2a61..755a7918111e7c8443a14ea6a837c0957d9bec7b 100644 --- a/doc/development/internal_api/index.md +++ b/doc/development/internal_api/index.md @@ -965,3 +965,253 @@ Example response: ### Known consumers - CustomersDot + +## SCIM API **(PREMIUM SAAS)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/9388) in GitLab 11.10. + +The SCIM API implements the [RFC7644 protocol](https://www.rfc-editor.org/rfc/rfc7644). As this API is for +**system** use for SCIM provider integration, it is subject to change without notice. + +To use this API, [Group SSO](../../user/group/saml_sso/index.md) must be enabled for the group. +This API is only in use where [SCIM for Group SSO](../../user/group/saml_sso/scim_setup.md) is enabled. It's a prerequisite to the creation of SCIM identities. + +Not to be confused with the [main SCIM API](../../api/scim.md). + +### Get a list of SCIM provisioned users + +This endpoint is used as part of the SCIM syncing mechanism. It only returns +a single user based on a unique ID which should match the `extern_uid` of the user. + +```plaintext +GET /api/scim/v2/groups/:group_path/Users +``` + +Parameters: + +| Attribute | Type | Required | Description | +|:----------|:--------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------| +| `filter` | string | no | A [filter](#available-filters) expression. | +| `group_path` | string | yes | Full path to the group. | +| `startIndex` | integer | no | The 1-based index indicating where to start returning results from. A value of less than one will be interpreted as 1. | +| `count` | integer | no | Desired maximum number of query results. | + +NOTE: +Pagination follows the [SCIM spec](https://www.rfc-editor.org/rfc/rfc7644#section-3.4.2.4) rather than GitLab pagination as used elsewhere. If records change between requests it is possible for a page to either be missing records that have moved to a different page or repeat records from a previous request. + +Example request: + +```shell +curl "https://gitlab.example.com/api/scim/v2/groups/test_group/Users?filter=id%20eq%20%220b1d561c-21ff-4092-beab-8154b17f82f2%22" \ + --header "Authorization: Bearer " \ + --header "Content-Type: application/scim+json" +``` + +Example response: + +```json +{ + "schemas": [ + "urn:ietf:params:scim:api:messages:2.0:ListResponse" + ], + "totalResults": 1, + "itemsPerPage": 20, + "startIndex": 1, + "Resources": [ + { + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:User" + ], + "id": "0b1d561c-21ff-4092-beab-8154b17f82f2", + "active": true, + "name.formatted": "Test User", + "userName": "username", + "meta": { "resourceType":"User" }, + "emails": [ + { + "type": "work", + "value": "name@example.com", + "primary": true + } + ] + } + ] +} +``` + +### Get a single SCIM provisioned user + +```plaintext +GET /api/scim/v2/groups/:group_path/Users/:id +``` + +Parameters: + +| Attribute | Type | Required | Description | +|:----------|:--------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------| +| `id` | string | yes | External UID of the user. | +| `group_path` | string | yes | Full path to the group. | + +Example request: + +```shell +curl "https://gitlab.example.com/api/scim/v2/groups/test_group/Users/f0b1d561c-21ff-4092-beab-8154b17f82f2" \ + --header "Authorization: Bearer " --header "Content-Type: application/scim+json" +``` + +Example response: + +```json +{ + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:User" + ], + "id": "0b1d561c-21ff-4092-beab-8154b17f82f2", + "active": true, + "name.formatted": "Test User", + "userName": "username", + "meta": { "resourceType":"User" }, + "emails": [ + { + "type": "work", + "value": "name@example.com", + "primary": true + } + ] +} +``` + +### Create a SCIM provisioned user + +```plaintext +POST /api/scim/v2/groups/:group_path/Users/ +``` + +Parameters: + +| Attribute | Type | Required | Description | +|:---------------|:----------|:----|:--------------------------| +| `externalId` | string | yes | External UID of the user. | +| `userName` | string | yes | Username of the user. | +| `emails` | JSON string | yes | Work email. | +| `name` | JSON string | yes | Name of the user. | +| `meta` | string | no | Resource type (`User`). | + +Example request: + +```shell +curl --verbose --request POST "https://gitlab.example.com/api/scim/v2/groups/test_group/Users" \ + --data '{"externalId":"test_uid","active":null,"userName":"username","emails":[{"primary":true,"type":"work","value":"name@example.com"}],"name":{"formatted":"Test User","familyName":"User","givenName":"Test"},"schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],"meta":{"resourceType":"User"}}' \ + --header "Authorization: Bearer " --header "Content-Type: application/scim+json" +``` + +Example response: + +```json +{ + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:User" + ], + "id": "0b1d561c-21ff-4092-beab-8154b17f82f2", + "active": true, + "name.formatted": "Test User", + "userName": "username", + "meta": { "resourceType":"User" }, + "emails": [ + { + "type": "work", + "value": "name@example.com", + "primary": true + } + ] +} +``` + +Returns a `201` status code if successful. + +### Update a single SCIM provisioned user + +Fields that can be updated are: + +| SCIM/IdP field | GitLab field | +|:---------------------------------|:-----------------------------------------------------------------------------| +| `id/externalId` | `extern_uid` | +| `name.formatted` | `name` ([Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/363058)) | +| `emails\[type eq "work"\].value` | `email` ([Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/363058)) | +| `active` | Identity removal if `active` = `false` | +| `userName` | `username` ([Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/363058)) | + +```plaintext +PATCH /api/scim/v2/groups/:group_path/Users/:id +``` + +Parameters: + +| Attribute | Type | Required | Description | +|:----------|:--------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------| +| `id` | string | yes | External UID of the user. | +| `group_path` | string | yes | Full path to the group. | +| `Operations` | JSON string | yes | An [operations](#available-operations) expression. | + +Example request: + +```shell +curl --verbose --request PATCH "https://gitlab.example.com/api/scim/v2/groups/test_group/Users/f0b1d561c-21ff-4092-beab-8154b17f82f2" \ + --data '{ "Operations": [{"op":"Add","path":"name.formatted","value":"New Name"}] }' \ + --header "Authorization: Bearer " --header "Content-Type: application/scim+json" +``` + +Returns an empty response with a `204` status code if successful. + +### Remove a single SCIM provisioned user + +Removes the user's SSO identity and group membership. + +```plaintext +DELETE /api/scim/v2/groups/:group_path/Users/:id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| ------------ | ------ | -------- | ------------------------- | +| `id` | string | yes | External UID of the user. | +| `group_path` | string | yes | Full path to the group. | + +Example request: + +```shell +curl --verbose --request DELETE "https://gitlab.example.com/api/scim/v2/groups/test_group/Users/f0b1d561c-21ff-4092-beab-8154b17f82f2" \ + --header "Authorization: Bearer " --header "Content-Type: application/scim+json" +``` + +Returns an empty response with a `204` status code if successful. + +### Available filters + +They match an expression as specified in [the RFC7644 filtering section](https://www.rfc-editor.org/rfc/rfc7644#section-3.4.2.2). + +| Filter | Description | +| ----- | ----------- | +| `eq` | The attribute matches exactly the specified value. | + +Example: + +```plaintext +id eq a-b-c-d +``` + +### Available operations + +They perform an operation as specified in [the RFC7644 update section](https://www.rfc-editor.org/rfc/rfc7644#section-3.5.2). + +| Operator | Description | +| ----- | ----------- | +| `Replace` | The attribute's value is updated. | +| `Add` | The attribute has a new value. | + +Example: + +```json +{ "op": "Add", "path": "name.formatted", "value": "New Name" } +``` diff --git a/doc/user/group/saml_sso/scim_setup.md b/doc/user/group/saml_sso/scim_setup.md index 6b64288f5af7a927f49329bab80ff17ff51a4b36..7fd335257aabb21da8a4c4fc5176971343615992 100644 --- a/doc/user/group/saml_sso/scim_setup.md +++ b/doc/user/group/saml_sso/scim_setup.md @@ -15,7 +15,7 @@ GitLab SAML SSO SCIM doesn't support updating users. When SCIM is enabled for a GitLab group, membership of that group is synchronized between GitLab and an identity provider. -The GitLab [SCIM API](../../../api/scim.md) implements part of [the RFC7644 protocol](https://www.rfc-editor.org/rfc/rfc7644). +The [internal GitLab SCIM API](../../../development/internal_api/index.md#scim-api) implements part of [the RFC7644 protocol](https://www.rfc-editor.org/rfc/rfc7644). ## Configure GitLab @@ -121,7 +121,7 @@ attributes and modify them accordingly. In particular, the `objectId` source att target attribute. If a mapping is not listed in the table, use the Azure Active Directory defaults. For a list of required attributes, -refer to the [SCIM API documentation](../../../api/scim.md). +refer to the [internal SCIM API](../../../development/internal_api/index.md#scim-api) documentation. ### Configure Okta diff --git a/doc/user/group/saml_sso/troubleshooting_scim.md b/doc/user/group/saml_sso/troubleshooting_scim.md index f98b6c61e11c36abf4c4a453a61d3f943eb29542..6f8aed4b386a830cd635e874ac8652664273138e 100644 --- a/doc/user/group/saml_sso/troubleshooting_scim.md +++ b/doc/user/group/saml_sso/troubleshooting_scim.md @@ -34,7 +34,7 @@ Administrators can use the Admin Area to [list SCIM identities for a user](../.. Group owners can see the list of users and the `externalId` stored for each user in the group SAML SSO Settings page. -A possible alternative is to use the [SCIM API](../../../api/scim.md#get-a-list-of-scim-provisioned-users) to manually retrieve the `externalId` we have stored for users, also called the `external_uid` or `NameId`. +A possible alternative is to use the [SCIM API](../../../api/scim.md) to manually retrieve the `externalId` we have stored for users, also called the `external_uid` or `NameId`. To see how the `external_uid` compares to the value returned as the SAML NameId, you can have the user use a [SAML Tracer](troubleshooting.md#saml-debugging-tools). @@ -53,7 +53,7 @@ you can address the problem in the following ways: - You can have users unlink and relink themselves, based on the ["SAML authentication failed: User has already been taken"](troubleshooting.md#message-saml-authentication-failed-user-has-already-been-taken) section. - You can unlink all users simultaneously, by removing all users from the SAML app while provisioning is turned on. -- It may be possible to use the [SCIM API](../../../api/scim.md#update-a-single-scim-provisioned-user) to manually correct the `externalId` stored for users to match the SAML `NameId`. +- Use the [SCIM API](../../../api/scim.md) to manually correct the `externalId` stored for users to match the SAML `NameId`. To look up a user, you need to know the desired value that matches the `NameId` as well as the current `externalId`. It is important not to update these to incorrect values, since this causes users to be unable to sign in. It is also important not to assign a value to the wrong user, as this causes users to get signed into the wrong account. @@ -71,11 +71,13 @@ Changing the SAML or SCIM configuration or provider can cause the following prob | Problem | Solution | | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | SAML and SCIM identity mismatch. | First [verify that the user's SAML NameId matches the SCIM externalId](#how-do-i-verify-users-saml-nameid-matches-the-scim-externalid) and then [update or fix the mismatched SCIM externalId and SAML NameId](#update-or-fix-mismatched-scim-externalid-and-saml-nameid). | -| SCIM identity mismatch between GitLab and the identity provider SCIM app. | You can confirm whether you're hitting the error because of your SCIM identity mismatch between your SCIM app and GitLab.com by using [SCIM API](../../../api/scim.md#update-a-single-scim-provisioned-user) which shows up in the `id` key and compares it with the user `externalId` in the SCIM app. You can use the same [SCIM API](../../../api/scim.md#update-a-single-scim-provisioned-user) to update the SCIM `id` for the user on GitLab.com. | +| SCIM identity mismatch between GitLab and the identity provider SCIM app. | You can confirm whether you're hitting the error because of your SCIM identity mismatch between your SCIM app and GitLab.com by using the [SCIM API](../../../api/scim.md) which shows up in the `id` key and compares it with the user `externalId` in the SCIM app. You can use the same [SCIM API](../../../api/scim.md) to update the SCIM `id` for the user on GitLab.com. | ## Search Rails logs for SCIM requests -GitLab.com administrators can search for SCIM requests in the `api_json.log` using the `pubsub-rails-inf-gprd-*` index in [Kibana](https://about.gitlab.com/handbook/support/workflows/kibana.html#using-kibana). Use the following filters based on the [SCIM API](../../../api/scim.md): +GitLab.com administrators can search for SCIM requests in the `api_json.log` using the `pubsub-rails-inf-gprd-*` index in +[Kibana](https://about.gitlab.com/handbook/support/workflows/kibana.html#using-kibana). Use the following filters based on the internal +[SCIM API](../../../development/internal_api/index.md#scim-api): - `json.path`: `/scim/v2/groups/` - `json.params.value`: `` diff --git a/ee/lib/api/provider_identity.rb b/ee/lib/api/provider_identity.rb new file mode 100644 index 0000000000000000000000000000000000000000..914fe073188fcfca6dd5dac83f6dc6c3c99e8d48 --- /dev/null +++ b/ee/lib/api/provider_identity.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +module API + class ProviderIdentity < ::API::Base + include ::Gitlab::Utils::StrongMemoize + + before { authenticate! } + before { authorize_admin_group } + + feature_category :authentication_and_authorization + + params do + requires :id, type: String, desc: 'The ID of a group' + end + resource :groups do + %w[saml scim].each do |provider_type| + resource ":id/#{provider_type}", requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + desc 'Get user identities for the provider' do + success EE::API::Entities::IdentityDetail + end + + get "/identities" do + group = find_group(params[:id]) + + case provider_type + when 'saml' + bad_request! unless group.saml_provider + present group.saml_provider.identities, with: EE::API::Entities::IdentityDetail + when 'scim' + present group.scim_identities, with: EE::API::Entities::IdentityDetail + end + end + + desc 'Update extern_uid for the user' do + success EE::API::Entities::IdentityDetail + end + + params do + requires :uid, type: String, desc: "Current external UID of the user" + requires :extern_uid, type: String, desc: "Desired/new external UID of the user" + end + + patch ':uid' do + group = find_group(params[:id]) + identity = find_provider_identity(provider_type, params[:uid], group) + + not_found!('Identity') unless identity + + if identity.update(extern_uid: params[:extern_uid]) + present identity, with: EE::API::Entities::IdentityDetail + else + render_api_error!(identity.errors.full_messages.join(",").to_s, 400) + end + end + end + end + end + + helpers do + def find_provider_identity(provider_type, extern_uid, group) + case provider_type + when 'scim' + group.scim_identities.with_extern_uid(extern_uid).first + when 'saml' + GroupSamlIdentityFinder.find_by_group_and_uid(group: group, uid: extern_uid) + end + end + end + end +end diff --git a/ee/lib/ee/api/api.rb b/ee/lib/ee/api/api.rb index 017d9548a06849ab0b868c85971676b1b7138038..4a3267f8b2ce036a655bfe41abe49d324f3ed047 100644 --- a/ee/lib/ee/api/api.rb +++ b/ee/lib/ee/api/api.rb @@ -31,6 +31,7 @@ module API mount ::API::GroupPushRule mount ::API::MergeTrains mount ::API::MemberRoles + mount ::API::ProviderIdentity mount ::API::GroupHooks mount ::API::MergeRequestApprovalSettings mount ::API::Scim diff --git a/ee/lib/ee/api/entities/identity_detail.rb b/ee/lib/ee/api/entities/identity_detail.rb new file mode 100644 index 0000000000000000000000000000000000000000..0b7263c36c8e3d1fe6b563e4322e36b5cf23731e --- /dev/null +++ b/ee/lib/ee/api/entities/identity_detail.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module EE + module API + module Entities + class IdentityDetail < Grape::Entity + expose :extern_uid + expose :user_id + end + end + end +end diff --git a/ee/spec/lib/ee/api/entities/identity_detail_spec.rb b/ee/spec/lib/ee/api/entities/identity_detail_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..162c20b6f4ebaea717c00239843bbbb2a05fae15 --- /dev/null +++ b/ee/spec/lib/ee/api/entities/identity_detail_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe EE::API::Entities::IdentityDetail do + describe 'exposes extern_uid and user_id fields' do + let(:user) { create(:user) } + let!(:identity) { create(:group_saml_identity, user: user) } + let(:identity_detail_entity) { described_class.new(identity) } + + subject(:entity) { identity_detail_entity.as_json } + + it 'exposes the attributes' do + expect(entity[:extern_uid]).to eq identity.extern_uid + expect(entity[:user_id]).to eq identity.user_id + end + end +end diff --git a/ee/spec/requests/api/provider_identity_spec.rb b/ee/spec/requests/api/provider_identity_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..7912457c177dea795e3d1de391e471a0faa30a3e --- /dev/null +++ b/ee/spec/requests/api/provider_identity_spec.rb @@ -0,0 +1,154 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe API::ProviderIdentity, api: true do + include ApiHelpers + + let_it_be(:owner) { create(:user) } + let_it_be(:user) { create(:user) } + let(:current_user) { nil } + + let_it_be(:group) do + group = create(:group) + group.add_guest(user) + group.add_owner(owner) + group + end + + let_it_be(:saml_provider) { create(:saml_provider, group: group) } + + let_it_be(:saml_identity_one) do + create(:identity, user_id: user.id, provider: 'group_saml', + saml_provider_id: saml_provider.id, extern_uid: 'saml-uid-1') + end + + let_it_be(:saml_identity_two) do + create(:identity, user_id: owner.id, provider: 'group_saml', + saml_provider_id: saml_provider.id, extern_uid: 'saml-uid-2') + end + + let_it_be(:scim_identity_one) do + create(:scim_identity, user: user, group: group, extern_uid: 'scim-uid-1') + end + + let_it_be(:scim_identity_two) do + create(:scim_identity, user: owner, group: group, extern_uid: 'scim-uid-2') + end + + describe "Provider Identity API" do + using RSpec::Parameterized::TableSyntax + + where(:provider_type, :provider_extern_uid_1, :provider_extern_uid_2, :identity_type, :validation_error) do + "saml" | "saml-uid-1" | "saml-uid-2" | Identity | "SAML NameID can't be blank" + "scim" | "scim-uid-1" | "scim-uid-2" | ScimIdentity | "Extern uid can't be blank" + end + + with_them do + context "when GET identities" do + subject(:get_identities) { get api("/groups/#{group.id}/#{provider_type}/identities", current_user) } + + context "when user is not a group owner" do + let(:current_user) { user } + + it "throws unauthorized error" do + get_identities + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context "when user is group owner" do + let(:current_user) { owner } + + it "returns the list of identities" do + get_identities + + expect(json_response).to( + match([ + { "extern_uid" => provider_extern_uid_1, "user_id" => user.id }, + { "extern_uid" => provider_extern_uid_2, "user_id" => owner.id } + ]) + ) + end + end + end + + context "when PATCH uid" do + subject(:patch_identities) do + patch api("/groups/#{group.id}/#{provider_type}/#{uid}", current_user), + params: { extern_uid: extern_uid } + end + + context "when user is not a group owner" do + let(:uid) { provider_extern_uid_1 } + let(:current_user) { user } + let(:extern_uid) { 'updated_uid' } + + it "throws forbidden error" do + patch_identities + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context "when user is a group owner" do + let(:current_user) { owner } + let(:extern_uid) { "updated_uid" } + + context "when invalid uid is passed" do + let(:uid) { "test_uid" } + + it "returns not found error" do + patch_identities + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context "when valid uid is passed" do + let(:uid) { provider_extern_uid_1 } + + it "updates the identity record with extern_uid passed" do + patch_identities + + expect(response).to have_gitlab_http_status(:ok) + + # Check that response is equal to the updated object + expect(json_response['extern_uid']).to eq('updated_uid') + end + + context "when invalid extern_uid to update is passed" do + let(:uid) { provider_extern_uid_1 } + let(:extern_uid) { "" } + + it "throws bad request error" do + patch_identities + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq(validation_error) + end + end + end + + context "when params contain attribute other than extern_uid" do + it "does not update any other param" do + expect do + patch api("/groups/#{group.id}/#{provider_type}/#{scim_identity_one.extern_uid}", current_user), + params: { active: false } + + expect(json_response['error']).to eq("extern_uid is missing") + end.not_to change(scim_identity_one, :active) + end + + it "throws error when param is missing" do + patch api("/groups/#{group.id}/#{provider_type}/#{provider_extern_uid_1}", current_user) + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + end + end + end + end +end