Access control 

Gadget has a built-in, role-based access control system. As you build out models and actions, Gadget auto-generates permissions that manage who can read data or run those actions. To read data or run an action, the actor making the request must be granted the specific permission to read that data or run that action. Actors are granted permissions by assigning roles to an actor. API keys are one type of actor -- they can be assigned roles within the Gadget editor quickly and easily. For more advanced use cases, like a full user-facing authentication system, specific records can be granted roles, which allows you to dynamically assign roles at runtime.

Roles 

A Gadget app has one global list of roles that governs permissions across the application. Each role is a set of permissions that allow or deny access to a particular resource. People or systems that want to use a Gadget application must be assigned roles that grants them a set of permissions.

By default, every Gadget project is given three distinct roles:

  • Writers: The writer role is intended for users and API keys that should have both read and write access. Writers are granted read and action permissions on all of your public API endpoints by default.
  • Readers: The reader role is intended for users and API keys that should have read-only access. Readers are not able to run actions. Gadget automatically assigns read permissions on all new models to the reader role by default.
  • Unauthenticated: The unauthenticated role governs what an unauthenticated session or API key may access. By default, an unauthenticated session can't read or write any data other than the session itself. Gadget does not automatically assign any other permissions to this role.

New roles can be added, or the default roles can be deleted if you want a different structure for your permissions system.

Permissions 

Every role, regardless of whether it's assigned to an API key or a user, is given a set of permissions. Each permission is represented by a checkbox in the Roles and Permissions screen. When the given operation's checkbox is checked, the users and API Keys who have that role assigned become allowed to perform the operation.

Permissions are automatically generated for your models. Every time you create a new model, Gadget automatically generates distinct permissions for reading the model (the read permission), as well as one permission per action for that model. For a default model with unchanged CRUD actions, the model will generate create, update, and delete permissions that can be given to the appropriate roles. If you add custom actions to a model, Gadget will also generate permissions for them.

Default permissions 

You can automatically grant permissions to new models by using Permission Defaults.

The Default Read: On selection tells Gadget to automatically enable read permissions on all new models. The System Admin, Writer, and Reader roles that come with every project are configured to be granted read permissions on all new models. The Default Actions: On selection tells Gadget to automatically enable permissions on all actions. The System Admin and Writer roles are configured to be granted action permissions on all new models.

Filtered model permissions 

You can grant certain roles permission to view only some records for a model by adding a filter snippet. Filter snippets can include all sorts of custom logic, like making sure a user is active, a user is in a certain group, or a Shopify session is only accessing data for their particular shop.

Model filters in Gadget

Different filters can be assigned to different roles, so you can allow administrators to view all the data in the system while restricting what data normal users (or unauthenticated users) can see.

Model filters are applied before any incoming filters from API calls, so they fully restrict what data a user can see, without them being able to "escape" the filter. If an API call also provides its own filters, searches, or sorts to further limit the set of data returned, those filters are applied in addition to the model filters.

Model filters are added by clicking the + Filter button next to an enabled permission for any model's Read or Action permissions. Within the file selector, you can pick an existing filter snippet or create a new one.

Writing filter snippets 

Filter snippets are authored in Gadget's expression language, Gelly, so they can be scaled by the Gadget platform while still allowing custom or complex filtering logic. Each filter snippet file is expected to contain one Gelly fragment. For example, if we're building a blog application with a Post model, we can create a filter snippet that only lets users see Post records that have the published field set to true.

Only show published posts
gelly
fragment Filter($session: Session) on Post {
*
[where published]
}

When this snippet is applied to a role, and a user with that role makes a request to read the posts, this filter will always be applied. This means the user will only be able to see posts that have been published.

Model filters can use boolean logic, access multiple fields of the model, and even traverse relationships of the model, just like any other snippet of Gelly. For example, let's say the Post model has a belongs to relationship to an Author model, as well as a boolean Published field and a boolean Archived field. We can update the post filter to only allow reads of posts with:

  • a published field set to true
  • an archived field set to false
  • and an author that has not been banned
Only show published posts from not banned authors
gelly
fragment Filter($session: Session) on Post {
*
[where published && !archived && !author.isBanned]
}

When a user with this role goes to fetch the post model, the system will examine the live values of the published and archived fields of the Post model and do a database join to examine the isBanned field of the Author model to filter down the returned set of posts.

Using properties of the session 

Model filter snippets can filter data based on the identity of the session making the request. On each request, model filter fragments are passed a variable for the session making the request, allowing for the filtering of visible data by accessing the properties of the session.

