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:
- The operation type ("create", "read", "update", "delete", or "all")
- 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
- Be explicit about your permissions
- Use
auth().id
to reference the current user - Consider using roles for more complex permission structures
- 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.