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 needs to 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 which 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 which 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 user or API key may access. By default, an unauthenticated user is only able to login/logout. Aside from the initial defaults, 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 checked, the users and API Keys who have that role assigned become allowed to perform the operation in question.

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 an unchanged CRUD statechart, 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, making sure a user is in a certain group, or making sure 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 which only lets users see Post records that have the published field set to true.

Only show published posts
gelly
fragment Filter($user: User, $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, limiting the posts that the user can see to just ones 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 BelongsTo 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($user: User, $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 user or session

Model filter snippets can filter data based on the identity of the user or session making the request. On each request, model filter fragments are passed variables for the current user and session, allowing for the filtering of visible data by accessing properties of either.

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 BelongsTo relationship to the User model, we can add a filter to the Post Update action:

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

This filter ensures that the Post's userId field (generated by the BelongsTo 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 then adding BelongsTo 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($user: User, $session: Session) on Post {
*
[where teamId == $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($user: User, $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 that's 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 BelongsTo 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 the 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 Keys 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 RoleAssignments field type.

Record roles with the RoleList 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 RoleAssignments 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 RoleAssignments 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 like a User model. 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 to solve your problem. If the model has Role Assignments, then it can be a request actor.

Gadget creates each new application with a User model set up to have RoleAssignments by default. This model can be changed or deleted if you don't need it -- it's just a starting point to help you get going quickly.

Authentication

Often the roles that a specific User might have are sensitive -- not just anyone should be able to make a request as that user and touch their stuff. For this reason, models with RoleAssignments are often protected by an authorization system, where the user needs 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 implements actions to allow Users to log in with an email / password. 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.