For example, we could give blog post authors the ability to update only the blog posts they have written. If we have a Post model with a belongs to relationship to a User model and a belongs to relationship to the same User model on our Session model, we can add a filter to the Post Update action:

Only allow posts authored by the session's current user
gelly
fragment Filter($session: Session) on Post {
*
[where userId == $session.userId]
}

This filter ensures that the Post's userId field (generated by the belongs to relationship) matches the requesting user's id field.

The same strategy can be used to build multitenant access control schemes within Gadget. For example, we could build a platform that supports many different blogging teams by adding a Team model representing each group using the system and adding belongs to relationships to both the Post and User models. With this in place, we can then ensure users can only read posts for their team by adding a filter to the Post Read permission:

Only show posts from the current user's team
gelly
fragment Filter($session: Session) on Post {
*
[where teamId == $session.user.teamId]
}

This filter ensures that the teamId field on the Post model matches the current user's teamId field, ensuring that different teams (tenants) can't see each other's data.

Requests authenticated using API Keys don't have an associated User or Session. If you assign an API key a role that has a model filter snippet, that snippet will always receive null for the value of the $user or $session variables. This can cause where conditions in the model filter to fail and return no data!

Filtering field access 

Model filters can also be used to determine which fields of a model a role can access. Most model filter fragments select * to pass through all the fields of the model, but you can also select specific fields to limit the set.

For example, we might want to hide internal-facing fields like published or archived from a Post model from the outside world.

Prevent access to internal fields
gelly
1fragment Filter($session: Session) on Post {
2 id
3 title
4 body
5 author
6 authorId
7 [where published && !archived]
8 }

When a request is made with a role using this filter, the requester will get a GGT_PERMISSION_DENIED error if they try to access fields that aren't listed in the filter.

Shopify model filters 

Gadget has built-in functionality for authenticating and authorizing requests made by a Shopify App. Gadget apps using the Shopify connection will have a Session model with a belongs to Shop model field. When a session is started using Shopify's OAuth authentication, the session will have this field populated with a reference to a shop record.

By default, Gadget will assign these sessions the Shopify App role and will set up default model filter snippets that ensure the session can only access data for its particular shop. This ensures that your multitenant Gadget application can safely store data and serve requests for multiple shops.

Gadget automatically manages the model filters for models owned by the Shopify connection, but not for models that you have created. Models that should be governed by the same access control as the Shopify connection data should have model filter snippets added to the permissions of the Shopify App role. This ensures users from different shops can't see each other's data for your custom models.

API key roles 

Every API Key created in the Gadget Editor has a list of roles that entitle it to perform actions within the application. System Admins can change each API Key's role list, or revoke the key altogether, preventing any future use.

We recommend using API Keys to implement secure system-to-system communication with a Gadget application. API Keys are great as a simple way to write a script to import data from your local development machine or as a secure way to move high volumes of data from another production system. API Keys are secrets, so care should be taken to ensure they are stored securely and never shared with untrusted parties.

API Key role lists can be changed, but they can't be modified dynamically at runtime. If you need a wide variety of different permissions for different actors in your system, consider creating one API Key for each actor, or using a model with the role list field type.

Record roles with the role list field type 

To represent individual entities that are entitled to a certain degree of access, individual records of a model in Gadget can also be assigned Roles. This is accomplished by using the role list field type. Role Assignments are like any other field in Gadget -- they can be stored and retrieved, manipulated via API calls or Action effects, edited in the editor, etc. Once a record has a role list field type, that field stores a list of Role keys, and they can now make requests and be entitled to perform the actions granted to those roles.

Role assignments allow you to build traditional authentication systems with individual users who each have a different set of roles. You can create this system by adding a User model, and you can model your User model however you wish -- rename it, change its fields, add a second copy for a different set of people, or do whatever you need to solve your problem. If the model has a role list field, it can be a request actor.

Authentication 

Often the roles that a specific request actor might have are sensitive -- not just anyone should be able to make a request with those roles and touch their stuff. For this reason, models with role list are often protected by an authorization system, where API calls need to prove they are who they say they are before they are allowed to take any actions.

Gadget creates each new application with a Session model to manage this authorization process by default. The default Session model doesn't implement actions for logging in and logging out -- that's up to you (or Gadget's built-in Connections to manage). The default Session model is intended as a starting point to allow you to get going quickly. The Set User and Unset User effects can be used to implement login and logout on a different model or different actions within the Session model.

If you're using the Shopify Connection, the connection manages the Session model for each merchant automatically in tandem with the @gadgetinc/react-shopify-app-bridge package on the frontend. Read more about automatic session manage in the Shopify App Frontends guide.