CoursesUsers, roles and using rolesCustom roles and resources

Users, roles and using roles

Lesson
3

Custom roles and resources

Set up content resources and roles to meet your requirements around security, compliance, workflows and user experience
Log in to mark your progress for each Lesson and Task

Each plan has a number of default roles. These have predefined permissions which are applied to all datasets within the project. They also have default project-level permissions, such as which roles can create API tokens for the project.

Whilst these default roles cover many common role-based workflows (such as draft → review → publish), for many of the use cases above, it’s necessary to configure custom roles.

It’s also important to note that users can have multiple roles. This is particularly useful in combining custom roles to create unique combinations of permissions.

Core to being able customize content operations to meet your requirements around security, compliance, workflow and user experience are custom roles.

See our documentation on Roles, all roles can be configured at sanity.io/manage

Custom roles are made up of two key elements:

  • Management permissions: control over the changes a role can make to project settings - like API/webhook configuration, dataset management and user access.
  • Content permissions: control over which roles have permissions to make changes to certain content resources. These content permissions can be granted across all datasets, a group of tagged datasets, or an individual dataset.
Tagged datasets – as with custom roles – are an enterprise only feature.

In many scenarios with custom roles, it may be that requirements involve removing the ability for a user to see content. This means that it might be necessary to set the dataset to private. In a public dataset, all documents are readable by all users regardless of authentication. That means documents you may want to hide will still show up in the Studio search as well as in public API calls (when published).

If in doubt, it's safest to make your dataset private. Just remember that your front-ends will then need to make authenticated calls and you'll need to consider securing your API token.

When removing access to certain content, it's important to remember that default roles grant access to everything in a dataset, rather than scoped access. Combining roles without consideration could unintentionally give incorrect access levels.

Whilst the default roles apply to all content in a dataset, custom roles support applying permissions to a subset of content in a dataset. This is done by creating content resources.

Content resources are essentially a set of documents in a dataset, defined by a GROQ filter.

This provides a high level of flexibility to assign permissions not just to particular document types, but to filtered scopes, too.

Let’s consider a few examples based on the common use cases we outlined earlier.

Let’s say we have a document type called “article”, which our editorial team want to lock down to prevent edits from users in our merchandising team that should only manage our product catalogue.

A GROQ filter could create an “Article” content resource:

Article Content Resource
_type == "article"

Below shows how this configuration would look at sanity.io/manage:

Using this, we could create a role to target all article types. However, if our editorial team wanted to add further permissions to ensure that embargoed content could be seen only by managers in the editorial team, they could add an “embargoed” boolean field to their schema and create an “Embargoed Article” resource:

Embargoed Article Content Resource
_type == "article" && embargoed == true

Consider creating a “Non-Embargoed Article” content resource to explicitly exclude the embargoed articles. A grant simply on “Article” would include all articles. This illustrates a key concept that roles are additive.

Non-Embargoed Article Content Resource
_type == "article" && embargoed != true

Using these content resources, we could create new roles to assign these content types to our users, which might look something like:

  • Article: No Access
  • Embargoed Article: No Access
  • Unembargoed Article: Publish
  • Product: Publish
  • Article: Publish
  • Embargoed Article: Publish
  • Product: Publish

Technically, the “Embargoed Article” permission is not needed as the simpler “Article” resource gives publish access to all articles. However it can be good to positively add this as a future-facing permission - this also ensures visibility of the non-embargoed articles.

When embargoing content, you might also want to consider assets. Whilst asset documents can be hidden from API calls with roles, the direct URL of the asset itself is not authenticated. Usually the autogenerated URL of the asset provides enough security through obscurity – but the file itself is publicly accessible, even in private datasets.

It might be that you have legal documents stored within your dataset, and want to restrict the ability of users not in your legal team from making changes to these documents.

In this case, you might create a “Legal Policy” content resource:

Legal Policy Content Resource
_type == "policy"

Let’s say we want to make it so each lawyer in our team is ultimately responsible for the documents they create. In this scenario, we might have a field on our document which declares the user ID of the user that created our document. We could create a “My Policies” content resource:

My Policies Content Resource
_type == "policy" && createdBy == identity()

In this case, we can see how we can use GROQ functions in the context of our content resources.

The createdBy field above isn’t a system field - it must be added to your Sanity Studio schema, and populated with an initial value. More on this in the Studio Customizations lesson of this course.

Now we could create a single role to ensure lawyers can see and edit all policies, but only publish their own:

  • Legal Policy: Update and Create
  • My Policies: Publish

In this example, we’ll cover a couple of scenarios. One in which you have a multilingual setup and want to restrict access to documents of a particular language. The other is where documents might belong to a particular location - let’s say a particular store.

Let’s create an “English Document” content resource which will cover all documents in English:

English Document Content Resource
language == 'en' // this could be 'en-gb' or 'en-us'

You might notice in this case we don’t add a type, as we want this content resource to simply look at the language field and control access for all English content, regardless of its _type.

For the store example, imagine a store document type, with each document representing a store location. Each store needs to have a number of document types associated with it - we have types for offer, person and product. Offers and people belong to a single store, but a product can belong to many stores - therefore offers and people have a single reference field called store whereas product has an array of references called stores[] .

In this scenario, you might assume to create a “Tom’s Toy Store Manager” role you can follow the reference in the content resource GROQ query… but this won’t work:

Tom’s Toy Store Content Resoure
// This won't work...
store->name == "Tom's Toy Store" || "Tom's Toy Store" in store[]->name

This won’t work because content resources can only be based on values within a document, and therefore cannot resolve references.

Instead you’ll need to know the ID of the document for Tom’s Toy Store and use this in the query instead:

Tom’s Toy Store Content Resource
// This will work
store._ref == "toms-toy-store-id" || "toms-toy-store-id" in store[]._ref

With the latter, you could create the “Tom’s Toy Store” content resource, and then apply it to your “Tom’s Toy Store Manager” role as necessary.

Mark lesson as complete
You have 1 uncompleted task in this lesson
0 of 1