Join us live Sept. 27 – How Sanity and Vercel powered Morning Brew's transformation –>
September 28, 2021

Sanity Access Control: moving from `_.groups` to the new APIs

One of the perks of Sanity's enterprise plan is the ability to customize very thoroughly the access control.

Our team maintains a marketplace where customers buy products from different sellers. We use custom access control to let sellers edit their own products descriptions. We certainly don't want them to edit other products or documents. Setting this up has been a lot of trial and errors, hence this guide.

Groups documents

Up until now, we have been using groups to set up custom access control rules. Groups are stored as regular Sanity documents in each dataset, and can be modified using mutations like any other document. Note that only create session tokens have the permission to create and update group documents.

This is how our groups looks like:

{
  "_id": "_.groups.seller-access.product-name",
  "_type": "system.group",
  "grants": [
    {
      "filter": "_id in [\"drafts.XXX\", \"XXX\"]",
      "permissions": ["read", "create", "update"]
    }
  ],
  "members": ["pk1234abc"],
  // updatedAt, _rev, etc.
}

Our user, with the id pk1234abc has the "Custom" role because it gives no permission by default.

The "Custom" role does not tell us much

The group above is then giving this user permission to create, read, and update the document with id XXX.

This setup works for us, but it has 2 main drawbacks:

  1. We need a create-session token to manipulate group documents, which means we cannot build a UI inside Sanity studio using the current logged in users to create and update the groups. Not all our employees handling access control are comfortable using command line tools (or even tools like Postman).
  2. All our members who are sellers appears in the Sanity.io user interface to manage project members with the "Custom" role. It's impossible to see easily who has access to which document.

Introducing better access control APIs

In June this year, the Sanity team introduced Improved Roles & Project Management. It gives enterprise customers access to HTTP APIs to customize access control.

Pros:

  • You can customize access control at several levels: organization, project or dataset.
  • You only need an admin token, so you can build a specific UI in the studio (that's what we did).
  • New roles are properly named in Sanity.io UI (we aren't limited to "Custom" anymore).

Cons:

  • The API documentation is a bit light.
  • The migration path from groups to new roles API is a bit unclear. We now have 2 different ways to restrict access to members.
  • New vocabulary to learn (roles, grants, permission, permissions resources, filters, etc.).
  • Hiding custom panels in the studio based on access control requires a lot of code or workarounds.

Creating a new role

The first step is to create a custom role. It only needs a title, a name, and optionally a description.

curl \
	-X POST \
	-H "Authorization: Bearer ${token}"
	-H "Content-Type: application/json"
	-d '{"title": "Product X Owner", "name": "product-x-owner", "description": "Members allowed to update the details about product X"}'
	https://api.sanity.io/v2021-06-07/projects/${projectId}/roles

We now have a new role that can even be assigned from Sanity.io user interface, but it currently has no granted permissions.

New role!

Creating new permission

We now need to create new rules to link to our new role. The list of all permission resources can be found by sending a GET request to the /projects/${projectId}/permissionResourceSchemas endpoint. They are endless possibilities!

With our goal to only give access to a specific set of documents, we can use the sanity.document.filter schema.

curl \
	-X POST
	-H "Authorization: Bearer ${token}"
	-H "Content-Type: application/json"
	-d '{"permissionResourceType": "sanity.document.filter", "title": "Access to product X", "description": "Only access to product X", "config": {"filter": "_id in [\"${document-id}\", \"drafts.${document-id}\"]" }}'
	https://api.sanity.io/v2021-06-07/projects/${projectId}/permissionResources

We just create a "permission resource" to restrict access to the document with the id document-id and its draft. This new resource has an id (appearing in the request response) that we will use in the next step. Because yes, this is not enough, we now need to link our new role, our new resource and a grant (like read, update, etc.)

Gluing everything together

Let's give our new role read access to our new permissions resource:

curl \
	-X POST
	-H "Authorization: Bearer ${token}"
	-H "Content-Type: application/json"
	-d '{"roleName": "product-x-owner", "permissionName": "read", "permissionResourceId": "${idReturnedInPreviousSection}"}'
	https://api.sanity.io/v2021-06-07/projects/${projectId}/grants

In our case, we want to repeat this request to assign the update and create role too.

Finally, assigning this role to members

For this you can either use the user interface on Sanity.io OR use the API:

curl \
	-X POST
	-H "Authorization: Bearer ${token}"
	-H "Content-Type: application/json"
	-d '{"roleName": "product-x-owner"}'
	https://api.sanity.io/v2021-06-07/projects/${projectId}/acl/${userId}

Making our own UI

Deep down inside the documentation for Sanity UI, there is an article about Building a custom tool with Sanity UI. It was the perfect opportunity for us to test the new access control API AND Sanity UI at the same time. Unfortunately, we cannot distribute this plugin as it's highly tailored to our use case. And there is a huge drawback to, which is that we cannot restrict which members can see this tool and access it in the toolbar. As a workaround, we display a warning message when a member with insufficient access levels tries to access it.

Our custom User Management tab in Sanity Studio