# Custom roles and resources https://www.sanity.io/learn/course/introduction-to-users-and-roles/custom-roles-and-resources.md Set up content resources and roles to meet your requirements around security, compliance, workflows and user experience ## Default roles Each plan has a number of [default roles](https://www.sanity.io/docs/roles#e2daad192df9). 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. ## Custom roles Core to being able customize content operations to meet your requirements around security, compliance, workflow and user experience are _custom roles._ 1. See our documentation on [Roles](https://www.sanity.io/learn/user-guides/roles), all roles can be configured at [sanity.io/manage](https://www.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. 1. Tagged datasets – as with custom roles – are an enterprise only feature. 2. When configuring custom roles, you may need to assign certain permissions for handling generation of preview tokens. See the [Visual Editing readme](https://github.com/sanity-io/visual-editing/blob/main/packages/preview-url-secret/README.md#permissions-model) for the latest information. ### **Dataset privacy** 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](https://www.sanity.io/docs/keeping-your-data-safe#5c2e941ea03c). 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](https://www.sanity.io/docs/http-auth#504058b73b71). 1. 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. ## Content resources 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. 1. **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. ### Example - Embargoed Articles 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: ```groq:Article Content Resource _type == "article" ``` Below shows how this configuration would look at [sanity.io/manage](https://www.sanity.io/manage): ![A custom content resource targeting article documents](https://cdn.sanity.io/images/3do82whm/next/0a425bbe7dbfde3081f915d25e1666db602d5e08-1609x1620.png) 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: ```groq: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. ```groq: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: #### **Role: Creator Team** * **Article:** No Access * **Embargoed Article:** No Access * **Unembargoed Article:** Publish * **Product:** Publish #### **Role: Article Editor** * **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. 1. When embargoing content, you might also want to consider assets. Whilst [asset documents](https://www.sanity.io/docs/assets#2cee91f4f62d) 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. ### Example - Legal Policies 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: ```groq: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: ```groq: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. 1. 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: #### **Role: Legal Team** * **Legal Policy:** Update and Create * **My Policies:** Publish ### Example - Locales 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. #### Languages Let’s create an “English Document” content resource which will cover all documents in English: ```groq: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`. #### Locations 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: ```groq: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: ```groq: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.