chore(rbac): user link and utils (#14320)

This commit is contained in:
Carlos R. L. Rodrigues
2026-01-07 10:40:15 -03:00
committed by GitHub
parent 7161cf1903
commit b2245cc672
72 changed files with 2746 additions and 703 deletions
@@ -1,15 +1,6 @@
import { Modules } from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
import { IRbacModuleService } from "@medusajs/types"
export type CreateRbacPolicyDTO = {
key: string
resource: string
operation: string
name?: string | null
description?: string | null
metadata?: Record<string, unknown> | null
}
import { CreateRbacPolicyDTO, IRbacModuleService } from "@medusajs/types"
export type CreateRbacPoliciesStepInput = {
policies: CreateRbacPolicyDTO[]
@@ -22,7 +13,14 @@ export const createRbacPoliciesStep = createStep(
async (data: CreateRbacPoliciesStepInput, { container }) => {
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
const created = await service.createRbacPolicies(data.policies)
// Normalize resource and operation to lowercase
const normalizedPolicies = data.policies.map((policy) => ({
...policy,
resource: policy.resource.toLowerCase(),
operation: policy.operation.toLowerCase(),
}))
const created = await service.createRbacPolicies(normalizedPolicies)
return new StepResponse(
created,
@@ -2,30 +2,28 @@ import { Modules } from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
import { IRbacModuleService } from "@medusajs/types"
export type CreateRbacRoleInheritanceDTO = {
export type CreateRbacRoleParentDTO = {
role_id: string
inherited_role_id: string
parent_id: string
metadata?: Record<string, unknown> | null
}
export type CreateRbacRoleInheritancesStepInput = {
role_inheritances: CreateRbacRoleInheritanceDTO[]
export type CreateRbacRoleParentsStepInput = {
role_parents: CreateRbacRoleParentDTO[]
}
export const createRbacRoleInheritancesStepId = "create-rbac-role-inheritances"
export const createRbacRoleParentsStepId = "create-rbac-role-parents"
export const createRbacRoleInheritancesStep = createStep(
createRbacRoleInheritancesStepId,
async (data: CreateRbacRoleInheritancesStepInput, { container }) => {
export const createRbacRoleParentsStep = createStep(
createRbacRoleParentsStepId,
async (data: CreateRbacRoleParentsStepInput, { container }) => {
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
if (!data.role_inheritances || data.role_inheritances.length === 0) {
if (!data.role_parents?.length) {
return new StepResponse([], [])
}
const created = await service.createRbacRoleInheritances(
data.role_inheritances
)
const created = await service.createRbacRoleParents(data.role_parents)
return new StepResponse(
created,
@@ -38,6 +36,6 @@ export const createRbacRoleInheritancesStep = createStep(
}
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
await service.deleteRbacRoleInheritances(createdIds)
await service.deleteRbacRoleParents(createdIds)
}
)
@@ -1,15 +1,9 @@
import { Modules } from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
import { IRbacModuleService } from "@medusajs/types"
export type CreateRbacRolePolicyDTO = {
role_id: string
scope_id: string
metadata?: Record<string, unknown> | null
}
import { CreateRbacRolePolicyDTO, IRbacModuleService } from "@medusajs/types"
export type CreateRbacRolePoliciesStepInput = {
role_policies: CreateRbacRolePolicyDTO[]
policies: CreateRbacRolePolicyDTO[]
}
export const createRbacRolePoliciesStepId = "create-rbac-role-policies"
@@ -19,7 +13,11 @@ export const createRbacRolePoliciesStep = createStep(
async (data: CreateRbacRolePoliciesStepInput, { container }) => {
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
const created = await service.createRbacRolePolicies(data.role_policies)
if (!data.policies?.length) {
return new StepResponse([], [])
}
const created = await service.createRbacRolePolicies(data.policies)
return new StepResponse(
created,
@@ -19,6 +19,9 @@ export const createRbacRolesStep = createStep(
async (data: CreateRbacRolesStepInput, { container }) => {
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
if (!data.roles?.length) {
return new StepResponse([], [])
}
const created = await service.createRbacRoles(data.roles)
return new StepResponse(
@@ -10,8 +10,23 @@ export const deleteRbacPoliciesStep = createStep(
{ name: deleteRbacPoliciesStepId, noCompensation: true },
async (ids: DeleteRbacPoliciesStepInput, { container }) => {
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
await service.deleteRbacPolicies(ids)
return new StepResponse(void 0)
if (!ids?.length) {
return new StepResponse([] as any, [])
}
const deleted = await service.deleteRbacPolicies(ids)
return new StepResponse(deleted, ids)
},
async () => {}
async (deletedPoliciesIds, { container }) => {
if (!deletedPoliciesIds?.length) {
return
}
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
// Restore the soft-deleted roles during compensation
await service.restoreRbacPolicies(deletedPoliciesIds)
}
)
@@ -10,8 +10,21 @@ export const deleteRbacRolePoliciesStep = createStep(
{ name: deleteRbacRolePoliciesStepId, noCompensation: true },
async (ids: DeleteRbacRolePoliciesStepInput, { container }) => {
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
await service.deleteRbacRolePolicies(ids)
return new StepResponse(void 0)
if (!ids?.length) {
return new StepResponse([] as any, [])
}
const deleted = await service.deleteRbacRolePolicies(ids)
return new StepResponse(deleted, ids)
},
async () => {}
async (deletedRolePolicyIds, { container }) => {
if (!deletedRolePolicyIds?.length) {
return
}
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
await service.restoreRbacRolePolicies(deletedRolePolicyIds)
}
)
@@ -6,12 +6,33 @@ export type DeleteRbacRolesStepInput = string[]
export const deleteRbacRolesStepId = "delete-rbac-roles"
/**
* This step deletes one or more RBAC roles.
* @param ids - The IDs of the roles to delete
* @param container - The workflow container
* @returns A step response with the deleted role IDs
*/
export const deleteRbacRolesStep = createStep(
{ name: deleteRbacRolesStepId, noCompensation: true },
deleteRbacRolesStepId,
async (ids: DeleteRbacRolesStepInput, { container }) => {
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
await service.deleteRbacRoles(ids)
return new StepResponse(void 0)
if (!ids?.length) {
return new StepResponse([] as any, [])
}
const deleted = await service.deleteRbacRoles(ids)
return new StepResponse(deleted, ids)
},
async () => {}
async (deletedRoleIds, { container }) => {
if (!deletedRoleIds?.length) {
return
}
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
// Restore the soft-deleted roles during compensation
await service.restoreRbacRoles(deletedRoleIds)
}
)
@@ -1,14 +1,11 @@
export * from "./create-rbac-roles"
export * from "./delete-rbac-roles"
export * from "./update-rbac-roles"
export * from "./create-rbac-policies"
export * from "./delete-rbac-policies"
export * from "./update-rbac-policies"
export * from "./create-rbac-role-parents"
export * from "./create-rbac-role-policies"
export * from "./create-rbac-roles"
export * from "./delete-rbac-policies"
export * from "./delete-rbac-role-policies"
export * from "./update-rbac-role-policies"
export * from "./create-rbac-role-inheritances"
export * from "./set-role-inheritance"
export * from "./delete-rbac-roles"
export * from "./set-role-parent"
export * from "./update-rbac-policies"
export * from "./update-rbac-roles"
export * from "./validate-user-permissions"
@@ -2,21 +2,21 @@ import { Modules } from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
import { IRbacModuleService } from "@medusajs/types"
export type SetRoleInheritanceStepInput = Array<{
export type SetRoleParentStepInput = Array<{
role_id: string
inherited_role_ids: string[]
parent_ids: string[]
}>
export const setRoleInheritanceStepId = "set-role-inheritance"
export const setRoleParentStepId = "set-role-parent"
export const setRoleInheritanceStep = createStep(
setRoleInheritanceStepId,
async (data: SetRoleInheritanceStepInput, { container }) => {
export const setRoleParentStep = createStep(
setRoleParentStepId,
async (data: SetRoleParentStepInput, { container }) => {
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
const allCompensationData: Array<{
role_id: string
previousInheritedRoleIds: string[]
previous_inherited_role_ids: string[]
}> = []
if (!data || data.length === 0) {
@@ -29,54 +29,52 @@ export const setRoleInheritanceStep = createStep(
const allToRemoveIds: string[] = []
const allToCreate: Array<{
role_id: string
inherited_role_id: string
parent_id: string
}> = []
for (const roleData of data) {
const existingInheritance = await service.listRbacRoleInheritances({
const existingParent = await service.listRbacRoleParents({
role_id: roleData.role_id,
})
const existingInheritedRoleIds = existingInheritance.map(
(ri) => ri.inherited_role_id
)
const existingInheritedRoleIds = existingParent.map((ri) => ri.parent_id)
allCompensationData.push({
role_id: roleData.role_id,
previousInheritedRoleIds: existingInheritedRoleIds,
previous_inherited_role_ids: existingInheritedRoleIds,
})
const toAdd = roleData.inherited_role_ids.filter(
const toAdd = roleData.parent_ids.filter(
(id) => !existingInheritedRoleIds.includes(id)
)
const toRemove = existingInheritedRoleIds.filter(
(id) => !roleData.inherited_role_ids.includes(id)
(id) => !roleData.parent_ids.includes(id)
)
if (toRemove.length > 0) {
const toRemoveRecords = existingInheritance.filter((ri) =>
toRemove.includes(ri.inherited_role_id)
const toRemoveRecords = existingParent.filter((ri) =>
toRemove.includes(ri.parent_id)
)
allToRemoveIds.push(...toRemoveRecords.map((ri) => ri.id))
}
if (toAdd.length > 0) {
allToCreate.push(
...toAdd.map((inherited_role_id) => ({
...toAdd.map((parent_id) => ({
role_id: roleData.role_id,
inherited_role_id,
parent_id,
}))
)
}
}
if (allToRemoveIds.length > 0) {
await service.deleteRbacRoleInheritances(allToRemoveIds)
await service.deleteRbacRoleParents(allToRemoveIds)
}
let created: any[] = []
if (allToCreate.length > 0) {
created = await service.createRbacRoleInheritances(allToCreate)
created = await service.createRbacRoleParents(allToCreate)
}
return new StepResponse(
@@ -86,7 +84,7 @@ export const setRoleInheritanceStep = createStep(
},
async (
compensationData:
| Array<{ role_id: string; previousInheritedRoleIds: string[] }>
| Array<{ role_id: string; previous_inherited_role_ids: string[] }>
| undefined,
{ container }
) => {
@@ -97,24 +95,20 @@ export const setRoleInheritanceStep = createStep(
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
for (const roleCompensation of compensationData) {
const currentInheritance = await service.listRbacRoleInheritances({
const currentParent = await service.listRbacRoleParents({
role_id: roleCompensation.role_id,
})
if (currentInheritance.length > 0) {
await service.deleteRbacRoleInheritances(
currentInheritance.map((ri) => ri.id)
)
if (currentParent.length > 0) {
await service.deleteRbacRoleParents(currentParent.map((ri) => ri.id))
}
if (roleCompensation.previousInheritedRoleIds.length > 0) {
await service.createRbacRoleInheritances(
roleCompensation.previousInheritedRoleIds.map(
(inherited_role_id) => ({
role_id: roleCompensation.role_id,
inherited_role_id,
})
)
if (roleCompensation.previous_inherited_role_ids.length > 0) {
await service.createRbacRoleParents(
roleCompensation.previous_inherited_role_ids.map((parent_id) => ({
role_id: roleCompensation.role_id,
parent_id,
}))
)
}
}
@@ -26,9 +26,18 @@ export const updateRbacPoliciesStep = createStep(
relations,
})
// Normalize resource and operation to lowercase if present
const normalizedUpdate = { ...data.update }
if (normalizedUpdate.resource) {
normalizedUpdate.resource = normalizedUpdate.resource.toLowerCase()
}
if (normalizedUpdate.operation) {
normalizedUpdate.operation = normalizedUpdate.operation.toLowerCase()
}
const updates = (prevData ?? []).map((p) => ({
id: p.id,
...data.update,
...normalizedUpdate,
})) as UpdateRbacPolicyDTO[]
const updated = await service.updateRbacPolicies(updates)
@@ -0,0 +1,96 @@
import {
ContainerRegistrationKeys,
MedusaError,
toSnakeCase,
} from "@medusajs/framework/utils"
import { createStep } from "@medusajs/framework/workflows-sdk"
export type ValidateUserPermissionsStepInput = {
actor_id: string
actor?: string
policy_ids?: string[]
actions?: {
resource: string
operation: string
}[]
}
export const validateUserPermissionsStepId = "validate-user-permissions"
/**
* 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(
validateUserPermissionsStepId,
async (data: ValidateUserPermissionsStepInput, { container }) => {
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", "rbac_roles.policies.*"],
filters: { id: actor_id },
})
if (!users?.[0]?.rbac_roles || users[0].rbac_roles.length === 0) {
throw new MedusaError(
MedusaError.Types.UNAUTHORIZED,
`User does not have any roles assigned and cannot create roles or assign policies`
)
}
const operationMap = new Map()
users[0].rbac_roles.forEach((role) => {
role.policies.forEach((policy) => {
const op =
policy.operation === "*" ? "*" : toSnakeCase(policy.operation)
operationMap.set(`${policy.resource}:${op}`, policy.id)
})
})
const allUserPolicies = users[0].rbac_roles.flatMap(
(role) => role.policies || []
)
const userPolicyIds = new Set(allUserPolicies.map((p) => p.id))
let unauthorizedPolicies: string[] = []
if (policy_ids?.length) {
unauthorizedPolicies = policy_ids.filter(
(policyId) => !userPolicyIds.has(policyId)
)
} else if (actions?.length) {
unauthorizedPolicies = actions
.filter((action) => {
const op =
action.operation === "*" ? "*" : toSnakeCase(action.operation)
return (
!operationMap.has(`${action.resource}:${op}`) &&
!operationMap.has(`${action.resource}:*`)
)
})
.map((action) => `${action.resource}:${action.operation}`)
}
if (unauthorizedPolicies.length) {
const policyMap = new Map(
allUserPolicies.map((p) => [p.id, p.name || p.key])
)
const unauthorizedNames = unauthorizedPolicies
.map((id) => policyMap.get(id) || id)
.join(", ")
throw new MedusaError(
MedusaError.Types.UNAUTHORIZED,
`User does not have access to the following policies and cannot assign them: ${unauthorizedNames}`
)
}
}
)
@@ -2,11 +2,19 @@ import {
WorkflowData,
WorkflowResponse,
createWorkflow,
transform,
when,
} from "@medusajs/framework/workflows-sdk"
import { createRbacRolePoliciesStep } from "../steps"
import { validateUserPermissionsStep } from "../steps/validate-user-permissions"
export type CreateRbacRolePoliciesWorkflowInput = {
role_policies: any[]
actor_id?: string
actor?: string
policies: {
role_id: string
policy_id: string
}[]
}
export const createRbacRolePoliciesWorkflowId = "create-rbac-role-policies"
@@ -14,6 +22,31 @@ export const createRbacRolePoliciesWorkflowId = "create-rbac-role-policies"
export const createRbacRolePoliciesWorkflow = createWorkflow(
createRbacRolePoliciesWorkflowId,
(input: WorkflowData<CreateRbacRolePoliciesWorkflowInput>) => {
return new WorkflowResponse(createRbacRolePoliciesStep(input))
const validationData = transform({ input }, ({ input }) => {
if (!input.actor_id) {
return null
}
const policyIds = new Set<string>()
input.policies.forEach((rp) => policyIds.add(rp.policy_id))
return {
actor_id: input.actor_id,
actor: input.actor,
policy_ids: Array.from(policyIds),
}
})
when({ validationData }, ({ validationData }) => {
return !!validationData?.actor_id && !!validationData?.policy_ids?.length
}).then(() => {
validateUserPermissionsStep(validationData)
})
const rolePolicies = createRbacRolePoliciesStep({
policies: input.policies,
})
return new WorkflowResponse(rolePolicies)
}
)
@@ -1,21 +1,25 @@
import {
WorkflowData,
WorkflowResponse,
createWorkflow,
transform,
when,
WorkflowData,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import {
createRbacRoleInheritancesStep,
createRbacRoleParentsStep,
createRbacRolePoliciesStep,
createRbacRolesStep,
} from "../steps"
import { validateUserPermissionsStep } from "../steps/validate-user-permissions"
export type CreateRbacRolesWorkflowInput = {
actor_id?: string
actor?: string
roles: {
name: string
description?: string | null
metadata?: Record<string, unknown> | null
inherited_role_ids?: string[]
parent_ids?: string[]
policy_ids?: string[]
}[]
}
@@ -25,6 +29,24 @@ export const createRbacRolesWorkflowId = "create-rbac-roles"
export const createRbacRolesWorkflow = createWorkflow(
createRbacRolesWorkflowId,
(input: WorkflowData<CreateRbacRolesWorkflowInput>) => {
const validationData = transform({ input }, ({ input }) => {
const allPolicyIds = new Set<string>()
input.roles.forEach((role) => {
role.policy_ids?.forEach((policyId) => allPolicyIds.add(policyId))
})
return {
actor_id: input.actor_id!,
actor: input.actor,
policy_ids: Array.from(allPolicyIds),
}
})
when({ validationData }, ({ validationData }) => {
return !!validationData?.actor_id && !!validationData?.policy_ids?.length
}).then(() => {
validateUserPermissionsStep(validationData)
})
const roleData = transform({ input }, ({ input }) => ({
roles: input.roles.map((r) => ({
name: r.name,
@@ -35,22 +57,22 @@ export const createRbacRolesWorkflow = createWorkflow(
const createdRoles = createRbacRolesStep(roleData)
const inheritanceData = transform(
const parentData = transform(
{ input, createdRoles },
({ input, createdRoles }) => {
const inheritances: any[] = []
const parents: any[] = []
createdRoles.forEach((role, index) => {
const inheritedRoleIds = input.roles[index].inherited_role_ids || []
const inheritedRoleIds = input.roles[index].parent_ids || []
inheritedRoleIds.forEach((inheritedRoleId) => {
inheritances.push({
parents.push({
role_id: role.id,
inherited_role_id: inheritedRoleId,
parent_id: inheritedRoleId,
})
})
})
return { role_inheritances: inheritances }
return { role_parents: parents }
}
)
@@ -63,15 +85,15 @@ export const createRbacRolesWorkflow = createWorkflow(
policyIds.forEach((policy_id) => {
allPolicies.push({
role_id: role.id,
scope_id: policy_id,
policy_id: policy_id,
})
})
})
return { role_policies: allPolicies }
return { policies: allPolicies }
}
)
createRbacRoleInheritancesStep(inheritanceData)
createRbacRoleParentsStep(parentData)
createRbacRolePoliciesStep(policiesData)
@@ -1,17 +1,23 @@
import { WorkflowData, createWorkflow } from "@medusajs/framework/workflows-sdk"
import {
WorkflowData,
WorkflowResponse,
createWorkflow,
} from "@medusajs/framework/workflows-sdk"
import { deleteRbacRolePoliciesStep } from "../steps"
export type DeleteRbacRolePoliciesWorkflowInput = {
ids: string[]
role_policy_ids: string[]
}
export const deleteRbacRolePoliciesWorkflowId = "delete-rbac-role-policies"
export const deleteRbacRolePoliciesWorkflow = createWorkflow(
deleteRbacRolePoliciesWorkflowId,
(
input: WorkflowData<DeleteRbacRolePoliciesWorkflowInput>
): WorkflowData<void> => {
deleteRbacRolePoliciesStep(input.ids)
(input: WorkflowData<DeleteRbacRolePoliciesWorkflowInput>) => {
const deletedRolePolicies = deleteRbacRolePoliciesStep(
input.role_policy_ids
)
return new WorkflowResponse(deletedRolePolicies)
}
)
@@ -8,4 +8,3 @@ export * from "./update-rbac-policies"
export * from "./create-rbac-role-policies"
export * from "./delete-rbac-role-policies"
export * from "./update-rbac-role-policies"
@@ -4,15 +4,19 @@ import {
WorkflowResponse,
createWorkflow,
transform,
when,
} from "@medusajs/framework/workflows-sdk"
import { UpdateRbacRoleDTO } from "@medusajs/types"
import { createRbacRolePoliciesStep, setRoleInheritanceStep } from "../steps"
import { createRbacRolePoliciesStep, setRoleParentStep } from "../steps"
import { updateRbacRolesStep } from "../steps/update-rbac-roles"
import { validateUserPermissionsStep } from "../steps/validate-user-permissions"
export type UpdateRbacRolesWorkflowInput = {
actor_id?: string
actor?: string
selector: Record<string, any>
update: Omit<UpdateRbacRoleDTO, "id"> & {
inherited_role_ids?: string[]
parent_ids?: string[]
policy_ids?: string[]
}
}
@@ -22,6 +26,21 @@ export const updateRbacRolesWorkflowId = "update-rbac-roles"
export const updateRbacRolesWorkflow = createWorkflow(
updateRbacRolesWorkflowId,
(input: WorkflowData<UpdateRbacRolesWorkflowInput>) => {
const validationData = transform({ input }, ({ input }) => {
const policyIds = input.update.policy_ids || []
return {
actor_id: input.actor_id!,
policy_ids: policyIds,
actor: input.actor,
}
})
when({ validationData }, ({ validationData }) => {
return !!validationData?.actor_id && !!validationData?.policy_ids?.length
}).then(() => {
validateUserPermissionsStep(validationData)
})
const roleUpdateData = transform({ input }, ({ input }) => ({
selector: input.selector,
update: {
@@ -33,40 +52,40 @@ export const updateRbacRolesWorkflow = createWorkflow(
const updatedRoles = updateRbacRolesStep(roleUpdateData)
const inheritanceUpdateData = transform(
const parentUpdateData = transform(
{ input, updatedRoles },
({ input, updatedRoles }) => {
if (!isDefined(input.update.inherited_role_ids)) {
if (!isDefined(input.update.parent_ids)) {
return []
}
return updatedRoles.map((role) => ({
role_id: role.id,
inherited_role_ids: input.update.inherited_role_ids || [],
parent_ids: input.update.parent_ids || [],
}))
}
)
setRoleInheritanceStep(inheritanceUpdateData)
setRoleParentStep(parentUpdateData)
const policiesUpdateData = transform(
{ input, updatedRoles },
({ input, updatedRoles }) => {
if (!isDefined(input.update.policy_ids)) {
return { role_policies: [] }
return { policies: [] }
}
const allPolicies: any[] = []
updatedRoles.forEach((role) => {
const policyIds = input.update.policy_ids || []
policyIds.forEach((policy_id) => {
policyIds.forEach((policyId) => {
allPolicies.push({
role_id: role.id,
scope_id: policy_id,
policy_id: policyId,
})
})
})
return { role_policies: allPolicies }
return { policies: allPolicies }
}
)