medusajs/medusa · error · MedusaError

Forbidden

Error message

Forbidden

What it means

Thrown by the RBAC validate-user-permissions step when the invoking user has no RBAC roles attached, so permission checks cannot proceed. Medusa treats a role-less user as unauthorized for the operation rather than falling back to any default access. It is a FORBIDDEN error from within workflow execution.

Source

Thrown at packages/core/core-flows/src/rbac/steps/validate-user-permissions.ts:56

    const { actor_id, actor, policy_ids, actions } = data

    if (!policy_ids?.length && !actions?.length) {
      return
    }

    const query = container.resolve(ContainerRegistrationKeys.QUERY)

    const { data: users } = await query.graph({
      entity: actor ?? "user",
      fields: ["rbac_roles.id"],
      filters: { id: actor_id },
    })

    const roleIds: string[] =
      users?.[0]?.rbac_roles?.map((r) => r.id).filter(Boolean) ?? []

    if (!roleIds.length) {
      throw new MedusaError(MedusaError.Types.FORBIDDEN, "Forbidden")
    }

    let actionsToCheck: { resource: string; operation: string }[] = []

    if (policy_ids?.length) {
      const { data: targetPolicies } = await query.graph({
        entity: "rbac_policy",
        fields: ["id", "resource", "operation"],
        filters: { id: policy_ids },
      })

      // A user cannot grant a policy that doesn't exist.
      const inexistentPolicies = arrayDifference(
        policy_ids,
        targetPolicies.map((p) => p.id)
      )
      if (inexistentPolicies.length) {
        throw new MedusaError(

View on GitHub (pinned to 5e06e544a2)

Solutions

  1. Assign at least one RBAC role to the user (via the RBAC admin APIs or seed script) and retry
  2. Verify the user id passed to the step matches the authenticated actor and that user-roles links exist
  3. If provisioning flow is custom, ensure role attachment happens at user-creation time

Example fix

// before
await validateUserPermissionsStep({ userId: 'user_123', action: 'assign', policy_ids: ['pol_1'] }) // throws Forbidden

// after
// first attach a role, then run the protected workflow
await link.create('user', 'user_123', 'rbac_role', 'role_admin')
await validateUserPermissionsStep({ userId: 'user_123', action: 'assign', policy_ids: ['pol_1'] })
Defensive patterns

Strategy: validation

Validate before calling

const { data: [user] } = await query.graph({ entity: 'user', filters: { id: userId }, fields: ['rbac_roles.id'] })
if (!user?.rbac_roles?.length) throw new Error('User has no RBAC roles; assign one before this operation')

Try / catch

try { await workflow(scope).run({ input }) } catch (e) { if (e instanceof MedusaError && e.type === MedusaError.Types.FORBIDDEN) { /* prompt role assignment / escalate */ } else throw e }

Prevention

When it happens

Trigger: Calling an RBAC-protected workflow/route (e.g. assigning policies to a user) with an authenticated actor that has no rbac_roles linked in the user's record loaded by the step.

Common situations: Newly created admin users that were never assigned an RBAC role; custom auth setups that bypass the RBAC provisioning step; deleting or detaching all roles from a user while they still hold API credentials.

Understand the failure class

Related errors


AI-assisted analysis of medusajs/medusa@5e06e544a2 (2026-08-27). Data as JSON: /api/errors/b9eb44c458852c2f. Report an issue: GitHub.