Permissions
Keel takes a secure by default approach, which means that unless you explicitly define permission rules all your actions will return permission denied errors. Keel supports both row-based and role-based permission rules at the model or individual action level.
A role-based permission rule allows anyone who has one of the given role to perform any of the specified action types.
model MyModel {
@permission(
roles: [Staff],
actions: [get, create, update, delete]
)
}A row-based permission rule is based on the data being acted on and uses an expression to determine if the action is allowed.
model MyModel {
@permission(
expression: post.author.identity == ctx.identity,
actions: [update]
)
}@permission attribute
The @permission attribute can be used at model or action level and can be used to define row-based or role-based permission rules. It accepts three arguments: actions, expression, and roles.
Here's an example of model level permission rules:
model Post {
fields {
author Author
}
// Permissions for each row on the whole model
@permission(
expression: post.author.identity == ctx.identity,
actions: [update]
)
}Actions
The actions argument must be provided when the rule is defined at the model level, and is a list of action types that the rule should apply to. For action level permission rules this argument must be omitted.
Here's an example of action level permission rules:
model Post {
fields {
author Author
}
actions {
update updatePost(id) with (title)
// Permission nested inside the update action
@permission(
expression: post.author.identity == ctx.identity
)
}
}Expressions
The expression argument is for defining row-based permissions, and the provided expression is evaluated against each record being acted on. An expression is boolean. For example, here's an expression that checks if the authenticated user is the author of a post:
model Post {
fields {
author Author
}
@permission(
expression: post.author.identity == ctx.identity,
actions: [update]
)
}Similarly, for a list action the expression is evaluated for each record in the list. For example, the following schema only allows users to list posts that they are the author of:
model Post {
fields {
author Author
}
@permission(
expression: post.author.identity == ctx.identity,
actions: [list]
)
}Some key things to understand are:
- There can be many permission rules in a model or action
- Action-level rules override model-level rules
- If any permission rule passes, the action is allowed
If provided the expression argument has the following in-scope identifiers:
- the record, available as the lowerCamelCase version of the model name
ctx- contains things like the authenticated identity, environment variables & secrets- any
enumdefined in your schema
If the expression evaluates to true for all records being acted on then the rule passes.
Roles
The roles argument is for role-based permissions and lets you define a list of role names that the rule should allow. The caller must have one of the provided roles.
Roles are defined in your schema and are applied to the authenticated Identity based on either a specific email address or an email domain name. For example the following schema says that anyone with a @mycorp.com email address has the Staff role but only bob@mycorp.com and sue@mycorp.com have the Developer role.
role Staff {
domains {
"mycorp.com"
}
}
role Developer {
emails {
"bob@mycorp.com"
"sue@mycorp.com"
}
}You can then use these role names in permission rules. As an example, the following schema allows anyone with the Staff role to update an order:
model Order {
fields {
status Text
}
actions {
update updateOrder(id) with (status)
}
@permission(
roles: [Staff],
actions: [update]
)
}Public access
You can make a permission rule that will always pass by using the expression true. The following schema allows public access to all actions in the Post model:
model Post {
actions {
get getPost(id)
list listPosts(createdAt)
create newPost() with (title) @function
}
@permission(
expression: true,
actions: [get, list, create]
)
}You can use the same approach to make a specific action public. For example, the following schema makes the getPost action public:
model Post {
actions {
get getPost(id) {
@permission(expression: true)
}
}
}Using Identity
A common permissions pattern is to only allow the owner/creator of some data to modify it. The following schema lets any authenticated user create posts, but only the author of a post can edit or publish a post.
model Post {
fields {
body Text
publishedAt Timestamp?
author Identity
}
actions {
create createPost() with (body) {
// set the author to the authenticated Identity
@set(post.author = ctx.identity)
}
update editPost(id) with (body)
update publishPost(id) {
@set(post.publishedAt = ctx.now)
}
}
// Any authenticated user can create
@permission(
expression: ctx.isAuthenticated,
actions: [create]
)
// Only the author of a post can update
@permission(
expression: post.author == ctx.identity,
actions: [update],
)
}Relationships
You'll generally only add an Identity field to the model that represents users in your app, which means that you'll often write permission rules that need to traverse your data model. Luckily this is very easy to do with expressions.
Imagine you're building a project management app which has users, projects, and tasks. Users can create projects and tasks can be added to projects. You only want the owner of a project to be able to update a task. To do this we just need to write a permission rule in the Task model that joins to the User and checks the identity field against the authenticated Identity calling the action.
model User {
fields {
identity Identity @unique
}
}
model Project {
fields {
user User
}
}
model Task {
fields {
project Project
}
@permission(
expression: task.project.user.identity == ctx.identity,
actions: [update]
)
}Many-to-many Relationships
In the previous example we saw how we can implement permission rules based on the owner (or creator) of some data, but what if we want to allow many people permission to edit some data. To illustrate this let's extend our project management app to allow for collaboration between users. Now many users can access a project and anyone with access to a project can create tasks in it.
model User {
fields {
identity Identity
projects UserProject[]
}
}
model Project {
fields {
users UserProject[]
}
}
model UserProject {
fields {
user User
project Project
}
}
model Task {
fields {
project Project
}
// allow any user with access to the project to create a Task
@permission(
expression: ctx.identity in task.project.users.user.identity,
actions: [create]
)
}This schema has a many-to-many relationship between users and projects, represented by the UserProject model, and so the expression task.project.users.user.identity does the following:
- Starting at the
Taskrecord (in this case the one being created), go to the parentProject - From the
Projectgo to the list ofUserProject's - From each
UserProjectgo to theUser - From each
Userget theidentityfield
The important thing here is the “for each” part of the last two steps. Because a project has many users we ultimately end up with a list of Identities. This means we can't compare ctx.identity to task.project.users.user.identity with == as they are different types. For this case we need to use the in operator to say that the authenticated Identity must be in the list of Identity's with access to the project.
Functions
Permission rules apply to both built-in actions and functions, and you can add permission rules to specific functions too. The only exception to this is for read and write functions - schema defined permission rules do not apply to these functions and so you must implement any permissions logic in your code. See the docs on custom functions for more info.
Examples
Model with permission attributes
model Employee {
fields {
name Text
department Text
isActive Boolean
}
@permission(
roles: [Manager],
actions: [create, update, delete]
)
@permission(
expression: employee.department == "HR",
actions: [read]
)
}In this example:
- Users with the
Managerrole can create, update, and delete employees. - Any user in the "HR" department can read employee data.
Action with permission attribute
model Document {
fields {
title Text
content Text
owner Identity
}
actions {
update updateDocument(id, content) {
@permission(
expression: ctx.identity == document.owner,
)
}
}
}Here, the updateDocument action can only be performed by the user who owns the document.
Permissions with complex expressions
@permission(
expression: (post.user.role == "Admin" || post.user.role == "Editor") && post.isPublished == false,
actions: [update, delete]
)This permission allows users with the Admin or Editor role to update or delete items that are not yet published.
Examples
Model with fields, actions, and permissions
model Post {
fields {
title Text
content Markdown
author Identity
}
actions {
create createPost(title, content) {
@function
}
update updatePost(id, title, content) {
@function
@permission(
expression: ctx.identity == post.author,
actions: [update]
)
}
delete deletePost(id) {
@function
@permission(
roles: [Admin],
actions: [delete]
)
}
}
@permission(
actions: [read],
roles: [Viewer]
)
}Role definitions
role Admin {
emails {
"admin@keel.xyz"
}
}
role Editor {
domains {
"keel.xyz"
}
}
role Viewer {
domains {
"public.keel.xyz"
}
}Model with role-based permissions
model Procedure {
fields {
name Text @unique
}
actions {
get getProc(name)
create doProcedure() with (name) {
@permission(roles: [Surgeon])
}
}
}
role Surgeon {
domains {
"barts.org"
}
emails {
"sam.brainsurgeon@gmail.com"
"sally.heartsurgeon@gmail.com"
}
}In this example:
- The
Proceduremodel defines adoProcedureaction that is restricted to users with theSurgeonrole. - The
Surgeonrole is assigned to specific domains and emails.
Model with complex permission expressions
model Post {
fields {
title Text?
views Number?
identity Identity?
}
actions {
create create() with (title, views)
create createUsingRole() with (title) {
@permission(roles: [Poster])
}
get get(id)
update update(id) with (title)
delete delete(id)
}
@permission(
expression: post.title == "hello",
actions: [create, get, update]
)
@permission(
expression: post.views == 5,
actions: [get]
)
@permission(
expression: true,
actions: [delete]
)
}
role Poster {
domains {
"times.co.uk"
}
emails {
"chiefEditor@dailygazette.com"
"sportsWriter@athleticnews.net"
"techReporter@futuretech.io"
"foodCritic@culinaryreview.org"
"travelBlogger@wanderlust.com"
}
}In this example:
- The
Postmodel has multiple@permissionattributes with expressions based on field values. - The
createUsingRoleaction is accessible only to users with thePosterrole. - The
Posterrole is defined with specific domains and emails.
Action with database field comparison in permissions
model Task {
fields {
title Text
project Project
}
actions {
get getTask(id)
}
@permission(
actions: [get],
expression: (
ctx.identity in task.project.users.user.identity &&
"Admin" in task.project.users.role
)
)
}
model Project {
fields {
name Text
users ProjectUser[]
}
}
model ProjectUser {
fields {
user User
project Project
role Text
}
}
model User {
fields {
name Text
identity Identity
projects ProjectUser[]
}
}In this example:
- The
Taskmodel'sgetTaskaction is restricted by a complex@permissionexpression. - The permission checks if the current user's identity is part of the task's project's users and whether they have the "Admin" role.
Using enums in permissions
enum PostType {
Technical
Lifestyle
Food
}
model Post {
fields {
title Text
type PostType
}
actions {
get getPost(id) {
@permission(expression: post.type == PostType.Technical)
}
}
}In this example:
- The
Postmodel uses an enumPostType. - The
getPostaction is permitted only when the post's type isTechnical.
Permission with identity comparison
model Post {
fields {
title Text
identity Identity
}
actions {
get getPost(id) {
@permission(expression: post.identity == ctx.identity)
}
}
}In this example:
- Access to the
getPostaction is granted only if the post'sidentitymatches the current user's identity (ctx.identity).
Jobs with permissions
job ManualJob {
inputs {
id ID
}
@permission(roles: [Admin])
}
job ManualJobTrueExpression {
inputs {
id ID
}
@permission(expression: true)
}
role Admin {
domains {
"keel.so"
}
}In this example:
- The
ManualJobjob can only be executed by users with theAdminrole. - The
ManualJobTrueExpressionjob is allowed for all users due to theexpression: truepermission. Jobs are only accessible via the console (unlike actions which are accessed via API) so thetrueexpressions represents all users with project access.