{"record":{"id":"b9eb44c458852c2f","repo":"medusajs/medusa","slug":"forbidden","errorCode":null,"errorMessage":"Forbidden","messagePattern":"Forbidden","errorType":"http","errorClass":"MedusaError","httpStatus":403,"severity":"error","filePath":"packages/core/core-flows/src/rbac/steps/validate-user-permissions.ts","lineNumber":56,"sourceCode":"    const { actor_id, actor, policy_ids, actions } = data\n\n    if (!policy_ids?.length && !actions?.length) {\n      return\n    }\n\n    const query = container.resolve(ContainerRegistrationKeys.QUERY)\n\n    const { data: users } = await query.graph({\n      entity: actor ?? \"user\",\n      fields: [\"rbac_roles.id\"],\n      filters: { id: actor_id },\n    })\n\n    const roleIds: string[] =\n      users?.[0]?.rbac_roles?.map((r) => r.id).filter(Boolean) ?? []\n\n    if (!roleIds.length) {\n      throw new MedusaError(MedusaError.Types.FORBIDDEN, \"Forbidden\")\n    }\n\n    let actionsToCheck: { resource: string; operation: string }[] = []\n\n    if (policy_ids?.length) {\n      const { data: targetPolicies } = await query.graph({\n        entity: \"rbac_policy\",\n        fields: [\"id\", \"resource\", \"operation\"],\n        filters: { id: policy_ids },\n      })\n\n      // A user cannot grant a policy that doesn't exist.\n      const inexistentPolicies = arrayDifference(\n        policy_ids,\n        targetPolicies.map((p) => p.id)\n      )\n      if (inexistentPolicies.length) {\n        throw new MedusaError(","sourceCodeStart":38,"sourceCodeEnd":74,"githubUrl":"https://github.com/medusajs/medusa/blob/5e06e544a296b9033f20f71f11c559f81a0e5739/packages/core/core-flows/src/rbac/steps/validate-user-permissions.ts#L38-L74","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Assign at least one RBAC role to the user (via the RBAC admin APIs or seed script) and retry","Verify the user id passed to the step matches the authenticated actor and that user-roles links exist","If provisioning flow is custom, ensure role attachment happens at user-creation time"],"exampleFix":"// before\nawait validateUserPermissionsStep({ userId: 'user_123', action: 'assign', policy_ids: ['pol_1'] }) // throws Forbidden\n\n// after\n// first attach a role, then run the protected workflow\nawait link.create('user', 'user_123', 'rbac_role', 'role_admin')\nawait validateUserPermissionsStep({ userId: 'user_123', action: 'assign', policy_ids: ['pol_1'] })","handlingStrategy":"validation","validationCode":"const { data: [user] } = await query.graph({ entity: 'user', filters: { id: userId }, fields: ['rbac_roles.id'] })\nif (!user?.rbac_roles?.length) throw new Error('User has no RBAC roles; assign one before this operation')","typeGuard":null,"tryCatchPattern":"try { await workflow(scope).run({ input }) } catch (e) { if (e instanceof MedusaError && e.type === MedusaError.Types.FORBIDDEN) { /* prompt role assignment / escalate */ } else throw e }","preventionTips":["Attach at least one RBAC role during user provisioning","Periodically audit for role-less active users","Surface role-assignment UI before granting policy management access"],"tags":["rbac","authorization","forbidden","users"],"backgroundTag":"authorization-role-missing","analyzedSha":"5e06e544a296b9033f20f71f11c559f81a0e5739","analyzedAt":"2026-08-27T07:24:39.599Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}