Define Custom Policies
In this guide, you'll learn how to define policies for your custom resources so they can be protected by role-based access control (RBAC).
How Custom Policies Work#
Medusa syncs the policies of its own resources, such as products and orders, every time the application starts. Those core policies live in the RBAC Module and you can't change them.
Policies for your custom resources work differently. You create them yourself by running the createRbacPoliciesWorkflow workflow, which stores them in the database. Medusa doesn't scan your project for policy files, so a policy only exists after you create it.
To keep the resource and operation names of your custom policies type-safe, you also register them in Medusa's type registries.
1. Register your Resources and Operations#
Medusa constrains the resource and operation of a policy to the values in two type registries. Add your custom resources and operations to those registries so that typos become compile errors.
Create the file with the following content:
declare module "@medusajs/types" {
interface PolicyResourceRegistry {
vendor: "vendor"
}
interface PolicyOperationRegistry {
approve: "approve"
}
}
export {}
You only need PolicyOperationRegistry if you use an operation other than read, create, update, and delete, which Medusa registers for you.
After this, "vendor" and "approve" are valid everywhere Medusa expects a resource or operation: the authorize middleware, buildPolicies, the RBAC Module's service methods, and the admin dashboard's permission checks.
tsconfig.json includes all the files in your project, so TypeScript picks up the augmentation file without any change. If you narrowed the include option, make sure it still matches src/types/rbac.d.ts. Also, keep the export {} line, as TypeScript only applies the augmentation if the file is a module.2. Create the Policies#
To create the policies, build them with the buildPolicies function which generates a policy for every operation on a resource, then pass them to the createRbacPoliciesWorkflow to store them in the database.
Create the policies in a data migration script, which the Medusa application runs once when you run migrations. This way, your policies reach every environment you deploy to.
For example, create the file src/migration-scripts/create-vendor-policies.ts with the following content:
import { ExecArgs } from "@medusajs/framework/types"
import {
buildPolicies,
ContainerRegistrationKeys,
} from "@medusajs/framework/utils"
import {
createRbacPoliciesWorkflow,
} from "@medusajs/medusa/core-flows"
export default async function ({ container }: ExecArgs) {
const logger = container.resolve(
ContainerRegistrationKeys.LOGGER
)
const policies = buildPolicies({
resource: ["vendor"],
})
const { result } = await createRbacPoliciesWorkflow(
container
).run({
input: { policies },
})
logger.info(`Created ${result.length} policies.`)
}
buildPolicies expands each resource you pass into one policy per operation. So, the above creates four policies for the read, create, update, and delete operations on vendors, with a key, name, and description:
- key:
vendor:read, name:ReadVendor, description:Read Vendor - key:
vendor:create, name:CreateVendor, description:Create Vendor - key:
vendor:update, name:UpdateVendor, description:Update Vendor - key:
vendor:delete, name:DeleteVendor, description:Delete Vendor
The RBAC Module also has the updateRbacPoliciesWorkflow and deleteRbacPoliciesWorkflow workflows to change or remove policies later.
name, description, and metadata. A policy's key, resource, and operation identify it, so changing them would silently change the access of every role that holds it. To change them, create a new policy instead, or update the policy from a migration script.Customize the Built Policies
buildPolicies accepts the following properties:
For example, to build a policy for a custom operation with your own name and description:
const policies = buildPolicies({
resource: "vendor",
operation: "approve",
name: "ApproveVendor",
description: "Approve a vendor before it goes live",
})
To pass name or description, set resource to a single string rather than an array. Passing them with an array of resources is a TypeScript error. When you also pass multiple operations, every built policy shares the same name and description.
You can only pass key when you build a single policy, since a key must be unique. Passing it with multiple operations throws an error.
Medusa's own operations are also available as values in the PolicyOperation constant exported from @medusajs/framework/utils, such as PolicyOperation.read or PolicyOperation.ALL for the wildcard *.
To build a single policy that allows all operations on a resource, pass the wildcard * as the operation:
Learn more about wildcard policies in the RBAC Concepts guide.
3. Run the Migrations#
To run the migration script and create the policies, run the following command:
Medusa tracks the scripts it ran, so this one doesn't run again on later migrations.
4. Guard your Routes#
A policy only restricts access once a route checks it. To guard an API route, use the authorize middleware imported from @medusajs/framework/http:
import {
authorize,
defineMiddlewares,
} from "@medusajs/framework/http"
export default defineMiddlewares({
routes: [
{
matcher: "/admin/vendors",
methods: ["POST"],
middlewares: [
authorize({
resource: "vendor",
operation: "create",
}),
],
},
],
})
Requests from admin users that don't have the vendor:create policy now receive a 403 error. When the RBAC Module isn't installed, the middleware allows every request.
5. Assign the Policies to Roles#
After creating a policy, assign it to roles. Admin users assigned a role with the policy can then perform the policy's operation on its resource.
You can manage roles and their policies:
- In the Medusa Admin dashboard.
- Or using the RBAC Module's workflows and service in your customizations.
Example: Create a Role with a Migration Script#
The following data migration script uses the createRbacRolesWorkflow to create a "Vendor Manager" role that grants the vendor policies:
import { ExecArgs } from "@medusajs/framework/types"
import {
Modules,
ContainerRegistrationKeys,
} from "@medusajs/framework/utils"
import {
createRbacRolesWorkflow,
} from "@medusajs/medusa/core-flows"
export default async function ({ container }: ExecArgs) {
const rbacModuleService = container.resolve(Modules.RBAC)
const logger = container.resolve(
ContainerRegistrationKeys.LOGGER
)
const policies = await rbacModuleService.listRbacPolicies({
key: ["vendor:read", "vendor:create"],
})
const { result } = await createRbacRolesWorkflow(
container
).run({
input: {
roles: [
{
name: "Vendor Manager",
description: "Manage vendors",
policy_ids: policies.map((policy) => policy.id),
},
],
},
})
logger.info(`Created role: ${result[0].name}`)
}
In the script, you:
- Resolve the RBAC Module's service to find the vendor policies by their keys.
- Run the
createRbacRolesWorkflowto create the role with the policies' IDs.
Then, run the migrations with the following command:
This will create the "Vendor Manager" role in the database, which you can assign to admin users in the Medusa Admin dashboard.
The RBAC Module also has workflows to update or delete roles, such as updateRbacRolesWorkflow and deleteRbacRolesWorkflow.