chore(rbac): user link and utils (#14320)
This commit is contained in:
@@ -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,
|
||||
|
||||
+11
-13
@@ -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"
|
||||
|
||||
+29
-35
@@ -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 }
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -7,11 +7,12 @@ export * from "./jobs"
|
||||
export * from "./links"
|
||||
export * from "./logger"
|
||||
export * from "./medusa-app-loader"
|
||||
export * from "./subscribers"
|
||||
export * from "./workflows"
|
||||
export * from "./telemetry"
|
||||
export * from "./zod"
|
||||
export * from "./migrations"
|
||||
export * from "./policies"
|
||||
export * from "./subscribers"
|
||||
export * from "./telemetry"
|
||||
export * from "./workflows"
|
||||
export * from "./zod"
|
||||
|
||||
export const MEDUSA_CLI_PATH = require.resolve("@medusajs/cli")
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./policy-loader"
|
||||
@@ -0,0 +1,16 @@
|
||||
import { discoverPoliciesFromDir } from "@medusajs/utils"
|
||||
import { normalize } from "path"
|
||||
|
||||
/**
|
||||
* Load RBAC policies from a directory
|
||||
* @param sourcePath - Path to scan for policies directories
|
||||
*/
|
||||
export async function policiesLoader(sourcePath?: string): Promise<void> {
|
||||
if (!sourcePath) {
|
||||
return
|
||||
}
|
||||
|
||||
const policyDir = normalize(sourcePath)
|
||||
|
||||
await discoverPoliciesFromDir(policyDir)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ export type RbacRoleDTO = {
|
||||
name: string
|
||||
description?: string | null
|
||||
metadata?: Record<string, unknown> | null
|
||||
policies?: RbacPolicyDTO[]
|
||||
}
|
||||
|
||||
export type FilterableRbacRoleProps = {
|
||||
@@ -19,11 +20,12 @@ export type RbacPolicyDTO = {
|
||||
name?: string | null
|
||||
description?: string | null
|
||||
metadata?: Record<string, unknown> | null
|
||||
deleted_at?: Date | string | null
|
||||
}
|
||||
|
||||
export type FilterableRbacPolicyProps = {
|
||||
id?: string | string[]
|
||||
key?: string
|
||||
key?: string | string[]
|
||||
resource?: string
|
||||
operation?: string
|
||||
q?: string
|
||||
@@ -32,25 +34,25 @@ export type FilterableRbacPolicyProps = {
|
||||
export type RbacRolePolicyDTO = {
|
||||
id: string
|
||||
role_id: string
|
||||
scope_id: string
|
||||
policy_id: string
|
||||
metadata?: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
export type FilterableRbacRolePolicyProps = {
|
||||
id?: string | string[]
|
||||
role_id?: string | string[]
|
||||
scope_id?: string | string[]
|
||||
policy_id?: string | string[]
|
||||
}
|
||||
|
||||
export type RbacRoleInheritanceDTO = {
|
||||
export type RbacRoleParentDTO = {
|
||||
id: string
|
||||
role_id: string
|
||||
inherited_role_id: string
|
||||
parent_id: string
|
||||
metadata?: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
export type FilterableRbacRoleInheritanceProps = {
|
||||
export type FilterableRbacRoleParentProps = {
|
||||
id?: string | string[]
|
||||
role_id?: string | string[]
|
||||
inherited_role_id?: string | string[]
|
||||
parent_id?: string | string[]
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export type UpdateRbacPolicyDTO = Partial<CreateRbacPolicyDTO> & {
|
||||
|
||||
export type CreateRbacRolePolicyDTO = {
|
||||
role_id: string
|
||||
scope_id: string
|
||||
policy_id: string
|
||||
metadata?: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
@@ -31,13 +31,12 @@ export type UpdateRbacRolePolicyDTO = Partial<CreateRbacRolePolicyDTO> & {
|
||||
id: string
|
||||
}
|
||||
|
||||
export type CreateRbacRoleInheritanceDTO = {
|
||||
export type CreateRbacRoleParentDTO = {
|
||||
role_id: string
|
||||
inherited_role_id: string
|
||||
parent_id: string
|
||||
metadata?: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
export type UpdateRbacRoleInheritanceDTO =
|
||||
Partial<CreateRbacRoleInheritanceDTO> & {
|
||||
id: string
|
||||
}
|
||||
export type UpdateRbacRoleParentDTO = Partial<CreateRbacRoleParentDTO> & {
|
||||
id: string
|
||||
}
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import { FindConfig } from "../common"
|
||||
import { RestoreReturn, SoftDeleteReturn } from "../dal"
|
||||
import { IModuleService } from "../modules-sdk"
|
||||
import { Context } from "../shared-context"
|
||||
import {
|
||||
FilterableRbacPolicyProps,
|
||||
FilterableRbacRoleInheritanceProps,
|
||||
FilterableRbacRoleParentProps,
|
||||
FilterableRbacRolePolicyProps,
|
||||
FilterableRbacRoleProps,
|
||||
RbacPolicyDTO,
|
||||
RbacRoleDTO,
|
||||
RbacRoleInheritanceDTO,
|
||||
RbacRoleParentDTO,
|
||||
RbacRolePolicyDTO,
|
||||
} from "./common"
|
||||
import {
|
||||
CreateRbacPolicyDTO,
|
||||
CreateRbacRoleDTO,
|
||||
CreateRbacRoleInheritanceDTO,
|
||||
CreateRbacRoleParentDTO,
|
||||
CreateRbacRolePolicyDTO,
|
||||
UpdateRbacPolicyDTO,
|
||||
UpdateRbacRoleDTO,
|
||||
UpdateRbacRoleInheritanceDTO,
|
||||
UpdateRbacRoleParentDTO,
|
||||
UpdateRbacRolePolicyDTO,
|
||||
} from "./mutations"
|
||||
|
||||
@@ -146,49 +147,90 @@ export interface IRbacModuleService extends IModuleService {
|
||||
sharedContext?: Context
|
||||
): Promise<[RbacRolePolicyDTO[], number]>
|
||||
|
||||
createRbacRoleInheritances(
|
||||
data: CreateRbacRoleInheritanceDTO,
|
||||
createRbacRoleParents(
|
||||
data: CreateRbacRoleParentDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<RbacRoleInheritanceDTO>
|
||||
createRbacRoleInheritances(
|
||||
data: CreateRbacRoleInheritanceDTO[],
|
||||
): Promise<RbacRoleParentDTO>
|
||||
createRbacRoleParents(
|
||||
data: CreateRbacRoleParentDTO[],
|
||||
sharedContext?: Context
|
||||
): Promise<RbacRoleInheritanceDTO[]>
|
||||
): Promise<RbacRoleParentDTO[]>
|
||||
|
||||
updateRbacRoleInheritances(
|
||||
data: UpdateRbacRoleInheritanceDTO,
|
||||
updateRbacRoleParents(
|
||||
data: UpdateRbacRoleParentDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<RbacRoleInheritanceDTO>
|
||||
updateRbacRoleInheritances(
|
||||
data: UpdateRbacRoleInheritanceDTO[],
|
||||
): Promise<RbacRoleParentDTO>
|
||||
updateRbacRoleParents(
|
||||
data: UpdateRbacRoleParentDTO[],
|
||||
sharedContext?: Context
|
||||
): Promise<RbacRoleInheritanceDTO[]>
|
||||
): Promise<RbacRoleParentDTO[]>
|
||||
|
||||
deleteRbacRoleInheritances(
|
||||
deleteRbacRoleParents(
|
||||
ids: string | string[],
|
||||
sharedContext?: Context
|
||||
): Promise<void>
|
||||
|
||||
retrieveRbacRoleInheritance(
|
||||
retrieveRbacRoleParent(
|
||||
id: string,
|
||||
config?: FindConfig<RbacRoleInheritanceDTO>,
|
||||
config?: FindConfig<RbacRoleParentDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<RbacRoleInheritanceDTO>
|
||||
): Promise<RbacRoleParentDTO>
|
||||
|
||||
listRbacRoleInheritances(
|
||||
filters?: FilterableRbacRoleInheritanceProps,
|
||||
config?: FindConfig<RbacRoleInheritanceDTO>,
|
||||
listRbacRoleParents(
|
||||
filters?: FilterableRbacRoleParentProps,
|
||||
config?: FindConfig<RbacRoleParentDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<RbacRoleInheritanceDTO[]>
|
||||
): Promise<RbacRoleParentDTO[]>
|
||||
|
||||
listAndCountRbacRoleInheritances(
|
||||
filters?: FilterableRbacRoleInheritanceProps,
|
||||
config?: FindConfig<RbacRoleInheritanceDTO>,
|
||||
listAndCountRbacRoleParents(
|
||||
filters?: FilterableRbacRoleParentProps,
|
||||
config?: FindConfig<RbacRoleParentDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<[RbacRoleInheritanceDTO[], number]>
|
||||
): Promise<[RbacRoleParentDTO[], number]>
|
||||
|
||||
listPoliciesForRole(
|
||||
roleId: string,
|
||||
sharedContext?: Context
|
||||
): Promise<RbacPolicyDTO[]>
|
||||
|
||||
softDeleteRbacRoles<TReturnableLinkableKeys extends string = string>(
|
||||
roleIds: string | string[],
|
||||
config?: SoftDeleteReturn<TReturnableLinkableKeys>,
|
||||
sharedContext?: Context
|
||||
): Promise<Record<string, string[]> | void>
|
||||
restoreRbacRoles<TReturnableLinkableKeys extends string = string>(
|
||||
roleIds: string | string[],
|
||||
config?: RestoreReturn<TReturnableLinkableKeys>,
|
||||
sharedContext?: Context
|
||||
): Promise<Record<string, string[]> | void>
|
||||
softDeleteRbacPolicies<TReturnableLinkableKeys extends string = string>(
|
||||
policyIds: string | string[],
|
||||
config?: SoftDeleteReturn<TReturnableLinkableKeys>,
|
||||
sharedContext?: Context
|
||||
): Promise<Record<string, string[]> | void>
|
||||
restoreRbacPolicies<TReturnableLinkableKeys extends string = string>(
|
||||
policyIds: string | string[],
|
||||
config?: RestoreReturn<TReturnableLinkableKeys>,
|
||||
sharedContext?: Context
|
||||
): Promise<Record<string, string[]> | void>
|
||||
softDeleteRbacRolePolicies<TReturnableLinkableKeys extends string = string>(
|
||||
rolePolicyIds: string | string[],
|
||||
config?: SoftDeleteReturn<TReturnableLinkableKeys>,
|
||||
sharedContext?: Context
|
||||
): Promise<Record<string, string[]> | void>
|
||||
restoreRbacRolePolicies<TReturnableLinkableKeys extends string = string>(
|
||||
rolePolicyIds: string | string[],
|
||||
config?: RestoreReturn<TReturnableLinkableKeys>,
|
||||
sharedContext?: Context
|
||||
): Promise<Record<string, string[]> | void>
|
||||
softDeleteRbacRoleParents<TReturnableLinkableKeys extends string = string>(
|
||||
roleParentIds: string | string[],
|
||||
config?: SoftDeleteReturn<TReturnableLinkableKeys>,
|
||||
sharedContext?: Context
|
||||
): Promise<Record<string, string[]> | void>
|
||||
restoreRbacRoleParents<TReturnableLinkableKeys extends string = string>(
|
||||
roleParentIds: string | string[],
|
||||
config?: RestoreReturn<TReturnableLinkableKeys>,
|
||||
sharedContext?: Context
|
||||
): Promise<Record<string, string[]> | void>
|
||||
}
|
||||
|
||||
@@ -53,8 +53,8 @@ export * from "./medusa-container"
|
||||
export * from "./merge-metadata"
|
||||
export * from "./merge-plugin-modules"
|
||||
export * from "./normalize-csv-value"
|
||||
export * from "./normalize-locale"
|
||||
export * from "./normalize-import-path-with-source"
|
||||
export * from "./normalize-locale"
|
||||
export * from "./object-from-string-path"
|
||||
export * from "./object-to-string-path"
|
||||
export * from "./omit-deep"
|
||||
@@ -85,6 +85,7 @@ export * from "./to-camel-case"
|
||||
export * from "./to-handle"
|
||||
export * from "./to-kebab-case"
|
||||
export * from "./to-pascal-case"
|
||||
export * from "./to-snake-case"
|
||||
export * from "./to-unix-slash"
|
||||
export * from "./trim-zeros"
|
||||
export * from "./try-convert-to-boolean"
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Converts a string to snake_case
|
||||
*/
|
||||
export function toSnakeCase(str: string): string {
|
||||
return str
|
||||
.replace(/([a-z])([A-Z])/g, "$1_$2")
|
||||
.replace(/[^a-zA-Z0-9]+/g, "_")
|
||||
.replace(/^_+|_+$/g, "")
|
||||
.toLowerCase()
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
export * from "./api-key"
|
||||
export * from "./analytics"
|
||||
export * from "./api-key"
|
||||
export * from "./auth"
|
||||
export * from "./bundles"
|
||||
export * from "./caching"
|
||||
export * from "./common"
|
||||
export * from "./core-flows"
|
||||
export * from "./dal"
|
||||
export * from "./defaults"
|
||||
export * from "./dev-server"
|
||||
export * from "./dml"
|
||||
export * from "./event-bus"
|
||||
export * from "./exceptions"
|
||||
@@ -21,6 +23,7 @@ export * from "./orchestration"
|
||||
export * from "./order"
|
||||
export * from "./payment"
|
||||
export * from "./pg"
|
||||
export * from "./policies"
|
||||
export * from "./pricing"
|
||||
export * from "./product"
|
||||
export * from "./promotion"
|
||||
@@ -28,10 +31,8 @@ export * from "./search"
|
||||
export * from "./shipping"
|
||||
export * from "./totals"
|
||||
export * from "./totals/big-number"
|
||||
export * from "./user"
|
||||
export * from "./caching"
|
||||
export * from "./translations"
|
||||
export * from "./dev-server"
|
||||
export * from "./user"
|
||||
|
||||
export const MedusaModuleType = Symbol.for("MedusaModule")
|
||||
export const MedusaModuleProviderType = Symbol.for("MedusaModuleProvider")
|
||||
|
||||
@@ -128,4 +128,10 @@ export const LINKS = {
|
||||
Modules.PAYMENT,
|
||||
"account_holder_id"
|
||||
),
|
||||
UserRbacRole: composeLinkName(
|
||||
Modules.USER,
|
||||
"user_id",
|
||||
Modules.RBAC,
|
||||
"rbac_role_id"
|
||||
),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import { getCallerFilePath, isFileDisabled, MEDUSA_SKIP_FILE } from "../common"
|
||||
import { toSnakeCase } from "../common/to-snake-case"
|
||||
|
||||
export const MedusaPolicySymbol = Symbol.for("MedusaPolicy")
|
||||
|
||||
export interface PolicyDefinition {
|
||||
name: string
|
||||
resource: string
|
||||
operation: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface definePoliciesExport {
|
||||
[MedusaPolicySymbol]: boolean
|
||||
policies: PolicyDefinition[]
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var Resource: Record<string, string>
|
||||
// eslint-disable-next-line no-var
|
||||
var Operation: Record<string, string>
|
||||
// eslint-disable-next-line no-var
|
||||
var Policy: Record<
|
||||
string,
|
||||
{ resource: string; operation: string; description?: string }
|
||||
>
|
||||
}
|
||||
|
||||
/**
|
||||
* Global registry for all unique resources.
|
||||
*/
|
||||
const defaultResources = [
|
||||
"api-key",
|
||||
"campaign",
|
||||
"claim",
|
||||
"collection",
|
||||
"currency",
|
||||
"customer",
|
||||
"customer-group",
|
||||
"draft-order",
|
||||
"exchange",
|
||||
"fulfillment",
|
||||
"fulfillment-provider",
|
||||
"fulfillment-set",
|
||||
"inventory",
|
||||
"inventory-item",
|
||||
"invite",
|
||||
"locale",
|
||||
"notification",
|
||||
"order",
|
||||
"order-change",
|
||||
"order-edit",
|
||||
"payment",
|
||||
"payment-collection",
|
||||
"payment-provider",
|
||||
"price-list",
|
||||
"price-preference",
|
||||
"product",
|
||||
"product-category",
|
||||
"product-tag",
|
||||
"product-type",
|
||||
"product-variant",
|
||||
"promotion",
|
||||
"rbac",
|
||||
"refund-reason",
|
||||
"region",
|
||||
"reservation",
|
||||
"return",
|
||||
"return-reason",
|
||||
"sales-channel",
|
||||
"shipping-option",
|
||||
"shipping-option-type",
|
||||
"shipping-profile",
|
||||
"stock-location",
|
||||
"store",
|
||||
"tax",
|
||||
"tax-provider",
|
||||
"tax-rate",
|
||||
"tax-region",
|
||||
"translation",
|
||||
"upload",
|
||||
"user",
|
||||
"workflow-execution",
|
||||
]
|
||||
|
||||
export const PolicyResource = global.PolicyResource ?? {}
|
||||
global.PolicyResource ??= PolicyResource
|
||||
|
||||
for (const resource of defaultResources) {
|
||||
const resourceKey = toSnakeCase(resource)
|
||||
PolicyResource[resourceKey] = resource
|
||||
}
|
||||
|
||||
/**
|
||||
* Global registry for all unique operations.
|
||||
*/
|
||||
const defaultOperations = ["read", "write", "update", "delete", "*"]
|
||||
|
||||
export const PolicyOperation = global.PolicyOperation ?? {}
|
||||
global.PolicyOperation ??= PolicyOperation
|
||||
|
||||
for (const operation of defaultOperations) {
|
||||
const operationKey = operation === "*" ? "*" : toSnakeCase(operation)
|
||||
PolicyOperation[operationKey] = operation
|
||||
}
|
||||
|
||||
export const Policy = global.Policy ?? {}
|
||||
global.Policy ??= Policy
|
||||
|
||||
/**
|
||||
* Define RBAC policies that will be automatically synced to the database
|
||||
* when the application starts.
|
||||
*
|
||||
* @param policies - Single policy or array of policy definitions
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* definePolicies({
|
||||
* name: "ReadBrands",
|
||||
* resource: "brand",
|
||||
* operation: "read"
|
||||
* description: "Read brands"
|
||||
* })
|
||||
*
|
||||
* definePolicies([
|
||||
* {
|
||||
* name: "ReadBrands",
|
||||
* resource: "brand",
|
||||
* operation: "read"
|
||||
* },
|
||||
* {
|
||||
* name: "WriteBrands",
|
||||
* resource: "brand",
|
||||
* operation: "write"
|
||||
* }
|
||||
* ])
|
||||
* ```
|
||||
*/
|
||||
export function definePolicies(
|
||||
policies: PolicyDefinition | PolicyDefinition[]
|
||||
): definePoliciesExport {
|
||||
const callerFilePath = getCallerFilePath()
|
||||
if (isFileDisabled(callerFilePath ?? "")) {
|
||||
return { [MEDUSA_SKIP_FILE]: true } as any
|
||||
}
|
||||
|
||||
const policiesArray = Array.isArray(policies) ? policies : [policies]
|
||||
|
||||
for (const policy of policiesArray) {
|
||||
if (!policy.name || !policy.resource || !policy.operation) {
|
||||
throw new Error(
|
||||
`Policy definition must include name, resource, and operation. Received: ${JSON.stringify(
|
||||
policy,
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for (const policy of policiesArray) {
|
||||
policy.resource = policy.resource.toLowerCase()
|
||||
policy.operation = policy.operation.toLowerCase()
|
||||
|
||||
const resourceKey = toSnakeCase(policy.resource)
|
||||
PolicyResource[resourceKey] = policy.resource
|
||||
|
||||
const operationKey = toSnakeCase(policy.operation)
|
||||
PolicyOperation[operationKey] = policy.operation
|
||||
|
||||
// Register in Policy object with name as key
|
||||
Policy[policy.name] = { ...policy }
|
||||
}
|
||||
|
||||
const output: definePoliciesExport = {
|
||||
[MedusaPolicySymbol]: true,
|
||||
policies: policiesArray,
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
export * from "./build-query"
|
||||
export * from "./create-medusa-mikro-orm-event-subscriber"
|
||||
export * from "./create-pg-connection"
|
||||
export * from "./decorators"
|
||||
export * from "./define-link"
|
||||
export * from "./define-policies"
|
||||
export * from "./definition"
|
||||
export * from "./event-builder-factory"
|
||||
export * from "./joiner-config-builder"
|
||||
@@ -16,9 +18,9 @@ export * from "./migration-scripts"
|
||||
export * from "./mikro-orm-cli-config-builder"
|
||||
export * from "./module"
|
||||
export * from "./module-provider"
|
||||
export * from "./module-provider-registration-key"
|
||||
export * from "./modules-to-container-types"
|
||||
export * from "./policy-to-types"
|
||||
export * from "./query-context"
|
||||
export * from "./types/links-config"
|
||||
export * from "./types/medusa-service"
|
||||
export * from "./module-provider-registration-key"
|
||||
export * from "./modules-to-container-types"
|
||||
export * from "./create-medusa-mikro-orm-event-subscriber"
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { FileSystem } from "../common/file-system"
|
||||
import { Policy, PolicyOperation, PolicyResource } from "./define-policies"
|
||||
|
||||
/**
|
||||
* Generates TypeScript type definitions for RBAC Resource, Operation, and Policy.
|
||||
* Creates a "policy-bindings.d.ts" file with type-safe autocomplete.
|
||||
*
|
||||
* @param outputDir - Directory where the type definition file should be created
|
||||
*/
|
||||
export async function generatePolicyTypes({
|
||||
outputDir,
|
||||
}: {
|
||||
outputDir: string
|
||||
}) {
|
||||
const policyTypeEntries: string[] = []
|
||||
|
||||
// Generate type entries for each named policy from Policy object
|
||||
for (const [name, { resource, operation }] of Object.entries(Policy)) {
|
||||
policyTypeEntries.push(
|
||||
` ${name}: { resource: "${resource}"; operation: "${operation}" }`
|
||||
)
|
||||
}
|
||||
|
||||
// If no policies are registered, create empty types
|
||||
const policyInterface =
|
||||
policyTypeEntries.length > 0
|
||||
? `{\n${policyTypeEntries.join("\n")}\n}`
|
||||
: "{}"
|
||||
|
||||
const fileSystem = new FileSystem(outputDir)
|
||||
const fileName = "policy-bindings.d.ts"
|
||||
const fileContents = `declare module '@medusajs/framework/utils' {
|
||||
/**
|
||||
* RBAC Resource registry with lowercase keys for type-safe access.
|
||||
* All resource names are normalized to lowercase.
|
||||
*
|
||||
* @example
|
||||
* import { PolicyResource } from '@medusajs/framework/utils'
|
||||
*
|
||||
* const productResource = PolicyResource.product // "product"
|
||||
* const apiKeyResource = PolicyResource.api_key // "api-key"
|
||||
*/
|
||||
export const Resource: {
|
||||
${Object.entries(PolicyResource)
|
||||
.map(([key, val]) => ` readonly ${key}: "${val}"`)
|
||||
.join("\n")}
|
||||
}
|
||||
|
||||
/**
|
||||
* RBAC Operation registry with lowercase keys for type-safe access.
|
||||
* All operation names are normalized to lowercase.
|
||||
*
|
||||
* @example
|
||||
* import { PolicyOperation } from '@medusajs/framework/utils'
|
||||
*
|
||||
* const readOp = PolicyOperation.read // "read"
|
||||
*/
|
||||
export const Operation: {
|
||||
${Object.entries(PolicyOperation)
|
||||
.map(([key, val]) => ` readonly ${key}: "${val}"`)
|
||||
.join("\n")}
|
||||
}
|
||||
|
||||
/**
|
||||
* RBAC Policy registry with all defined policies.
|
||||
* Maps policy names to their resource and operation pairs.
|
||||
*
|
||||
* @example
|
||||
* import { Policy } from '@medusajs/framework/utils'
|
||||
*
|
||||
* const readProduct = Policy.ReadProduct
|
||||
* // { resource: "product", operation: "read" }
|
||||
*/
|
||||
export const Policy: ${policyInterface}
|
||||
}`
|
||||
|
||||
await fileSystem.create(fileName, fileContents)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { readdir } from "fs/promises"
|
||||
import { join, normalize } from "path"
|
||||
import { dynamicImport, readDirRecursive } from "../common"
|
||||
import { MedusaPolicySymbol } from "../modules-sdk"
|
||||
|
||||
const excludedFiles = ["index.js", "index.ts"]
|
||||
const excludedExtensions = [".d.ts", ".d.ts.map", ".js.map"]
|
||||
|
||||
function isPolicyExport(value: unknown): boolean {
|
||||
return !!value && typeof value === "object" && MedusaPolicySymbol in value
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover policy definitions from a directory and subdirectories
|
||||
*/
|
||||
export async function discoverPoliciesFromDir(
|
||||
sourcePath?: string,
|
||||
maxDepth: number = 2
|
||||
): Promise<void> {
|
||||
if (!sourcePath) {
|
||||
return
|
||||
}
|
||||
|
||||
const root = normalize(sourcePath)
|
||||
|
||||
const allEntries = await readDirRecursive(root, {
|
||||
ignoreMissing: true,
|
||||
maxDepth,
|
||||
})
|
||||
|
||||
const policyDirs = allEntries
|
||||
.filter((e) => e.isDirectory() && e.name === "policies")
|
||||
.map((e) => join((e as any).path as string, e.name))
|
||||
|
||||
if (!policyDirs.length) {
|
||||
return
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
policyDirs.map(async (scanDir) => {
|
||||
const entries = await readdir(scanDir, { withFileTypes: true })
|
||||
await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
if (entry.isDirectory()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
excludedExtensions.some((ext) => entry.name.endsWith(ext)) ||
|
||||
excludedFiles.includes(entry.name)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// Import the file - this will execute definePolicies() calls
|
||||
const fileExports = await dynamicImport(join(scanDir, entry.name))
|
||||
|
||||
// Validate that at least one export is a policy
|
||||
const values = Object.values(fileExports)
|
||||
const hasPolicies = values.some((value) => isPolicyExport(value))
|
||||
|
||||
if (!hasPolicies) {
|
||||
console.warn(
|
||||
`File ${entry.name} in policies directory does not export any policies`
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./discover-policies"
|
||||
Reference in New Issue
Block a user