Skip to main content

Roles & Permissions

Marblism integrates powerful authorization and permissions system.

Simple Admin use-case

If your app have users and admin, everything is already built-in. The data model User has a globalRole attribute that determines wether a user is USER or ADMIN.

Verify User Admin Status

import { useUserContext } from '@/core/context'

export default function ProfilePage() {
const { user, checkRole } = useUserContext()

return <>{checkRole('ADMIN') && <div>You are admin!</div>}</>
}

Protect back-end for Admin only

models/document.zmodel

model Document {

id String @id @default(uuid())
url String

// allow only admin to create documents
@@allow("create", auth().globalRole == 'ADMIN')
}

Marketplace use-case

Let's say you have a marketplace with Freelancers and Clients. Here is how to make it work.

Set-up

In the register page, add a field where user can choose between 'Freelancer' and 'Client'

app/routes/_auth.register_/route.tsx

<Form.Item label="Role" name="globalRole">
<Radio.Group
options={[
{ label: 'Freelancer', value: 'FREELANCER' },
{ label: 'Client', value: 'CLIENT' },
]}
optionType="button"
buttonStyle="solid"
/>
</Form.Item>

Usage

Check if a User is a Freelancer in the front-end:

import { useUserContext } from '@/core/context'

export default function ProfilePage() {
const { user, checkRole } = useUserContext()

return <>{checkRole('FREELANCER') && <div>You are a freelancer!</div>}</>
}

Protect a back-end endpoint

models/document.zmodel

model Document {

id String @id @default(uuid())
url String

// allow only freelancers to create documents
@@allow("create", auth().globalRole == 'FREELANCER')
}

SaaS Use-case

When you pick "Multi Organisation" in the modules during the set-up process, it comes already with a multi-tenant system where users can create different Organizations, invite other users in these Organizations and define different roles for their organizations members.



To secure the back-end of your SaaS you will have to edit your models with the relevant business logic, for example:

model Document {

id String @id @default(uuid())
url String
topSecret Boolean
userId String
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], name:"organization")
user User @relation(fields: [userId], references: [id], name:"user")

// allow members of a organization to upload a document to this organization
@@allow("create", organization.roles?[user == auth()])

// allow only owners of a organization to upload a document to this organization
@@allow("create", organization.roles?[user == auth() and name == 'OWNER'])

// allow the user to edit its own tweets
@@allow("update", auth() == user)

}

Change the navigation based on a role

Go to app/designSystem/layouts/NavigationLayout/index.tsx that's where you define all the left/top bar navigation links.

To make a link visible only to a specific role, you can update the isVisible attribute

const {checkRole} = useUserContext()

const items: NavigationItem[] = [
{
key: '/home',
label: 'Home',
position: 'leftbar',
onClick: () => goTo('/home'),
},
{
key: '/freelancers',
label: 'Freelancers',
position: 'leftbar',
onClick: () => goTo('/freelancers'),

isVisible: checkRole('FREELANCER')
},
{
key: '/admin',
label: 'Admin only',
position: 'leftbar',
onClick: () => goTo('/admin'),

isVisible: checkRole('ADMIN')
},

Permissions

Your data models is the source of truth of your permission system.

You just have to define how you want to protect your different endpoints ("create", "update", "read", "delete" or "all") and all your API endpoints will be protected.

model Tweet {

id String @id @default(uuid())
content String?
userId String
user User @relation(fields: [userId], references: [id], name:"user")

// allow everyone to create a tweet
@@allow("create", true)

// allow the user to edit and delete its own tweets
@@allow("update, delete", auth() == user)

// allow an admin to do everything
@@allow("all", auth().roles?[name == 'admin'])
}

The auth() function represents the currently authenticated user. It allows you to reference the current user's properties in your permission rules.

Using @@allow Decorators

The @@allow decorators define access rules for your models. They take two arguments:

  1. The operation type ("create", "read", "update", "delete", or "all")
  2. A boolean expression that determines if the operation is allowed

Here's an example from the User model:

model User {
// ... other fields ...

@@allow("all", auth().id == this.id)
@@allow('all', auth().globalRole == 'ADMIN')
@@allow("create", globalRole != 'ADMIN')
@@allow("read", true)
}

Let's break down these rules:

  • @@allow("all", auth().id == this.id): Allows users to perform all operations on their own record
  • @@allow('all', auth().globalRole == 'ADMIN'): Allows admins to perform all operations on any user record
  • @@allow("create", globalRole != 'ADMIN'): Allows any user to create non-admin users
  • @@allow("read", true): Allows any user to read information of all user records

Protecting Other Models

To protect other models, you can use similar @@allow decorators. For example:

model Post {
id String @id @default(uuid())
title String
content String
authorId String
author User @relation(fields: [authorId], references: [id])

@@allow('all', auth().id == authorId)
@@allow('read', true)
}

This setup:

  • Allows users to create, update, and delete their own posts
  • Allows anyone to read posts

Remember to always start with the most restrictive permissions and then add exceptions as needed.

Best Practices

  1. Be explicit about your permissions
  2. Use auth().id to reference the current user
  3. Consider using roles for more complex permission structures
  4. Test your permissions thoroughly to ensure they work as expected

By understanding and properly implementing these concepts, you can create a secure and flexible permission system for your application.

Automatic Permission Application

It's important to note that these permissions are automatically applied when querying the API. This means that when you query the database using this client, the results are automatically filtered according to the logged-in user and the permissions applied to the model.

For example, consider the following query:

const { data: posts } = await Api.post.findMany.useQuery()

In this case, the findMany() query will only return posts that the current user has permission to read, based on the rules defined in the Post model.

Advanced Permissions

For more complex use cases, refer to the Zenstack documentation.