RBAC Scopes
In this guide, you'll learn what an RBAC scope is, when to use one, and how to add scopes to your application.
What is a Scope?#
A scope restricts a role assignment to a single entity instance. Without a scope, an assignment applies everywhere: a user with the "Product Manager" role manages every product in the store.
With a scope, the same role only applies within one instance of an entity that you own, such as a single vendor on a marketplace. The user manages that vendor's products, and nothing else.
A scope is a type and an id, such as the type vendor and the ID ven_123. When a request acts within a scope, Medusa resolves the actor's roles from the assignments that are either unscoped or scoped to that exact pair. So an unscoped assignment always counts, which is how a Super Admin keeps access in every scope.
Learn more about assignments and how their scope and scope_id properties are stored in the RBAC Concepts guide.
When to Use Scopes#
Scopes are useful when the same role means different things in different parts of your store, and one user shouldn't hold it everywhere. For example:
- A marketplace where each vendor manages its own products and orders.
- A multi-brand business where a manager runs one brand's catalog.
- An agency that operates a subset of your stores or regions on behalf of a client.
If every role in your application applies to the whole store, you don't need scopes. Medusa's own API routes never put a request in a scope, so roles work without any scope configuration.
You can assign scoped roles to any actor, including admin users, such as with the POST /admin/users/:id/roles API route. However, since Medusa's own routes never set a scope, a scoped role only takes effect on the routes where your middleware sets a matching scope.
Add Scopes to Your Application#
To restrict roles to a scope, you need the following:
- Configure the actor type in the RBAC Module's options, so Medusa knows which entities hold the actor's role assignments. You only need this for actor types of your own, such as a
vendor_admin. - Set the scope of each request in a middleware, so Medusa knows which entity the request acts within.
- Assign roles restricted to a scope as you onboard users.
The first two steps are one-time configuration. A scoped assignment only grants its role's policies on requests whose scope matches it. On a request without a scope, or with another scope, the assignment grants nothing.
1. Configure the Actor Type#
The RBAC Module's actors option, set in medusa-config.ts, tells Medusa which of your actor types can hold roles and which entities hold their role assignments. An actor type without an entry resolves to no roles at all.
For example, to let a vendor_admin actor hold roles:
module.exports = defineConfig({
// ...
modules: [
{
resolve: "@medusajs/medusa/rbac",
options: {
actors: {
vendor_admin: {
grantees: [
{
entity: "vendor_admin",
path: "id",
},
],
},
},
},
},
],
})
On every request, Medusa queries the actor with the paths of its grantees, collects the IDs it reaches, and looks up the assignments that match them. Medusa queries the entity set in the actor entry's actor_entity property, or the actor type itself when you don't set it.
Each grantee in the grantees array accepts the following properties:
entity is user and whose path is id to the user actor type, and keeps any grantees you configure for it. So, you can add grantees to admin users, such as a team they belong to, and they gain the roles assigned to that team.2. Set the Scope of a Request#
Medusa never puts a request in a scope. A request acts within a scope only when a middleware you write sets req.rbac_context.scope. Where the scope's ID comes from is up to you, such as a request header, the authenticated actor, or a path parameter.
List the middleware before the route's authorize middleware, since Medusa runs a route's middlewares in the order you list them. Since the route isn't under /admin, also add the authenticate middleware first, so the request has an authenticated actor.
For example, to scope a request to the vendor passed in a header:
import {
authenticate,
defineMiddlewares,
authorize,
} from "@medusajs/framework/http"
export default defineMiddlewares({
routes: [
{
matcher: "/vendors/:id/products",
methods: ["POST"],
middlewares: [
authenticate("vendor_admin", ["bearer", "session"]),
(req, res, next) => {
req.rbac_context = {
...req.rbac_context,
scope: {
type: "vendor",
id: req.headers["x-vendor-id"] as string,
},
}
return next()
},
authorize({
resource: "product",
operation: "create",
}),
],
},
],
})
A vendor_admin can now create products for the vendor in the header, but only if one of their role assignments is unscoped or scoped to that vendor.
Refer to the Protect API Routes guide for more ordering rules, and the Share a Scope Middleware Across Routes section to learn how to set the scope for many routes at once.
3. Assign a Scoped Role#
To restrict an assignment to a scope, pass the scope property to the assignRolesWorkflow. You run this every time a user gains a role, such as when a vendor adds a member to its team, so it happens once per user and role rather than once per application.
For example, the following workflow creates a vendor admin, then grants it a role scoped to the vendor it belongs to:
import {
createWorkflow,
WorkflowResponse,
WorkflowData,
} from "@medusajs/framework/workflows-sdk"
import {
assignRolesWorkflow,
} from "@medusajs/medusa/core-flows"
import {
createVendorAdminStep,
} from "./steps/create-vendor-admin"
type Input = {
vendor_id: string
email: string
role_id: string
}
export const createVendorAdminWorkflow = createWorkflow(
"create-vendor-admin",
(input: WorkflowData<Input>) => {
const vendorAdmin = createVendorAdminStep({
vendor_id: input.vendor_id,
email: input.email,
})
assignRolesWorkflow.runAsStep({
input: {
assignments: [
{
role_id: input.role_id,
reference: "vendor_admin",
reference_id: vendorAdmin.id,
scope: {
type: "vendor",
id: input.vendor_id,
},
},
],
},
})
return new WorkflowResponse(vendorAdmin)
}
)
The assignment's reference matches the entity of the grantee you configured, and its scope uses the type that your middleware sets. So the new admin holds the role on requests that enter their own vendor's scope, and holds nothing on requests that enter another vendor's scope.
You can assign scoped roles before you add the middleware of the previous step, but the scope has no effect until a request enters it.
Learn more about assigning roles, including how to prevent a user from granting access in a scope they don't have, in the RBAC Concepts guide.