This document explains how to test the SCIM /Groups
and /Roles
endpoints as proposed in this PR: gristlabs/grist-core#1357
To help having a setup quickly with multiple accounts, I suggest to use grist-omnibus. I have made a fork (source) so the image is based on my PR and it provides accounts for testing.
First fetch and run the grist-omnibus image with the following environment arguments:
docker run --name scim-group --rm -v /tmp/grist:/persist -p 8484:80 \
-e URL=http://localhost:8484 -e GRIST_SCIM_USER="[email protected]" \
-e GRIST_ENABLE_SCIM=1 -e [email protected] -e TEAM=cool-bean \
fflorent/grist-omnibus:scim-group
Then you may go to this link: http://localhost:8484
To login, you are provided these 3 test accounts:
- [email protected] (password:
[email protected]
); - [email protected] (password:
[email protected]
); - [email protected] (password:
[email protected]
);
In order to access the Grist API endpoint, you must create an API key for the user [email protected]
as described in this document: https://support.getgrist.com/rest-api/#authentication
Then store this API key as the BEARER
variable in your bash environment:
BEARER="<your api key here>"
You are invited to log in using the testing accounts provided.
Please note that it is expected that admin2
and admin3
don't have access to the default team site. So after logging in, they are displayed an error of this sort:
Before starting, a quick overview of the changes brought by the PR. The details are provided in the PR description:
- The
/Groups
endpoint is provided to represent a group of users (aka teams); - The
/Roles
endpoint to allow giving access to Users and Groups to a certain resource (team sites, workspaces, and doucuments). This endpoint is actually an extension I made, which is allowed by the RFC;
The API is not documented yet. Note that they are identical to the /Users
API, which is documented here: https://support.getgrist.com/api/#tag/scim
And important note regarding the /Roles
endpoints:
- creation and deletion of Roles are not supported, they are tight to their associated resource and may be created / destroyed with them;
- We only allow to update their memberships, in other word, to add a Role to Users and Groups through their endpoints;
- the other endpoints (
GET /Roles
,GET /Roles/{id}
andPOST /Roles/.search
) are supported, they work similarly to the other SCIM endpoints;
Let's create a Group of name test-group
:
curl -X POST -H "Authorization: Bearer $BEARER" -v http://localhost:8484/api/scim/v2/Groups \
-d '{"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"], "displayName": "test-group"}' \
-H 'Content-Type: application/json'
The group should exist now, let's fetch all the groups:
$ curl -X GET -H "Authorization: Bearer $BEARER" -v http://localhost:8484/api/scim/v2/Groups | jq
Note: Unnecessary use of -X or --request, GET is already inferred.
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying 127.0.0.1:8484...
* Connected to localhost (127.0.0.1) port 8484 (#0)
> GET /api/scim/v2/Groups HTTP/1.1
> Host: localhost:8484
> User-Agent: curl/7.88.1
> Accept: */*
> Authorization: Bearer dac808d7f6da9309a7dc8c2e07607989bb38ee8f
>
< HTTP/1.1 200 OK
< Access-Control-Allow-Credentials: true
< Access-Control-Allow-Headers: Authorization, Content-Type, X-Requested-With
< Access-Control-Allow-Methods: GET, PATCH, PUT, POST, DELETE, OPTIONS
< Cache-Control: no-cache
< Content-Language: en
< Content-Length: 308
< Content-Type: application/scim+json; charset=utf-8
< Date: Sun, 02 Feb 2025 19:28:50 GMT
< Etag: W/"134-9YHU63lYRStGHIAK4zehDL+rD90"
< Set-Cookie: grist_core=s%3Ag-f564ce4e93a7325b0c47739bd1faf59d455ca905a59cc14d5b1978cd8569e460.jckAH2bl7hjzSlytWxPpfy8H2NmeC9STHfidJdDpVv0; Path=/; HttpOnly; SameSite=Lax
< X-Powered-By: Express
<
{ [308 bytes data]
100 308 100 308 0 0 11847 0 --:--:-- --:--:-- --:--:-- 12320
* Connection #0 to host localhost left intact
{
"schemas": [
"urn:ietf:params:scim:api:messages:2.0:ListResponse"
],
"totalResults": 1,
"Resources": [
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:Group"
],
"id": "29",
"meta": {
"resourceType": "Group",
"location": "/api/scim/v2/Groups/29"
},
"displayName": "test-group",
"members": []
}
],
"startIndex": 1,
"itemsPerPage": 20
}
In our example, the ID of the Group is 29. As we would like to add [email protected]
, we have to search for this member using the SCIM /Users
endpoint (
$ curl -H "Authorization: Bearer $BEARER" \
'http://localhost:8484/api/scim/v2/Users?filter=userName%20eq%20%[email protected]%22' | jq
{
"schemas": [
"urn:ietf:params:scim:api:messages:2.0:ListResponse"
],
"totalResults": 1,
"Resources": [
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User"
],
"id": "6",
"meta": {
"resourceType": "User",
"location": "/api/scim/v2/Users/6"
},
"userName": "[email protected]",
"name": {
"formatted": "admin2"
},
"displayName": "admin2",
"preferredLanguage": "en",
"locale": "en",
"emails": [
{
"value": "[email protected]",
"primary": true
}
]
}
],
"startIndex": 1,
"itemsPerPage": 20
}
➡️ admin2's ID is 6 in our example.
Now let's update the Group to add this user using the PUT
verb (alternatively, you may use PATCH
as specified in the RFC):
GROUP_ID=29
ADMIN2_ID=6
curl -X PUT -H "Authorization: Bearer $BEARER" -v http://localhost:8484/api/scim/v2/Groups/$GROUP_ID \
-d '{"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"], "displayName": "test-group", "members": [{"value": "'$ADMIN2_ID'", "type": "User"}]}' \
-H 'Content-Type: application/json'
Let's inspect the group now:
$ curl -H "Authorization: Bearer $BEARER" \
'http://localhost:8484/api/scim/v2/Groups/'$GROUP_ID | jq
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:Group"
],
"id": "29",
"meta": {
"resourceType": "Group",
"location": "/api/scim/v2/Groups/29"
},
"displayName": "test-group",
"members": [
{
"value": "6",
"display": "admin2",
"$ref": "/api/scim/v2/Users/6",
"type": "User"
}
]
}
(You may repeat the operation to add admin3
if you would like)
🎉 Great, at this point, we've got a Group, but no access have been granted to admin2
. Let's take a look at the Roles:
$ curl -X GET -H "Authorization: Bearer $BEARER" -v http://localhost:8484/api/scim/v2/Roles | jq
Note: Unnecessary use of -X or --request, GET is already inferred.
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying 127.0.0.1:8484...
* Connected to localhost (127.0.0.1) port 8484 (#0)
> GET /api/scim/v2/Roles HTTP/1.1
> Host: localhost:8484
> User-Agent: curl/7.88.1
> Accept: */*
> Authorization: Bearer dac808d7f6da9309a7dc8c2e07607989bb38ee8f
>
< HTTP/1.1 200 OK
< Access-Control-Allow-Credentials: true
< Access-Control-Allow-Headers: Authorization, Content-Type, X-Requested-With
< Access-Control-Allow-Methods: GET, PATCH, PUT, POST, DELETE, OPTIONS
< Cache-Control: no-cache
< Content-Language: en
< Content-Length: 4541
< Content-Type: application/scim+json; charset=utf-8
< Date: Sun, 02 Feb 2025 19:53:32 GMT
< Etag: W/"11bd-S0BFHE7OB+VkPO47yzTWn6rkN6Y"
< Set-Cookie: grist_core=s%3Ag-3d7a957a657a01a01f3f21e3acfb4df2767e72263bce9c6d0f9bd406fa377c76.6GG35jhUr9wwKz%2BBikLt3y3cy2A4QqAXJC%2FFl9DZF1E; Path=/; HttpOnly; SameSite=Lax
< X-Powered-By: Express
<
{ [3490 bytes data]
100 4541 100 4541 0 0 88037 0 --:--:-- --:--:-- --:--:-- 89039
* Connection #0 to host localhost left intact
{
"schemas": [
"urn:ietf:params:scim:api:messages:2.0:ListResponse"
],
"totalResults": 45,
"Resources": [
{
"schemas": [
"urn:ietf:params:scim:schemas:Grist:1.0:Role"
],
"id": "1",
"meta": {
"resourceType": "Role",
"location": "/api/scim/v2/Roles/1"
},
"displayName": "owners",
"members": [
{
"value": "4",
"display": "Support",
"$ref": "/api/scim/v2/Users/4",
"type": "User"
}
],
"orgId": 1
},
{
"schemas": [
"urn:ietf:params:scim:schemas:Grist:1.0:Role"
],
"id": "2",
"meta": {
"resourceType": "Role",
"location": "/api/scim/v2/Roles/2"
},
"displayName": "editors",
"members": [],
"orgId": 1
},
....
There are a lot of them, that's not very helpful. We should instead use a filter. Let's retrieve first the cool-bean
team site using the Grist API:
curl -X GET -H "Authorization: Bearer $BEARER" http://localhost:8484/api/orgs | jq
[
{
"name": "Personal",
"createdAt": "2025-02-02T09:31:10.000Z",
"updatedAt": "2025-02-02T09:31:10.651Z",
"id": 2,
"domain": "docs-5",
"host": null,
"owner": {
"id": 5,
"name": "[email protected]",
"picture": null,
"ref": "2NmoT3Kq7fZ36PfQ4TCL8K"
},
"access": "owners"
},
{
"name": "cool-bean",
"createdAt": "2025-02-02T09:31:10.000Z",
"updatedAt": "2025-02-02T09:31:10.667Z",
"id": 3,
"domain": "cool-bean",
"host": null,
"owner": null,
"access": "owners"
}
]
➡️ The cool-bean team site (aka organization) has an ID of 3 in our example.
Let's find the Roles for this team site:
$ ORG_ID=3
$ curl -H "Authorization: Bearer $BEARER" -X POST http://localhost:8484/api/scim/v2/Roles/.search \
--header 'Content-Type: application/json' -d \
'{"schemas": ["urn:ietf:params:scim:api:messages:2.0:SearchRequest"], "filter": "orgId eq '$ORG_ID'"}' | jq
{
"schemas": [
"urn:ietf:params:scim:api:messages:2.0:ListResponse"
],
"totalResults": 5,
"Resources": [
{
"schemas": [
"urn:ietf:params:scim:schemas:Grist:1.0:Role"
],
"id": "19",
"meta": {
"resourceType": "Role",
"location": "/api/scim/v2/Roles/19"
},
"displayName": "owners",
"members": [
{
"value": "5",
"display": "[email protected]",
"$ref": "/api/scim/v2/Users/5",
"type": "User"
}
],
"orgId": 3
},
{
"schemas": [
"urn:ietf:params:scim:schemas:Grist:1.0:Role"
],
"id": "20",
"meta": {
"resourceType": "Role",
"location": "/api/scim/v2/Roles/20"
},
"displayName": "editors",
"members": [],
"orgId": 3
},
{
"schemas": [
"urn:ietf:params:scim:schemas:Grist:1.0:Role"
],
"id": "21",
"meta": {
"resourceType": "Role",
"location": "/api/scim/v2/Roles/21"
},
"displayName": "viewers",
"members": [],
"orgId": 3
},
{
"schemas": [
"urn:ietf:params:scim:schemas:Grist:1.0:Role"
],
"id": "22",
"meta": {
"resourceType": "Role",
"location": "/api/scim/v2/Roles/22"
},
"displayName": "guests",
"members": [],
"orgId": 3
},
{
"schemas": [
"urn:ietf:params:scim:schemas:Grist:1.0:Role"
],
"id": "23",
"meta": {
"resourceType": "Role",
"location": "/api/scim/v2/Roles/23"
},
"displayName": "members",
"members": [],
"orgId": 3
}
],
"startIndex": 1,
"itemsPerPage": 20
}
OK, let's give our group the editor
Role (id=20) using PATCH:
$ ROLE_ID=20
$ curl -H "Authorization: Bearer $BEARER" -X PATCH "http://localhost:8484/api/scim/v2/Roles/$ROLE_ID" \
--header 'Content-Type: application/json' -d \
'{"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], "Operations": [{"op": "add", "path": "members", "value": [{"value": "'$GROUP_ID'", "type": "Group"}]}]}'
Now, you should see the group added to the given Role, so its users should have now access to the cool-bean
org as editors:
$ curl -H "Authorization: Bearer $BEARER" -X GET "http://localhost:8484/api/scim/v2/Roles/$ROLE_ID" --header 'Content-Type: application/json' | jq
{
"schemas": [
"urn:ietf:params:scim:schemas:Grist:1.0:Role"
],
"id": "20",
"meta": {
"resourceType": "Role",
"location": "/api/scim/v2/Roles/20"
},
"displayName": "editors",
"members": [
{
"value": "29",
"display": "test-group",
"$ref": "/api/scim/v2/Groups/29",
"type": "Group"
}
],
"orgId": 3
}
🥳 If you log in using admin2, you should see the cool-bean
team site now!
Now let's say our group has no reasons to exist anymore, let's delete it:
$ curl -H "Authorization: Bearer $BEARER" -X DELETE "http://localhost:8484/api/scim/v2/Groups/$GROUP_ID"
Now admin2 should normally have lost access to cool-bean
.
With this showcase, we have been able to:
- List groups, optionally with filters (
GET /Groups
) - List a single group (
GET /Groups/{id}
) - Create a group (
POST /Groups
) - Update an existing group to add a user member (
PUT /Groups/{id}
orPATCH /Groups/{id}
) - Delete a group
- List Roles, optionally with filters (
GET /Roles
) - List a certain Role (
GET /Roles/{id}
) - Update a Role to add members, so a Group is granted a certain permission to an organization (
PUT /Roles/{id}
orPATCH /Roles/{id}
)