Skip to content

Conversation

@carlos-r-l-rodrigues
Copy link
Contributor

@carlos-r-l-rodrigues carlos-r-l-rodrigues commented Dec 16, 2025

Defining Policies with definePolicy

Custom policies are defined using the definePolicy helper, which registers them globally for use in roles and permission checks.
In Medusa projects, place policy definition files inside a policies folder (e.g., src/policies/my-policy.ts). These are automatically discovered and loaded during application startup, similar to links, workflows, and other modular components. Registered policies are synced to the database on start.

Example 1: Single policy

import { definePolicy } from "@medusajs/framework/utils"

definePolicy({
  name: "MyCustomPolicy",
  resource: "order",
  operation: "read",
  description: "Allows reading my custom implementation on orders",
})

Example 2: Multiple policies in one file

import { definePolicy } from "@medusajs/framework/utils"

definePolicy([
  {
    name: "ReadProducts",
    resource: "product",
    operation: "read",
    description: "Allows reading products",
  },
  {
    name: "CreateProducts",
    resource: "product",
    operation: "create",
    description: "Allows creating products",
  },
])

Example 3: Policy for a custom resource

import { definePolicy } from "@medusajs/framework/utils"

definePolicy({
  name: "CreateCustom",
  resource: "my_custom",
  operation: "create",
  description: "Create new custom stuff",
})

hasPermission function checks if the provided role(s) grant permission for the specified action(s).

Example 1: Single role and single action

import { hasPermission } from "@medusajs/medusa"

const canWriteProduct = await hasPermission({
  roles: "role_123",
  actions: { resource: "product", operation: "create" },
  container,
})
// Returns true if the role allows creating products

Example 2: Multiple roles and multiple actions

import { hasPermission } from "@medusajs/medusa"

const hasRequiredPermissions = await hasPermission({
  roles: ["customer_support_role", "editor_role"],
  actions: [
    { resource: "product", operation: "read" },
    { resource: "inventory", operation: "read" },
  ],
  container,
})
// Returns true only if roles grants all specified actions

Note

Introduces RBAC feature flag with policy discovery/registry and sync, migrates role inheritance to role parents and policy_id associations, adds permission checks and JWT role embedding, updates workflows/APIs, and adds link/types generation with comprehensive tests.

  • RBAC Foundation:
    • Add feature flag rbac and module wiring; enable/disable via env.
    • Introduce policy registry (definePolicy, Policy/Resource/Operation) with directory discovery and DB sync on app start; generate policy-bindings.d.ts.
    • Replace role inheritance with role parents (rbac_role_parent), and switch role-policy associations from scope_id to policy_id.
    • Add hasPermission util for role-based action checks.
  • Core Flows/Workflows:
    • Normalize policy resource/operation to lowercase.
    • Update role creation/update flows to use parent_ids and policy_id and validate user permissions.
    • Remove deprecated role-policy update/delete flows/steps.
  • RBAC Module:
    • New models/migration for rbac_role_parent; update rbac_role_policy schema/indices.
    • Repository adds recursive listing and circular parent detection.
    • Service syncs registered policies (create/update/restore/soft-delete) and enforces cycle checks.
  • API:
    • Admin RBAC routes guarded by feature flag; role-policies use policy_id; delete handled directly via service.
  • Auth:
    • JWT generation now embeds user role IDs when RBAC enabled.
  • Framework/Loaders/CLI:
    • Expose policiesLoader; load policies from project/plugins; medusa start generates container, GraphQL, and policy types.
  • Links:
    • Add UserRbacRole link and expose roles on user entity.
  • Tests/Config:
    • Integration tests for admin RBAC (policies, roles, role-policies) and workflows, including parent/permission/cycle scenarios; test config enables RBAC.

Written by Cursor Bugbot for commit 5d3d54b. This will update automatically on new commits. Configure here.

@vercel
Copy link

vercel bot commented Dec 16, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

8 Skipped Deployments
Project Deployment Review Updated (UTC)
api-reference Ignored Ignored Dec 22, 2025 0:02am
api-reference-v2 Ignored Ignored Preview Dec 22, 2025 0:02am
cloud-docs Ignored Ignored Preview Dec 22, 2025 0:02am
docs-ui Ignored Ignored Preview Dec 22, 2025 0:02am
docs-v2 Ignored Ignored Preview Dec 22, 2025 0:02am
medusa-docs Ignored Ignored Preview Dec 22, 2025 0:02am
resources-docs Ignored Ignored Preview Dec 22, 2025 0:02am
user-guide Ignored Ignored Preview Dec 22, 2025 0:02am

@changeset-bot
Copy link

changeset-bot bot commented Dec 16, 2025

⚠️ No Changeset found

Latest commit: 5d3d54b

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Contributor

@olivermrbl olivermrbl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks pretty good, I know it's probably not done, but I gave it a read 😄

* Validates that a user has access to all the policies they are trying to assign.
* A user can only create roles and add policies that they themselves have access to.
*/
export const validateUserPermissionsStep = createStep(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought(non-blocking): realistically, the flow would probably be that certain users, e.g. admins, have the manage:roles and manage:policies permissions and are therefore the only ones capable of assigning user roles ... don't you think?

Copy link
Contributor Author

@carlos-r-l-rodrigues carlos-r-l-rodrigues Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, but even these can only assign permissions that they have themselves.

@carlos-r-l-rodrigues carlos-r-l-rodrigues changed the title Chore/rbac user chore(rbac): user link and utils Dec 19, 2025
@carlos-r-l-rodrigues carlos-r-l-rodrigues marked this pull request as ready for review December 22, 2025 10:19
@carlos-r-l-rodrigues carlos-r-l-rodrigues requested review from a team as code owners December 22, 2025 10:19
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is being reviewed by Cursor Bugbot

Details

Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

`Cannot update role parent relationship: this would create a circular dependency (role_id: ${role_id}, parent_id: ${parent_id})`
)
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circular dependency check fails when role_id omitted in update

In updateRbacRoleParents, when an update payload contains only parent_id without role_id, the destructured role_id is undefined. This causes the self-reference check (role_id === parent_id) to always pass, and checkForCycle(undefined, parent_id, ...) is called with an undefined roleId. The SQL query then looks for id = NULL which never matches, so the cycle check incorrectly returns false. To properly validate, the method needs to fetch the existing record's role_id when it's not provided in the update payload.

Fix in Cursor Fix in Web

allPolicies.push({
role_id: role.id,
scope_id: policy_id,
policy_id: policyId,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update workflow adds policies instead of replacing them

The update workflow uses createRbacRolePoliciesStep for policy_ids, which only adds new policy associations. This is inconsistent with parent_ids, which uses setRoleParentStep to fully replace existing parents. When updating a role with policy_ids that overlap with existing associations, the unique index on (role_id, policy_id) causes a database constraint violation. The workflow needs a "set" operation for policies similar to how parents are handled, to delete removed policies and only create new ones.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants