chore(rbac): user link and utils (#14320)
This commit is contained in:
committed by
GitHub
parent
7161cf1903
commit
b2245cc672
@@ -83,6 +83,7 @@ The code snippets in this section assume that your forked Medusa project and the
|
||||
"@medusajs/pricing": "file:../medusa/packages/modules/pricing",
|
||||
"@medusajs/product": "file:../medusa/packages/modules/product",
|
||||
"@medusajs/promotion": "file:../medusa/packages/modules/promotion",
|
||||
"@medusajs/rbac": "file:../medusa/packages/modules/rbac",
|
||||
"@medusajs/region": "file:../medusa/packages/modules/region",
|
||||
"@medusajs/sales-channel": "file:../medusa/packages/modules/sales-channel",
|
||||
"@medusajs/stock-location": "file:../medusa/packages/modules/stock-location",
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
|
||||
jest.setTimeout(60000)
|
||||
|
||||
process.env.MEDUSA_FF_RBAC = "true"
|
||||
|
||||
medusaIntegrationTestRunner({
|
||||
testSuite: ({ dbConnection, api, getContainer }) => {
|
||||
let container
|
||||
@@ -15,6 +17,10 @@ medusaIntegrationTestRunner({
|
||||
await createAdminUser(dbConnection, adminHeaders, container)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
delete process.env.MEDUSA_FF_RBAC
|
||||
})
|
||||
|
||||
describe("RBAC Policies - Admin API", () => {
|
||||
describe("POST /admin/rbac/policies", () => {
|
||||
it("should create a policy", async () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils"
|
||||
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
||||
import {
|
||||
adminHeaders,
|
||||
@@ -6,6 +7,8 @@ import {
|
||||
|
||||
jest.setTimeout(60000)
|
||||
|
||||
process.env.MEDUSA_FF_RBAC = "true"
|
||||
|
||||
medusaIntegrationTestRunner({
|
||||
testSuite: ({ dbConnection, api, getContainer }) => {
|
||||
let container
|
||||
@@ -15,6 +18,10 @@ medusaIntegrationTestRunner({
|
||||
await createAdminUser(dbConnection, adminHeaders, container)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
delete process.env.MEDUSA_FF_RBAC
|
||||
})
|
||||
|
||||
describe("RBAC Roles - Admin API", () => {
|
||||
describe("POST /admin/rbac/roles", () => {
|
||||
it("should create a role", async () => {
|
||||
@@ -225,8 +232,13 @@ medusaIntegrationTestRunner({
|
||||
let policies
|
||||
let viewerRole
|
||||
let editorRole
|
||||
let adminUser
|
||||
|
||||
beforeEach(async () => {
|
||||
const userModule = container.resolve(Modules.USER)
|
||||
const remoteLink = container.resolve(ContainerRegistrationKeys.LINK)
|
||||
|
||||
// Create policies
|
||||
const policy1 = await api.post(
|
||||
"/admin/rbac/policies",
|
||||
{
|
||||
@@ -266,6 +278,40 @@ medusaIntegrationTestRunner({
|
||||
policy3.data.policy,
|
||||
]
|
||||
|
||||
// Create an admin role with all policies
|
||||
const adminRoleResponse = await api.post(
|
||||
"/admin/rbac/roles",
|
||||
{
|
||||
name: "Admin Role",
|
||||
description: "Has all permissions",
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
const adminRole = adminRoleResponse.data.role
|
||||
|
||||
// Associate all policies with the admin role using the module directly
|
||||
const rbacModule = container.resolve(Modules.RBAC)
|
||||
await rbacModule.createRbacRolePolicies([
|
||||
{ role_id: adminRole.id, policy_id: policies[0].id },
|
||||
{ role_id: adminRole.id, policy_id: policies[1].id },
|
||||
{ role_id: adminRole.id, policy_id: policies[2].id },
|
||||
])
|
||||
|
||||
// Get the admin user
|
||||
const users = await userModule.listUsers({ email: "admin@medusa.js" })
|
||||
adminUser = users[0]
|
||||
|
||||
// Link the admin user to the admin role
|
||||
await remoteLink.create({
|
||||
[Modules.USER]: {
|
||||
user_id: adminUser.id,
|
||||
},
|
||||
[Modules.RBAC]: {
|
||||
rbac_role_id: adminRole.id,
|
||||
},
|
||||
})
|
||||
|
||||
// Create viewer and editor roles for the tests
|
||||
const viewer = await api.post(
|
||||
"/admin/rbac/roles",
|
||||
{
|
||||
@@ -287,96 +333,91 @@ medusaIntegrationTestRunner({
|
||||
editorRole = editor.data.role
|
||||
})
|
||||
|
||||
it("should create role-policy associations", async () => {
|
||||
it("should add policies to a role", async () => {
|
||||
const response = await api.post(
|
||||
"/admin/rbac/role-policies",
|
||||
`/admin/rbac/roles/${viewerRole.id}/policies`,
|
||||
{
|
||||
role_id: viewerRole.id,
|
||||
scope_id: policies[0].id,
|
||||
policies: [policies[0].id],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.role_policy).toEqual(
|
||||
expect.objectContaining({
|
||||
|
||||
expect(response.data.policies).toHaveLength(1)
|
||||
expect(response.data.policies[0]).toMatchObject({
|
||||
role_id: viewerRole.id,
|
||||
scope_id: policies[0].id,
|
||||
policy_id: policies[0].id,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should list role-policies for a specific role", async () => {
|
||||
// Add multiple policies to the role
|
||||
await api.post(
|
||||
"/admin/rbac/role-policies",
|
||||
`/admin/rbac/roles/${viewerRole.id}/policies`,
|
||||
{
|
||||
role_id: viewerRole.id,
|
||||
scope_id: policies[0].id,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
await api.post(
|
||||
"/admin/rbac/role-policies",
|
||||
{
|
||||
role_id: viewerRole.id,
|
||||
scope_id: policies[1].id,
|
||||
policies: [policies[0].id, policies[1].id],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
// List the role to get its policies
|
||||
const response = await api.get(
|
||||
`/admin/rbac/role-policies?role_id=${viewerRole.id}`,
|
||||
`/admin/rbac/roles/${viewerRole.id}/?fields=policies`,
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.count).toEqual(2)
|
||||
expect(response.data.role_policies).toEqual(
|
||||
expect(Array.isArray(response.data.role.policies)).toBe(true)
|
||||
expect(response.data.role.policies).toHaveLength(2)
|
||||
expect(response.data.role.policies).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
role_id: viewerRole.id,
|
||||
scope_id: policies[0].id,
|
||||
id: policies[0].id,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
role_id: viewerRole.id,
|
||||
scope_id: policies[1].id,
|
||||
id: policies[1].id,
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("should delete a role-policy association", async () => {
|
||||
const createResponse = await api.post(
|
||||
"/admin/rbac/role-policies",
|
||||
it("should remove a policy from a role", async () => {
|
||||
// First add a policy to the role
|
||||
await api.post(
|
||||
`/admin/rbac/roles/${editorRole.id}/policies`,
|
||||
{
|
||||
role_id: editorRole.id,
|
||||
scope_id: policies[2].id,
|
||||
policies: [policies[2].id],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const rolePolicyId = createResponse.data.role_policy.id
|
||||
// Verify the policy was added
|
||||
const initialResponse = await api.get(
|
||||
`/admin/rbac/roles/${editorRole.id}?fields=policies`,
|
||||
adminHeaders
|
||||
)
|
||||
expect(initialResponse.data.role.policies).toHaveLength(1)
|
||||
|
||||
// Remove the policy from the role
|
||||
const deleteResponse = await api.delete(
|
||||
`/admin/rbac/role-policies/${rolePolicyId}`,
|
||||
`/admin/rbac/roles/${editorRole.id}/policies/${policies[2].id}`,
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(deleteResponse.status).toEqual(200)
|
||||
expect(deleteResponse.data).toEqual({
|
||||
id: rolePolicyId,
|
||||
id: expect.stringContaining("rlpl_"),
|
||||
object: "rbac_role_policy",
|
||||
deleted: true,
|
||||
})
|
||||
|
||||
const listResponse = await api.get(
|
||||
`/admin/rbac/role-policies?role_id=${editorRole.id}`,
|
||||
// Verify the policy was removed
|
||||
const finalResponse = await api.get(
|
||||
`/admin/rbac/roles/${editorRole.id}?fields=policies`,
|
||||
adminHeaders
|
||||
)
|
||||
expect(
|
||||
listResponse.data.role_policies.find((rp) => rp.id === rolePolicyId)
|
||||
).toBeUndefined()
|
||||
expect(finalResponse.data.role.policies).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -68,6 +68,7 @@ const modules = {
|
||||
},
|
||||
[Modules.RBAC]: {
|
||||
resolve: "@medusajs/rbac",
|
||||
disable: process.env.MEDUSA_FF_RBAC !== "true",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -89,6 +90,7 @@ module.exports = defineConfig({
|
||||
featureFlags: {
|
||||
index_engine: process.env.ENABLE_INDEX_MODULE === "true",
|
||||
translation: process.env.MEDUSA_FF_TRANSLATION === "true",
|
||||
rbac: process.env.MEDUSA_FF_RBAC === "true",
|
||||
},
|
||||
modules,
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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) => ({
|
||||
if (roleCompensation.previous_inherited_role_ids.length > 0) {
|
||||
await service.createRbacRoleParents(
|
||||
roleCompensation.previous_inherited_role_ids.map((parent_id) => ({
|
||||
role_id: roleCompensation.role_id,
|
||||
inherited_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")
|
||||
|
||||
|
||||
1
packages/core/framework/src/policies/index.ts
Normal file
1
packages/core/framework/src/policies/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./policy-loader"
|
||||
16
packages/core/framework/src/policies/policy-loader.ts
Normal file
16
packages/core/framework/src/policies/policy-loader.ts
Normal file
@@ -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> & {
|
||||
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"
|
||||
|
||||
10
packages/core/utils/src/common/to-snake-case.ts
Normal file
10
packages/core/utils/src/common/to-snake-case.ts
Normal file
@@ -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"
|
||||
),
|
||||
}
|
||||
|
||||
182
packages/core/utils/src/modules-sdk/define-policies.ts
Normal file
182
packages/core/utils/src/modules-sdk/define-policies.ts
Normal file
@@ -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"
|
||||
|
||||
78
packages/core/utils/src/modules-sdk/policy-to-types.ts
Normal file
78
packages/core/utils/src/modules-sdk/policy-to-types.ts
Normal file
@@ -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)
|
||||
}
|
||||
71
packages/core/utils/src/policies/discover-policies.ts
Normal file
71
packages/core/utils/src/policies/discover-policies.ts
Normal file
@@ -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`
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
1
packages/core/utils/src/policies/index.ts
Normal file
1
packages/core/utils/src/policies/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./discover-policies"
|
||||
@@ -1,11 +1,9 @@
|
||||
import { MiddlewareRoute } from "@medusajs/framework/http"
|
||||
|
||||
import { adminRbacPolicyRoutesMiddlewares } from "./policies/middlewares"
|
||||
import { adminRbacRolePolicyRoutesMiddlewares } from "./role-policies/middlewares"
|
||||
import { adminRbacRoleRoutesMiddlewares } from "./roles/middlewares"
|
||||
|
||||
export const adminRbacRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
...adminRbacRoleRoutesMiddlewares,
|
||||
...adminRbacPolicyRoutesMiddlewares,
|
||||
...adminRbacRolePolicyRoutesMiddlewares,
|
||||
]
|
||||
|
||||
@@ -3,7 +3,12 @@ import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
defineFileConfig,
|
||||
FeatureFlag,
|
||||
} from "@medusajs/framework/utils"
|
||||
import RbacFeatureFlag from "../../../../feature-flags/rbac"
|
||||
import { AdminCreateRbacPolicyType } from "./validators"
|
||||
|
||||
export const GET = async (
|
||||
@@ -48,3 +53,7 @@ export const POST = async (
|
||||
|
||||
res.status(200).json({ policy })
|
||||
}
|
||||
|
||||
defineFileConfig({
|
||||
isDisabled: () => !FeatureFlag.isFeatureEnabled(RbacFeatureFlag.key),
|
||||
})
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import {
|
||||
deleteRbacRolePoliciesWorkflow,
|
||||
updateRbacRolePoliciesWorkflow,
|
||||
} from "@medusajs/core-flows"
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
MedusaError,
|
||||
} from "@medusajs/framework/utils"
|
||||
|
||||
import { AdminUpdateRbacRolePolicyType } from "../validators"
|
||||
|
||||
export const GET = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
|
||||
const { data: rolePolicies } = await query.graph({
|
||||
entity: "rbac_role_policy",
|
||||
filters: { id: req.params.id },
|
||||
fields: req.queryConfig.fields,
|
||||
})
|
||||
|
||||
const role_policy = rolePolicies[0]
|
||||
|
||||
if (!role_policy) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Role policy with id: ${req.params.id} not found`
|
||||
)
|
||||
}
|
||||
|
||||
res.status(200).json({ role_policy })
|
||||
}
|
||||
|
||||
export const POST = async (
|
||||
req: AuthenticatedMedusaRequest<AdminUpdateRbacRolePolicyType>,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
|
||||
const { data: existing } = await query.graph({
|
||||
entity: "rbac_role_policy",
|
||||
filters: { id: req.params.id },
|
||||
fields: ["id"],
|
||||
})
|
||||
|
||||
const existingRolePolicy = existing[0]
|
||||
if (!existingRolePolicy) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Role policy with id "${req.params.id}" not found`
|
||||
)
|
||||
}
|
||||
|
||||
const { result } = await updateRbacRolePoliciesWorkflow(req.scope).run({
|
||||
input: {
|
||||
selector: { id: req.params.id },
|
||||
update: req.validatedBody,
|
||||
},
|
||||
})
|
||||
|
||||
const { data: rolePolicies } = await query.graph({
|
||||
entity: "rbac_role_policy",
|
||||
filters: { id: result[0].id },
|
||||
fields: req.queryConfig.fields,
|
||||
})
|
||||
|
||||
const role_policy = rolePolicies[0]
|
||||
|
||||
res.status(200).json({ role_policy })
|
||||
}
|
||||
|
||||
export const DELETE = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const id = req.params.id
|
||||
|
||||
await deleteRbacRolePoliciesWorkflow(req.scope).run({
|
||||
input: { ids: [id] },
|
||||
})
|
||||
|
||||
res.status(200).json({
|
||||
id,
|
||||
object: "rbac_role_policy",
|
||||
deleted: true,
|
||||
})
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import * as QueryConfig from "./query-config"
|
||||
|
||||
import {
|
||||
validateAndTransformBody,
|
||||
validateAndTransformQuery,
|
||||
} from "@medusajs/framework"
|
||||
import { MiddlewareRoute } from "@medusajs/framework/http"
|
||||
|
||||
import {
|
||||
AdminCreateRbacRolePolicy,
|
||||
AdminGetRbacRolePoliciesParams,
|
||||
AdminGetRbacRolePolicyParams,
|
||||
AdminUpdateRbacRolePolicy,
|
||||
} from "./validators"
|
||||
|
||||
export const adminRbacRolePolicyRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: ["GET"],
|
||||
matcher: "/admin/rbac/role-policies",
|
||||
middlewares: [
|
||||
validateAndTransformQuery(
|
||||
AdminGetRbacRolePoliciesParams,
|
||||
QueryConfig.listTransformQueryConfig
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
matcher: "/admin/rbac/role-policies/:id",
|
||||
middlewares: [
|
||||
validateAndTransformQuery(
|
||||
AdminGetRbacRolePolicyParams,
|
||||
QueryConfig.retrieveTransformQueryConfig
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/admin/rbac/role-policies",
|
||||
middlewares: [
|
||||
validateAndTransformBody(AdminCreateRbacRolePolicy),
|
||||
validateAndTransformQuery(
|
||||
AdminGetRbacRolePolicyParams,
|
||||
QueryConfig.retrieveTransformQueryConfig
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/admin/rbac/role-policies/:id",
|
||||
middlewares: [
|
||||
validateAndTransformBody(AdminUpdateRbacRolePolicy),
|
||||
validateAndTransformQuery(
|
||||
AdminGetRbacRolePolicyParams,
|
||||
QueryConfig.retrieveTransformQueryConfig
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
method: ["DELETE"],
|
||||
matcher: "/admin/rbac/role-policies/:id",
|
||||
middlewares: [],
|
||||
},
|
||||
]
|
||||
@@ -1,20 +0,0 @@
|
||||
export const defaultAdminRbacRolePolicyFields = [
|
||||
"id",
|
||||
"role_id",
|
||||
"scope_id",
|
||||
"metadata",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"deleted_at",
|
||||
]
|
||||
|
||||
export const retrieveTransformQueryConfig = {
|
||||
defaults: defaultAdminRbacRolePolicyFields,
|
||||
isList: false,
|
||||
}
|
||||
|
||||
export const listTransformQueryConfig = {
|
||||
...retrieveTransformQueryConfig,
|
||||
defaultLimit: 20,
|
||||
isList: true,
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { createRbacRolePoliciesWorkflow } from "@medusajs/core-flows"
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
|
||||
import { AdminCreateRbacRolePolicyType } from "./validators"
|
||||
|
||||
export const GET = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
|
||||
|
||||
const { data: role_policies, metadata } = await query.graph({
|
||||
entity: "rbac_role_policy",
|
||||
fields: req.queryConfig.fields,
|
||||
filters: req.filterableFields,
|
||||
pagination: req.queryConfig.pagination,
|
||||
})
|
||||
|
||||
res.status(200).json({
|
||||
role_policies,
|
||||
count: metadata?.count ?? 0,
|
||||
offset: metadata?.skip ?? 0,
|
||||
limit: metadata?.take ?? 0,
|
||||
})
|
||||
}
|
||||
|
||||
export const POST = async (
|
||||
req: AuthenticatedMedusaRequest<AdminCreateRbacRolePolicyType>,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const input = [req.validatedBody]
|
||||
|
||||
const { result } = await createRbacRolePoliciesWorkflow(req.scope).run({
|
||||
input: { role_policies: input },
|
||||
})
|
||||
|
||||
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
|
||||
const { data: rolePolicies } = await query.graph({
|
||||
entity: "rbac_role_policy",
|
||||
fields: req.queryConfig.fields,
|
||||
filters: { id: result[0].id },
|
||||
})
|
||||
|
||||
const role_policy = rolePolicies[0]
|
||||
|
||||
res.status(200).json({ role_policy })
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { z } from "zod"
|
||||
import { applyAndAndOrOperators } from "../../../utils/common-validators"
|
||||
import {
|
||||
createFindParams,
|
||||
createOperatorMap,
|
||||
createSelectParams,
|
||||
} from "../../../utils/validators"
|
||||
|
||||
export type AdminGetRbacRolePolicyParamsType = z.infer<
|
||||
typeof AdminGetRbacRolePolicyParams
|
||||
>
|
||||
export const AdminGetRbacRolePolicyParams = createSelectParams()
|
||||
|
||||
export const AdminGetRbacRolePoliciesParamsFields = z.object({
|
||||
q: z.string().optional(),
|
||||
id: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
role_id: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
scope_id: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
created_at: createOperatorMap().optional(),
|
||||
updated_at: createOperatorMap().optional(),
|
||||
deleted_at: createOperatorMap().optional(),
|
||||
})
|
||||
|
||||
export type AdminGetRbacRolePoliciesParamsType = z.infer<
|
||||
typeof AdminGetRbacRolePoliciesParams
|
||||
>
|
||||
export const AdminGetRbacRolePoliciesParams = createFindParams({
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
})
|
||||
.merge(AdminGetRbacRolePoliciesParamsFields)
|
||||
.merge(applyAndAndOrOperators(AdminGetRbacRolePoliciesParamsFields))
|
||||
|
||||
export type AdminCreateRbacRolePolicyType = z.infer<
|
||||
typeof AdminCreateRbacRolePolicy
|
||||
>
|
||||
export const AdminCreateRbacRolePolicy = z
|
||||
.object({
|
||||
role_id: z.string(),
|
||||
scope_id: z.string(),
|
||||
metadata: z.record(z.unknown()).nullish(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
export type AdminUpdateRbacRolePolicyType = z.infer<
|
||||
typeof AdminUpdateRbacRolePolicy
|
||||
>
|
||||
export const AdminUpdateRbacRolePolicy = z
|
||||
.object({
|
||||
metadata: z.record(z.unknown()).nullish(),
|
||||
})
|
||||
.strict()
|
||||
@@ -0,0 +1,40 @@
|
||||
import { deleteRbacRolePoliciesWorkflow } from "@medusajs/core-flows"
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
import { defineFileConfig, FeatureFlag } from "@medusajs/framework/utils"
|
||||
import RbacFeatureFlag from "../../../../../../../feature-flags/rbac"
|
||||
|
||||
export const DELETE = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const { policy_id, id: role_id } = req.params
|
||||
|
||||
// First, we need to find the role_policy_id that connects this role and policy
|
||||
const query = req.scope.resolve("query")
|
||||
const { data: rolePolicies } = await query.graph({
|
||||
entity: "rbac_role_policy",
|
||||
fields: ["id"],
|
||||
filters: { role_id, policy_id },
|
||||
})
|
||||
|
||||
const rolePolicyId = rolePolicies[0]?.id
|
||||
|
||||
await deleteRbacRolePoliciesWorkflow(req.scope).run({
|
||||
input: {
|
||||
role_policy_ids: rolePolicyId ? [rolePolicyId] : [],
|
||||
},
|
||||
})
|
||||
|
||||
res.status(200).json({
|
||||
id: rolePolicyId,
|
||||
object: "rbac_role_policy",
|
||||
deleted: true,
|
||||
})
|
||||
}
|
||||
|
||||
defineFileConfig({
|
||||
isDisabled: () => !FeatureFlag.isFeatureEnabled(RbacFeatureFlag.key),
|
||||
})
|
||||
@@ -0,0 +1,69 @@
|
||||
import { createRbacRolePoliciesWorkflow } from "@medusajs/core-flows"
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
defineFileConfig,
|
||||
FeatureFlag,
|
||||
} from "@medusajs/framework/utils"
|
||||
import RbacFeatureFlag from "../../../../../../feature-flags/rbac"
|
||||
import { AdminAddRolePoliciesType } from "../../validators"
|
||||
|
||||
export const GET = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const roleId = req.params.id
|
||||
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
|
||||
|
||||
const { data: policies, metadata } = await query.graph({
|
||||
entity: "rbac_role_policy",
|
||||
fields: req.queryConfig?.fields,
|
||||
filters: { ...req.filterableFields, role_id: roleId },
|
||||
pagination: req.queryConfig?.pagination || {},
|
||||
})
|
||||
|
||||
res.status(200).json({
|
||||
policies,
|
||||
count: metadata?.count ?? 0,
|
||||
offset: metadata?.skip ?? 0,
|
||||
limit: metadata?.take ?? 0,
|
||||
})
|
||||
}
|
||||
|
||||
export const POST = async (
|
||||
req: AuthenticatedMedusaRequest<AdminAddRolePoliciesType>,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const roleId = req.params.id
|
||||
const { policies } = req.validatedBody
|
||||
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
|
||||
|
||||
const rolePolicies = policies.map((policyId) => ({
|
||||
role_id: roleId,
|
||||
policy_id: policyId,
|
||||
}))
|
||||
|
||||
const { result } = await createRbacRolePoliciesWorkflow(req.scope).run({
|
||||
input: {
|
||||
actor_id: req.auth_context.actor_id,
|
||||
actor: req.auth_context.actor_type,
|
||||
policies: rolePolicies,
|
||||
},
|
||||
})
|
||||
|
||||
// Get the created role-policy association
|
||||
const { data } = await query.graph({
|
||||
entity: "rbac_role_policy",
|
||||
fields: req.queryConfig?.fields,
|
||||
filters: { id: result[0].id },
|
||||
})
|
||||
|
||||
res.status(200).json({ policies: data })
|
||||
}
|
||||
|
||||
defineFileConfig({
|
||||
isDisabled: () => !FeatureFlag.isFeatureEnabled(RbacFeatureFlag.key),
|
||||
})
|
||||
@@ -57,6 +57,8 @@ export const POST = async (
|
||||
|
||||
const { result } = await updateRbacRolesWorkflow(req.scope).run({
|
||||
input: {
|
||||
actor_id: req.auth_context.actor_id,
|
||||
actor: req.auth_context.actor_type,
|
||||
selector: { id: req.params.id },
|
||||
update: req.validatedBody,
|
||||
},
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export {}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import { MiddlewareRoute } from "@medusajs/framework/http"
|
||||
|
||||
import {
|
||||
AdminAddRolePoliciesType,
|
||||
AdminCreateRbacRole,
|
||||
AdminGetRbacRoleParams,
|
||||
AdminGetRbacRolesParams,
|
||||
@@ -56,6 +57,32 @@ export const adminRbacRoleRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
matcher: "/admin/rbac/roles/:id/policies",
|
||||
middlewares: [
|
||||
validateAndTransformQuery(
|
||||
AdminGetRbacRoleParams,
|
||||
QueryConfig.retrieveRolePoliciesTransformQueryConfig
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/admin/rbac/roles/:id/policies",
|
||||
middlewares: [
|
||||
validateAndTransformBody(AdminAddRolePoliciesType),
|
||||
validateAndTransformQuery(
|
||||
AdminGetRbacRoleParams,
|
||||
QueryConfig.retrieveRolePoliciesTransformQueryConfig
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
method: ["DELETE"],
|
||||
matcher: "/admin/rbac/roles/:id/policies/:policy_id",
|
||||
middlewares: [],
|
||||
},
|
||||
{
|
||||
method: ["DELETE"],
|
||||
matcher: "/admin/rbac/roles/:id",
|
||||
|
||||
@@ -19,3 +19,24 @@ export const listTransformQueryConfig = {
|
||||
defaultLimit: 20,
|
||||
isList: true,
|
||||
}
|
||||
|
||||
export const defaultAdminRolePoliciesFields = [
|
||||
"id",
|
||||
"role_id",
|
||||
"policy_id",
|
||||
"policy",
|
||||
"metadata",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"deleted_at",
|
||||
]
|
||||
|
||||
export const retrieveRolePoliciesTransformQueryConfig = {
|
||||
defaults: defaultAdminRolePoliciesFields,
|
||||
isList: false,
|
||||
}
|
||||
|
||||
export const listRolePoliciesTransformQueryConfig = {
|
||||
...retrieveRolePoliciesTransformQueryConfig,
|
||||
isList: true,
|
||||
}
|
||||
|
||||
@@ -3,7 +3,12 @@ import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
defineFileConfig,
|
||||
FeatureFlag,
|
||||
} from "@medusajs/framework/utils"
|
||||
import RbacFeatureFlag from "../../../../feature-flags/rbac"
|
||||
import { AdminCreateRbacRoleType } from "./validators"
|
||||
|
||||
export const GET = async (
|
||||
@@ -34,7 +39,11 @@ export const POST = async (
|
||||
const input = [req.validatedBody]
|
||||
|
||||
const { result } = await createRbacRolesWorkflow(req.scope).run({
|
||||
input: { roles: input },
|
||||
input: {
|
||||
actor_id: req.auth_context.actor_id,
|
||||
actor: req.auth_context.actor_type,
|
||||
roles: input,
|
||||
},
|
||||
})
|
||||
|
||||
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
|
||||
@@ -48,3 +57,7 @@ export const POST = async (
|
||||
|
||||
res.status(200).json({ role })
|
||||
}
|
||||
|
||||
defineFileConfig({
|
||||
isDisabled: () => !FeatureFlag.isFeatureEnabled(RbacFeatureFlag.key),
|
||||
})
|
||||
|
||||
@@ -7,7 +7,11 @@ import {
|
||||
} from "../../../utils/validators"
|
||||
|
||||
export type AdminGetRbacRoleParamsType = z.infer<typeof AdminGetRbacRoleParams>
|
||||
export const AdminGetRbacRoleParams = createSelectParams()
|
||||
export const AdminGetRbacRoleParams = createSelectParams().merge(
|
||||
z.object({
|
||||
policies: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
})
|
||||
)
|
||||
|
||||
export const AdminGetRbacRolesParamsFields = z.object({
|
||||
q: z.string().optional(),
|
||||
@@ -48,3 +52,9 @@ export const AdminUpdateRbacRole = z
|
||||
metadata: z.record(z.unknown()).nullish(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const AdminAddRolePoliciesType = z.object({
|
||||
policies: z.array(z.string().min(1)).min(1),
|
||||
})
|
||||
|
||||
export type AdminAddRolePoliciesType = z.infer<typeof AdminAddRolePoliciesType>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||
import {
|
||||
AuthenticationInput,
|
||||
ConfigModule,
|
||||
@@ -8,7 +9,6 @@ import {
|
||||
MedusaError,
|
||||
Modules,
|
||||
} from "@medusajs/framework/utils"
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||
import { generateJwtTokenForAuthIdentity } from "../../../utils/generate-jwt-token"
|
||||
|
||||
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
@@ -35,8 +35,13 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
if (success && authIdentity) {
|
||||
const { http } = config.projectConfig
|
||||
|
||||
const token = generateJwtTokenForAuthIdentity(
|
||||
{ authIdentity, actorType: actor_type, authProvider: auth_provider },
|
||||
const token = await generateJwtTokenForAuthIdentity(
|
||||
{
|
||||
authIdentity,
|
||||
actorType: actor_type,
|
||||
authProvider: auth_provider,
|
||||
container: req.scope,
|
||||
},
|
||||
{
|
||||
secret: http.jwtSecret!,
|
||||
expiresIn: http.jwtExpiresIn,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||
import {
|
||||
AuthenticationInput,
|
||||
ConfigModule,
|
||||
@@ -8,7 +9,6 @@ import {
|
||||
MedusaError,
|
||||
Modules,
|
||||
} from "@medusajs/framework/utils"
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||
import { generateJwtTokenForAuthIdentity } from "../../../utils/generate-jwt-token"
|
||||
|
||||
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
@@ -35,11 +35,12 @@ export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
if (success && authIdentity) {
|
||||
const { http } = config.projectConfig
|
||||
|
||||
const token = generateJwtTokenForAuthIdentity(
|
||||
const token = await generateJwtTokenForAuthIdentity(
|
||||
{
|
||||
authIdentity,
|
||||
actorType: actor_type,
|
||||
authProvider: auth_provider,
|
||||
container: req.scope,
|
||||
},
|
||||
{
|
||||
secret: http.jwtSecret!,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||
import {
|
||||
AuthenticationInput,
|
||||
ConfigModule,
|
||||
@@ -8,7 +9,6 @@ import {
|
||||
MedusaError,
|
||||
Modules,
|
||||
} from "@medusajs/framework/utils"
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||
import { generateJwtTokenForAuthIdentity } from "../../utils/generate-jwt-token"
|
||||
|
||||
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
@@ -39,11 +39,12 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
if (success && authIdentity) {
|
||||
const { http } = config.projectConfig
|
||||
|
||||
const token = generateJwtTokenForAuthIdentity(
|
||||
const token = await generateJwtTokenForAuthIdentity(
|
||||
{
|
||||
authIdentity,
|
||||
actorType: actor_type,
|
||||
authProvider: auth_provider,
|
||||
container: req.scope,
|
||||
},
|
||||
{
|
||||
secret: http.jwtSecret!,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { IAuthModuleService } from "@medusajs/framework/types"
|
||||
import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils"
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
import { IAuthModuleService } from "@medusajs/framework/types"
|
||||
import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils"
|
||||
import { generateJwtTokenForAuthIdentity } from "../../utils/generate-jwt-token"
|
||||
|
||||
// Retrieve a newly generated JWT token. All checks that the existing token is valid already happen in the auth middleware.
|
||||
@@ -23,8 +23,12 @@ export const POST = async (
|
||||
ContainerRegistrationKeys.CONFIG_MODULE
|
||||
).projectConfig
|
||||
|
||||
const token = generateJwtTokenForAuthIdentity(
|
||||
{ authIdentity, actorType: req.auth_context.actor_type },
|
||||
const token = await generateJwtTokenForAuthIdentity(
|
||||
{
|
||||
authIdentity,
|
||||
actorType: req.auth_context.actor_type,
|
||||
container: req.scope,
|
||||
},
|
||||
{
|
||||
secret: http.jwtSecret!,
|
||||
expiresIn: http.jwtExpiresIn,
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
import {
|
||||
AuthIdentityDTO,
|
||||
MedusaContainer,
|
||||
ProjectConfigOptions,
|
||||
} from "@medusajs/framework/types"
|
||||
import { generateJwtToken } from "@medusajs/framework/utils"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
FeatureFlag,
|
||||
generateJwtToken,
|
||||
} from "@medusajs/framework/utils"
|
||||
import { type Secret } from "jsonwebtoken"
|
||||
import RbacFeatureFlag from "../../../feature-flags/rbac"
|
||||
|
||||
export function generateJwtTokenForAuthIdentity(
|
||||
export async function generateJwtTokenForAuthIdentity(
|
||||
{
|
||||
authIdentity,
|
||||
actorType,
|
||||
authProvider,
|
||||
container,
|
||||
}: {
|
||||
authIdentity: AuthIdentityDTO
|
||||
actorType: string
|
||||
authProvider?: string
|
||||
container?: MedusaContainer
|
||||
},
|
||||
{
|
||||
secret,
|
||||
@@ -37,6 +45,29 @@ export function generateJwtTokenForAuthIdentity(
|
||||
(identity) => identity.provider === authProvider
|
||||
)[0]
|
||||
|
||||
let roles: string[] = []
|
||||
|
||||
if (FeatureFlag.isFeatureEnabled(RbacFeatureFlag.key)) {
|
||||
if (container && entityId) {
|
||||
try {
|
||||
const query = container.resolve(ContainerRegistrationKeys.QUERY)
|
||||
const { data: userRoles } = await query.graph({
|
||||
entity: actorType,
|
||||
fields: ["rbac_roles.id"],
|
||||
filters: {
|
||||
id: entityId,
|
||||
},
|
||||
})
|
||||
|
||||
if (userRoles?.[0]?.rbac_roles) {
|
||||
roles = userRoles[0].rbac_roles.map((role) => role.id)
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return generateJwtToken(
|
||||
{
|
||||
actor_id: entityId ?? "",
|
||||
@@ -44,6 +75,7 @@ export function generateJwtTokenForAuthIdentity(
|
||||
auth_identity_id: authIdentity?.id ?? "",
|
||||
app_metadata: {
|
||||
[entityIdKey]: entityId,
|
||||
roles,
|
||||
},
|
||||
user_metadata: providerIdentity?.user_metadata ?? {},
|
||||
},
|
||||
|
||||
@@ -11,10 +11,12 @@ import {
|
||||
dynamicImport,
|
||||
FileSystem,
|
||||
generateContainerTypes,
|
||||
generatePolicyTypes,
|
||||
gqlSchemaToTypes,
|
||||
GracefulShutdownServer,
|
||||
isFileSkipped,
|
||||
isPresent,
|
||||
promiseAll,
|
||||
} from "@medusajs/framework/utils"
|
||||
|
||||
import { MedusaModule } from "@medusajs/framework/modules-sdk"
|
||||
@@ -274,27 +276,35 @@ async function start(args: {
|
||||
if (generateTypes) {
|
||||
const typesDirectory = path.join(directory, ".medusa/types")
|
||||
|
||||
/**
|
||||
* Cleanup existing types directory before creating new artifacts
|
||||
*/
|
||||
await new FileSystem(typesDirectory).cleanup({ recursive: true })
|
||||
const fileGenPromises: Promise<void>[] = []
|
||||
|
||||
await generateContainerTypes(modules, {
|
||||
fileGenPromises.push(
|
||||
generateContainerTypes(modules, {
|
||||
outputDir: typesDirectory,
|
||||
interfaceName: "ModuleImplementations",
|
||||
})
|
||||
logger.debug("Generated container types")
|
||||
)
|
||||
|
||||
if (gqlSchema) {
|
||||
await gqlSchemaToTypes({
|
||||
fileGenPromises.push(
|
||||
gqlSchemaToTypes({
|
||||
outputDir: typesDirectory,
|
||||
filename: "query-entry-points",
|
||||
interfaceName: "RemoteQueryEntryPoints",
|
||||
schema: gqlSchema,
|
||||
joinerConfigs: MedusaModule.getAllJoinerConfigs(),
|
||||
})
|
||||
logger.debug("Generated modules types")
|
||||
)
|
||||
}
|
||||
|
||||
fileGenPromises.push(
|
||||
generatePolicyTypes({
|
||||
outputDir: typesDirectory,
|
||||
})
|
||||
)
|
||||
|
||||
await promiseAll(fileGenPromises)
|
||||
logger.debug("Generated policy types")
|
||||
}
|
||||
|
||||
// Register a health check endpoint. Ideally this also checks the readiness of the service, rather than just returning a static response.
|
||||
|
||||
10
packages/medusa/src/feature-flags/rbac.ts
Normal file
10
packages/medusa/src/feature-flags/rbac.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { FlagSettings } from "@medusajs/framework/feature-flags"
|
||||
|
||||
const RbacFeatureFlag: FlagSettings = {
|
||||
key: "rbac",
|
||||
default_val: false,
|
||||
env_key: "MEDUSA_FF_RBAC",
|
||||
description: "Enable role based access control",
|
||||
}
|
||||
|
||||
export default RbacFeatureFlag
|
||||
@@ -1,4 +1,5 @@
|
||||
import { container, MedusaAppLoader } from "@medusajs/framework"
|
||||
import { container, MedusaAppLoader, policiesLoader } from "@medusajs/framework"
|
||||
import { asValue } from "@medusajs/framework/awilix"
|
||||
import { configLoader } from "@medusajs/framework/config"
|
||||
import { pgConnectionLoader } from "@medusajs/framework/database"
|
||||
import { featureFlagsLoader } from "@medusajs/framework/feature-flags"
|
||||
@@ -22,7 +23,6 @@ import {
|
||||
validateModuleName,
|
||||
} from "@medusajs/framework/utils"
|
||||
import { WorkflowLoader } from "@medusajs/framework/workflows"
|
||||
import { asValue } from "@medusajs/framework/awilix"
|
||||
import { Express, NextFunction, Request, Response } from "express"
|
||||
import { join } from "path"
|
||||
import requestIp from "request-ip"
|
||||
@@ -182,6 +182,12 @@ export default async ({
|
||||
)
|
||||
await new LinkLoader(linksSourcePaths, logger).load()
|
||||
|
||||
// Load policies from project root and all plugins
|
||||
await policiesLoader(rootDirectory)
|
||||
for (const plugin of plugins) {
|
||||
await policiesLoader(plugin.resolve)
|
||||
}
|
||||
|
||||
const {
|
||||
onApplicationStart,
|
||||
onApplicationShutdown,
|
||||
|
||||
6
packages/medusa/src/modules/rbac.ts
Normal file
6
packages/medusa/src/modules/rbac.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import RbacModule from "@medusajs/rbac"
|
||||
|
||||
export * from "@medusajs/rbac"
|
||||
|
||||
export default RbacModule
|
||||
export const discoveryPath = require.resolve("@medusajs/rbac")
|
||||
152
packages/medusa/src/utils/rbac/has-permission.ts
Normal file
152
packages/medusa/src/utils/rbac/has-permission.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { MedusaContainer } from "@medusajs/framework/types"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
FeatureFlag,
|
||||
useCache,
|
||||
} from "@medusajs/framework/utils"
|
||||
import RbacFeatureFlag from "../../feature-flags/rbac"
|
||||
|
||||
export type PermissionAction = {
|
||||
resource: string
|
||||
operation: string
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
*
|
||||
* @property roles the role(s) to check. Can be a single string or an array of strings.
|
||||
* @property actions the action(s) to check. Can be a single `PermissionAction` or an array of `PermissionAction`s.
|
||||
* @property container the Medusa container
|
||||
*/
|
||||
export type HasPermissionInput = {
|
||||
roles: string | string[]
|
||||
actions: PermissionAction | PermissionAction[]
|
||||
container: MedusaContainer
|
||||
}
|
||||
|
||||
type RolePoliciesCache = Map<string, Map<string, Set<string>>>
|
||||
|
||||
/**
|
||||
* Checks if the given role(s) have permission to perform the specified action(s).
|
||||
*
|
||||
* @param input - The input containing roles, actions, and container
|
||||
* @returns true if all actions are permitted, false otherwise
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const canWrite = await hasPermission({
|
||||
* roles: ['role_123'],
|
||||
* actions: { resource: 'product', operation: 'write' },
|
||||
* container
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export async function hasPermission(
|
||||
input: HasPermissionInput
|
||||
): Promise<boolean> {
|
||||
const { roles, actions, container } = input
|
||||
|
||||
const roleIds = Array.isArray(roles) ? roles : [roles]
|
||||
const actionList = Array.isArray(actions) ? actions : [actions]
|
||||
|
||||
const isDisabled = !FeatureFlag.isFeatureEnabled(RbacFeatureFlag.key)
|
||||
if (isDisabled || !roleIds?.length || !actionList?.length) {
|
||||
return true
|
||||
}
|
||||
|
||||
const rolePoliciesMap = await fetchRolePolicies(roleIds, container)
|
||||
|
||||
for (const action of actionList) {
|
||||
let hasAccess = false
|
||||
|
||||
for (const roleId of roleIds) {
|
||||
const resourceMap = rolePoliciesMap.get(roleId)
|
||||
if (!resourceMap) {
|
||||
continue
|
||||
}
|
||||
|
||||
const operations = resourceMap.get(action.resource)
|
||||
if (
|
||||
operations &&
|
||||
(operations.has(action.operation) || operations.has("*"))
|
||||
) {
|
||||
hasAccess = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasAccess) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a single role's policies from cache or database.
|
||||
*/
|
||||
async function fetchSingleRolePolicies(
|
||||
roleId: string,
|
||||
container: MedusaContainer
|
||||
): Promise<Map<string, Set<string>>> {
|
||||
const query = container.resolve(ContainerRegistrationKeys.QUERY)
|
||||
|
||||
const tags: string[] = []
|
||||
return await useCache<Map<string, Set<string>>>(
|
||||
async () => {
|
||||
const { data: roles } = await query.graph({
|
||||
entity: "rbac_role",
|
||||
fields: ["id", "policies.*"],
|
||||
filters: { id: roleId },
|
||||
})
|
||||
|
||||
const role = roles[0]
|
||||
const resourceMap = new Map<string, Set<string>>()
|
||||
|
||||
tags.push(`rbac_role:${roleId}`)
|
||||
if (role?.policies && Array.isArray(role.policies)) {
|
||||
const policyIds: string[] = []
|
||||
|
||||
for (const policy of role.policies) {
|
||||
policyIds.push(policy.id)
|
||||
|
||||
if (!resourceMap.has(policy.resource)) {
|
||||
resourceMap.set(policy.resource, new Set())
|
||||
}
|
||||
resourceMap.get(policy.resource)!.add(policy.operation)
|
||||
|
||||
tags.push(`rbac_policy:${policy.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
return resourceMap
|
||||
},
|
||||
{
|
||||
container,
|
||||
key: roleId,
|
||||
tags,
|
||||
ttl: 60 * 60 * 24 * 7,
|
||||
providers: ["cache-memory"],
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches policies for multiple roles by composing individually cached role queries.
|
||||
*/
|
||||
async function fetchRolePolicies(
|
||||
roleIds: string[],
|
||||
container: MedusaContainer
|
||||
): Promise<RolePoliciesCache> {
|
||||
const rolePoliciesMap: RolePoliciesCache = new Map()
|
||||
|
||||
await Promise.all(
|
||||
roleIds.map(async (roleId) => {
|
||||
const resourceMap = await fetchSingleRolePolicies(roleId, container)
|
||||
rolePoliciesMap.set(roleId, resourceMap)
|
||||
})
|
||||
)
|
||||
|
||||
return rolePoliciesMap
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from "./cart-payment-collection"
|
||||
export * from "./cart-promotion"
|
||||
export * from "./customer-account-holder"
|
||||
export * from "./fulfillment-provider-location"
|
||||
export * from "./fulfillment-set-location"
|
||||
export * from "./order-cart"
|
||||
@@ -8,6 +9,7 @@ export * from "./order-payment-collection"
|
||||
export * from "./order-promotion"
|
||||
export * from "./order-return-fulfillment"
|
||||
export * from "./product-sales-channel"
|
||||
export * from "./product-shipping-profile"
|
||||
export * from "./product-variant-inventory-item"
|
||||
export * from "./product-variant-price-set"
|
||||
export * from "./publishable-api-key-sales-channel"
|
||||
@@ -15,5 +17,4 @@ export * from "./readonly"
|
||||
export * from "./region-payment-provider"
|
||||
export * from "./sales-channel-location"
|
||||
export * from "./shipping-option-price-set"
|
||||
export * from "./product-shipping-profile"
|
||||
export * from "./customer-account-holder"
|
||||
export * from "./user-rbac-role"
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { ModuleJoinerConfig } from "@medusajs/framework/types"
|
||||
import { LINKS, Modules } from "@medusajs/framework/utils"
|
||||
|
||||
export const UserRbacRole: ModuleJoinerConfig = {
|
||||
serviceName: LINKS.UserRbacRole,
|
||||
isLink: true,
|
||||
databaseConfig: {
|
||||
tableName: "user_rbac_role",
|
||||
idPrefix: "userrole",
|
||||
},
|
||||
alias: [
|
||||
{
|
||||
name: "user_rbac_role",
|
||||
},
|
||||
{
|
||||
name: "user_rbac_roles",
|
||||
},
|
||||
],
|
||||
primaryKeys: ["id", "user_id", "rbac_role_id"],
|
||||
relationships: [
|
||||
{
|
||||
serviceName: Modules.USER,
|
||||
entity: "User",
|
||||
primaryKey: "id",
|
||||
foreignKey: "user_id",
|
||||
alias: "user",
|
||||
args: {
|
||||
methodSuffix: "Users",
|
||||
},
|
||||
hasMany: true,
|
||||
},
|
||||
{
|
||||
serviceName: Modules.RBAC,
|
||||
entity: "RbacRole",
|
||||
primaryKey: "id",
|
||||
foreignKey: "rbac_role_id",
|
||||
alias: "rbac_role",
|
||||
args: {
|
||||
methodSuffix: "RbacRoles",
|
||||
},
|
||||
hasMany: true,
|
||||
},
|
||||
],
|
||||
extends: [
|
||||
{
|
||||
serviceName: Modules.USER,
|
||||
entity: "User",
|
||||
fieldAlias: {
|
||||
rbac_roles: {
|
||||
path: "rbac_roles_link.rbac_role",
|
||||
isList: true,
|
||||
},
|
||||
},
|
||||
relationship: {
|
||||
serviceName: LINKS.UserRbacRole,
|
||||
primaryKey: "user_id",
|
||||
foreignKey: "id",
|
||||
alias: "rbac_roles_link",
|
||||
isList: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
serviceName: Modules.RBAC,
|
||||
entity: "RbacRole",
|
||||
relationship: {
|
||||
serviceName: LINKS.UserRbacRole,
|
||||
primaryKey: "rbac_role_id",
|
||||
foreignKey: "id",
|
||||
alias: "users_link",
|
||||
isList: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Module } from "@medusajs/framework/utils"
|
||||
import { Module, Modules } from "@medusajs/framework/utils"
|
||||
import { RbacModuleService } from "@services"
|
||||
|
||||
export default Module("rbac", {
|
||||
export default Module(Modules.RBAC, {
|
||||
service: RbacModuleService,
|
||||
})
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{
|
||||
"namespaces": [
|
||||
"public"
|
||||
],
|
||||
"namespaces": ["public"],
|
||||
"name": "public",
|
||||
"tables": [
|
||||
{
|
||||
@@ -143,9 +141,7 @@
|
||||
},
|
||||
{
|
||||
"keyName": "rbac_policy_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"columnNames": ["id"],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
@@ -250,9 +246,7 @@
|
||||
},
|
||||
{
|
||||
"keyName": "rbac_role_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"columnNames": ["id"],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
@@ -283,8 +277,8 @@
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"inherited_role_id": {
|
||||
"name": "inherited_role_id",
|
||||
"parent_id": {
|
||||
"name": "parent_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
@@ -334,50 +328,48 @@
|
||||
"mappedType": "datetime"
|
||||
}
|
||||
},
|
||||
"name": "rbac_role_inheritance",
|
||||
"name": "rbac_role_parent",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "IDX_rbac_role_inheritance_role_id",
|
||||
"keyName": "IDX_rbac_role_parent_role_id",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_rbac_role_inheritance_role_id\" ON \"rbac_role_inheritance\" (\"role_id\") WHERE deleted_at IS NULL"
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_rbac_role_parent_role_id\" ON \"rbac_role_parent\" (\"role_id\") WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_rbac_role_inheritance_inherited_role_id",
|
||||
"keyName": "IDX_rbac_role_parent_parent_id",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_rbac_role_inheritance_inherited_role_id\" ON \"rbac_role_inheritance\" (\"inherited_role_id\") WHERE deleted_at IS NULL"
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_rbac_role_parent_parent_id\" ON \"rbac_role_parent\" (\"parent_id\") WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_rbac_role_inheritance_deleted_at",
|
||||
"keyName": "IDX_rbac_role_parent_deleted_at",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_rbac_role_inheritance_deleted_at\" ON \"rbac_role_inheritance\" (\"deleted_at\") WHERE deleted_at IS NULL"
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_rbac_role_parent_deleted_at\" ON \"rbac_role_parent\" (\"deleted_at\") WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_rbac_role_inheritance_role_id_inherited_role_id_unique",
|
||||
"keyName": "IDX_rbac_role_parent_role_id_parent_id_unique",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_rbac_role_inheritance_role_id_inherited_role_id_unique\" ON \"rbac_role_inheritance\" (\"role_id\", \"inherited_role_id\") WHERE deleted_at IS NULL"
|
||||
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_rbac_role_parent_role_id_parent_id_unique\" ON \"rbac_role_parent\" (\"role_id\", \"parent_id\") WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "rbac_role_inheritance_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"keyName": "rbac_role_parent_pkey",
|
||||
"columnNames": ["id"],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
@@ -386,27 +378,19 @@
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {
|
||||
"rbac_role_inheritance_role_id_foreign": {
|
||||
"constraintName": "rbac_role_inheritance_role_id_foreign",
|
||||
"columnNames": [
|
||||
"role_id"
|
||||
],
|
||||
"localTableName": "public.rbac_role_inheritance",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"rbac_role_parent_role_id_foreign": {
|
||||
"constraintName": "rbac_role_parent_role_id_foreign",
|
||||
"columnNames": ["role_id"],
|
||||
"localTableName": "public.rbac_role_parent",
|
||||
"referencedColumnNames": ["id"],
|
||||
"referencedTableName": "public.rbac_role",
|
||||
"updateRule": "cascade"
|
||||
},
|
||||
"rbac_role_inheritance_inherited_role_id_foreign": {
|
||||
"constraintName": "rbac_role_inheritance_inherited_role_id_foreign",
|
||||
"columnNames": [
|
||||
"inherited_role_id"
|
||||
],
|
||||
"localTableName": "public.rbac_role_inheritance",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"rbac_role_parent_parent_id_foreign": {
|
||||
"constraintName": "rbac_role_parent_parent_id_foreign",
|
||||
"columnNames": ["parent_id"],
|
||||
"localTableName": "public.rbac_role_parent",
|
||||
"referencedColumnNames": ["id"],
|
||||
"referencedTableName": "public.rbac_role",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
@@ -433,8 +417,8 @@
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"scope_id": {
|
||||
"name": "scope_id",
|
||||
"policy_id": {
|
||||
"name": "policy_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
@@ -497,13 +481,13 @@
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_rbac_role_policy_role_id\" ON \"rbac_role_policy\" (\"role_id\") WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_rbac_role_policy_scope_id",
|
||||
"keyName": "IDX_rbac_role_policy_policy_id",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_rbac_role_policy_scope_id\" ON \"rbac_role_policy\" (\"scope_id\") WHERE deleted_at IS NULL"
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_rbac_role_policy_policy_id\" ON \"rbac_role_policy\" (\"policy_id\") WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_rbac_role_policy_deleted_at",
|
||||
@@ -515,19 +499,17 @@
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_rbac_role_policy_deleted_at\" ON \"rbac_role_policy\" (\"deleted_at\") WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_rbac_role_policy_role_id_scope_id_unique",
|
||||
"keyName": "IDX_rbac_role_policy_role_id_policy_id_unique",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"constraint": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_rbac_role_policy_role_id_scope_id_unique\" ON \"rbac_role_policy\" (\"role_id\", \"scope_id\") WHERE deleted_at IS NULL"
|
||||
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_rbac_role_policy_role_id_policy_id_unique\" ON \"rbac_role_policy\" (\"role_id\", \"policy_id\") WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "rbac_role_policy_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"columnNames": ["id"],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
@@ -538,25 +520,17 @@
|
||||
"foreignKeys": {
|
||||
"rbac_role_policy_role_id_foreign": {
|
||||
"constraintName": "rbac_role_policy_role_id_foreign",
|
||||
"columnNames": [
|
||||
"role_id"
|
||||
],
|
||||
"columnNames": ["role_id"],
|
||||
"localTableName": "public.rbac_role_policy",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedColumnNames": ["id"],
|
||||
"referencedTableName": "public.rbac_role",
|
||||
"updateRule": "cascade"
|
||||
},
|
||||
"rbac_role_policy_scope_id_foreign": {
|
||||
"constraintName": "rbac_role_policy_scope_id_foreign",
|
||||
"columnNames": [
|
||||
"scope_id"
|
||||
],
|
||||
"rbac_role_policy_policy_id_foreign": {
|
||||
"constraintName": "rbac_role_policy_policy_id_foreign",
|
||||
"columnNames": ["policy_id"],
|
||||
"localTableName": "public.rbac_role_policy",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedColumnNames": ["id"],
|
||||
"referencedTableName": "public.rbac_policy",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
import { Migration } from "@medusajs/framework/mikro-orm/migrations";
|
||||
|
||||
export class Migration20251215113723 extends Migration {
|
||||
export class Migration20251219163509 extends Migration {
|
||||
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(`alter table if exists "rbac_role_policy" drop constraint if exists "rbac_role_policy_role_id_scope_id_unique";`);
|
||||
this.addSql(`alter table if exists "rbac_role_inheritance" drop constraint if exists "rbac_role_inheritance_role_id_inherited_role_id_unique";`);
|
||||
this.addSql(`alter table if exists "rbac_role_policy" drop constraint if exists "rbac_role_policy_role_id_policy_id_unique";`);
|
||||
this.addSql(`alter table if exists "rbac_role_parent" drop constraint if exists "rbac_role_parent_role_id_parent_id_unique";`);
|
||||
this.addSql(`alter table if exists "rbac_role" drop constraint if exists "rbac_role_name_unique";`);
|
||||
this.addSql(`alter table if exists "rbac_policy" drop constraint if exists "rbac_policy_key_unique";`);
|
||||
this.addSql(`create table if not exists "rbac_policy" ("id" text not null, "key" text not null, "resource" text not null, "operation" text not null, "name" text null, "description" text null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "rbac_policy_pkey" primary key ("id"));`);
|
||||
@@ -17,23 +17,23 @@ export class Migration20251215113723 extends Migration {
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_rbac_role_deleted_at" ON "rbac_role" ("deleted_at") WHERE deleted_at IS NULL;`);
|
||||
this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_rbac_role_name_unique" ON "rbac_role" ("name") WHERE deleted_at IS NULL;`);
|
||||
|
||||
this.addSql(`create table if not exists "rbac_role_inheritance" ("id" text not null, "role_id" text not null, "inherited_role_id" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "rbac_role_inheritance_pkey" primary key ("id"));`);
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_rbac_role_inheritance_role_id" ON "rbac_role_inheritance" ("role_id") WHERE deleted_at IS NULL;`);
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_rbac_role_inheritance_inherited_role_id" ON "rbac_role_inheritance" ("inherited_role_id") WHERE deleted_at IS NULL;`);
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_rbac_role_inheritance_deleted_at" ON "rbac_role_inheritance" ("deleted_at") WHERE deleted_at IS NULL;`);
|
||||
this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_rbac_role_inheritance_role_id_inherited_role_id_unique" ON "rbac_role_inheritance" ("role_id", "inherited_role_id") WHERE deleted_at IS NULL;`);
|
||||
this.addSql(`create table if not exists "rbac_role_parent" ("id" text not null, "role_id" text not null, "parent_id" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "rbac_role_parent_pkey" primary key ("id"));`);
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_rbac_role_parent_role_id" ON "rbac_role_parent" ("role_id") WHERE deleted_at IS NULL;`);
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_rbac_role_parent_parent_id" ON "rbac_role_parent" ("parent_id") WHERE deleted_at IS NULL;`);
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_rbac_role_parent_deleted_at" ON "rbac_role_parent" ("deleted_at") WHERE deleted_at IS NULL;`);
|
||||
this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_rbac_role_parent_role_id_parent_id_unique" ON "rbac_role_parent" ("role_id", "parent_id") WHERE deleted_at IS NULL;`);
|
||||
|
||||
this.addSql(`create table if not exists "rbac_role_policy" ("id" text not null, "role_id" text not null, "scope_id" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "rbac_role_policy_pkey" primary key ("id"));`);
|
||||
this.addSql(`create table if not exists "rbac_role_policy" ("id" text not null, "role_id" text not null, "policy_id" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "rbac_role_policy_pkey" primary key ("id"));`);
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_rbac_role_policy_role_id" ON "rbac_role_policy" ("role_id") WHERE deleted_at IS NULL;`);
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_rbac_role_policy_scope_id" ON "rbac_role_policy" ("scope_id") WHERE deleted_at IS NULL;`);
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_rbac_role_policy_policy_id" ON "rbac_role_policy" ("policy_id") WHERE deleted_at IS NULL;`);
|
||||
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_rbac_role_policy_deleted_at" ON "rbac_role_policy" ("deleted_at") WHERE deleted_at IS NULL;`);
|
||||
this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_rbac_role_policy_role_id_scope_id_unique" ON "rbac_role_policy" ("role_id", "scope_id") WHERE deleted_at IS NULL;`);
|
||||
this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_rbac_role_policy_role_id_policy_id_unique" ON "rbac_role_policy" ("role_id", "policy_id") WHERE deleted_at IS NULL;`);
|
||||
|
||||
this.addSql(`alter table if exists "rbac_role_inheritance" add constraint "rbac_role_inheritance_role_id_foreign" foreign key ("role_id") references "rbac_role" ("id") on update cascade;`);
|
||||
this.addSql(`alter table if exists "rbac_role_inheritance" add constraint "rbac_role_inheritance_inherited_role_id_foreign" foreign key ("inherited_role_id") references "rbac_role" ("id") on update cascade;`);
|
||||
this.addSql(`alter table if exists "rbac_role_parent" add constraint "rbac_role_parent_role_id_foreign" foreign key ("role_id") references "rbac_role" ("id") on update cascade;`);
|
||||
this.addSql(`alter table if exists "rbac_role_parent" add constraint "rbac_role_parent_parent_id_foreign" foreign key ("parent_id") references "rbac_role" ("id") on update cascade;`);
|
||||
|
||||
this.addSql(`alter table if exists "rbac_role_policy" add constraint "rbac_role_policy_role_id_foreign" foreign key ("role_id") references "rbac_role" ("id") on update cascade;`);
|
||||
this.addSql(`alter table if exists "rbac_role_policy" add constraint "rbac_role_policy_scope_id_foreign" foreign key ("scope_id") references "rbac_policy" ("id") on update cascade;`);
|
||||
this.addSql(`alter table if exists "rbac_role_policy" add constraint "rbac_role_policy_policy_id_foreign" foreign key ("policy_id") references "rbac_policy" ("id") on update cascade;`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export { default as RbacPolicy } from "./rbac-policy"
|
||||
export { default as RbacRole } from "./rbac-role"
|
||||
export { default as RbacRoleInheritance } from "./rbac-role-inheritance"
|
||||
export { default as RbacRoleParent } from "./rbac-role-parent"
|
||||
export { default as RbacRolePolicy } from "./rbac-role-policy"
|
||||
|
||||
27
packages/modules/rbac/src/models/rbac-role-parent.ts
Normal file
27
packages/modules/rbac/src/models/rbac-role-parent.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
import RbacRole from "./rbac-role"
|
||||
|
||||
const RbacRoleParent = model
|
||||
.define("rbac_role_parent", {
|
||||
id: model.id({ prefix: "rlin" }).primaryKey(),
|
||||
role: model.belongsTo(() => RbacRole, { mappedBy: "parents" }),
|
||||
parent: model.belongsTo(() => RbacRole),
|
||||
metadata: model.json().nullable(),
|
||||
})
|
||||
.indexes([
|
||||
{
|
||||
on: ["role_id"],
|
||||
where: "deleted_at IS NULL",
|
||||
},
|
||||
{
|
||||
on: ["parent_id"],
|
||||
where: "deleted_at IS NULL",
|
||||
},
|
||||
{
|
||||
on: ["role_id", "parent_id"],
|
||||
unique: true,
|
||||
where: "deleted_at IS NULL",
|
||||
},
|
||||
])
|
||||
|
||||
export default RbacRoleParent
|
||||
@@ -6,7 +6,7 @@ const RbacRolePolicy = model
|
||||
.define("rbac_role_policy", {
|
||||
id: model.id({ prefix: "rlpl" }).primaryKey(),
|
||||
role: model.belongsTo(() => RbacRole),
|
||||
scope: model.belongsTo(() => RbacPolicy),
|
||||
policy: model.belongsTo(() => RbacPolicy),
|
||||
metadata: model.json().nullable(),
|
||||
})
|
||||
.indexes([
|
||||
@@ -15,11 +15,11 @@ const RbacRolePolicy = model
|
||||
where: "deleted_at IS NULL",
|
||||
},
|
||||
{
|
||||
on: ["scope_id"],
|
||||
on: ["policy_id"],
|
||||
where: "deleted_at IS NULL",
|
||||
},
|
||||
{
|
||||
on: ["role_id", "scope_id"],
|
||||
on: ["role_id", "policy_id"],
|
||||
unique: true,
|
||||
where: "deleted_at IS NULL",
|
||||
},
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
import RbacRoleParent from "./rbac-role-parent"
|
||||
import RbacRolePolicy from "./rbac-role-policy"
|
||||
|
||||
const RbacRole = model
|
||||
.define("rbac_role", {
|
||||
@@ -6,6 +8,12 @@ const RbacRole = model
|
||||
name: model.text().searchable(),
|
||||
description: model.text().nullable(),
|
||||
metadata: model.json().nullable(),
|
||||
policies: model.hasMany(() => RbacRolePolicy, {
|
||||
mappedBy: "role",
|
||||
}),
|
||||
parents: model.hasMany(() => RbacRoleParent, {
|
||||
mappedBy: "role",
|
||||
}),
|
||||
})
|
||||
.indexes([
|
||||
{
|
||||
|
||||
@@ -35,17 +35,19 @@ export class RbacRepository extends MikroOrmBase {
|
||||
|
||||
const query = `
|
||||
WITH RECURSIVE role_hierarchy AS (
|
||||
SELECT id, name, id as original_role_id
|
||||
SELECT id, name, id as original_role_id, ARRAY[id] as path
|
||||
FROM rbac_role
|
||||
WHERE id IN (${placeholders}) AND deleted_at IS NULL
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT r.id, r.name, rh.original_role_id
|
||||
SELECT r.id, r.name, rh.original_role_id, rh.path || r.id
|
||||
FROM rbac_role r
|
||||
INNER JOIN rbac_role_inheritance ri ON ri.inherited_role_id = r.id
|
||||
INNER JOIN rbac_role_parent ri ON ri.parent_id = r.id
|
||||
INNER JOIN role_hierarchy rh ON rh.id = ri.role_id
|
||||
WHERE r.deleted_at IS NULL AND ri.deleted_at IS NULL
|
||||
WHERE r.deleted_at IS NULL
|
||||
AND ri.deleted_at IS NULL
|
||||
AND NOT (r.id = ANY(rh.path))
|
||||
)
|
||||
SELECT DISTINCT
|
||||
rh.original_role_id,
|
||||
@@ -60,7 +62,7 @@ export class RbacRepository extends MikroOrmBase {
|
||||
p.updated_at,
|
||||
CASE WHEN rp.role_id = rh.original_role_id THEN NULL ELSE rp.role_id END as inherited_from_role_id
|
||||
FROM rbac_policy p
|
||||
INNER JOIN rbac_role_policy rp ON rp.scope_id = p.id
|
||||
INNER JOIN rbac_role_policy rp ON rp.policy_id = p.id
|
||||
INNER JOIN role_hierarchy rh ON rh.id = rp.role_id
|
||||
WHERE p.deleted_at IS NULL AND rp.deleted_at IS NULL
|
||||
ORDER BY rh.original_role_id, p.resource, p.operation, p.key
|
||||
@@ -85,4 +87,40 @@ export class RbacRepository extends MikroOrmBase {
|
||||
|
||||
return policiesByRole
|
||||
}
|
||||
|
||||
async checkForCycle(
|
||||
roleId: string,
|
||||
parentId: string,
|
||||
sharedContext: Context = {}
|
||||
): Promise<boolean> {
|
||||
const manager = this.getActiveManager<SqlEntityManager>(sharedContext)
|
||||
const knex = manager.getKnex()
|
||||
|
||||
// Check if adding this parent would create a circular dependency
|
||||
// A cycle exists if role_id is already an ancestor of parent_id
|
||||
// (i.e., if we traverse up from parent_id, we reach role_id)
|
||||
const query = `
|
||||
WITH RECURSIVE role_hierarchy AS (
|
||||
SELECT id, ARRAY[id] as path
|
||||
FROM rbac_role
|
||||
WHERE id = ? AND deleted_at IS NULL
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT r.id, rh.path || r.id
|
||||
FROM role_hierarchy rh
|
||||
INNER JOIN rbac_role_parent ri ON ri.role_id = rh.id
|
||||
INNER JOIN rbac_role r ON r.id = ri.parent_id
|
||||
WHERE r.deleted_at IS NULL
|
||||
AND ri.deleted_at IS NULL
|
||||
AND NOT (r.id = ANY(rh.path))
|
||||
)
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM role_hierarchy WHERE id = ?
|
||||
) as has_cycle
|
||||
`
|
||||
|
||||
const result = await knex.raw(query, [parentId, roleId])
|
||||
return result.rows[0]?.has_cycle || false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,38 @@
|
||||
import { Context, FindConfig } from "@medusajs/framework/types"
|
||||
import {
|
||||
Context,
|
||||
FilterableRbacRoleProps,
|
||||
FindConfig,
|
||||
RbacRoleDTO,
|
||||
} from "@medusajs/framework/types"
|
||||
import {
|
||||
InjectManager,
|
||||
MedusaContext,
|
||||
MedusaService,
|
||||
Policy,
|
||||
promiseAll,
|
||||
} from "@medusajs/framework/utils"
|
||||
import {
|
||||
RbacPolicy,
|
||||
RbacRole,
|
||||
RbacRoleInheritance,
|
||||
RbacRolePolicy,
|
||||
} from "@models"
|
||||
CreateRbacRoleParentDTO,
|
||||
IRbacModuleService,
|
||||
RbacRoleParentDTO,
|
||||
UpdateRbacRoleParentDTO,
|
||||
} from "@medusajs/types"
|
||||
import { RbacPolicy, RbacRole, RbacRoleParent, RbacRolePolicy } from "@models"
|
||||
import { RbacRepository } from "../repositories"
|
||||
|
||||
type InjectedDependencies = {
|
||||
rbacRepository: RbacRepository
|
||||
}
|
||||
|
||||
export default class RbacModuleService extends MedusaService<{
|
||||
RbacRole: { dto: any }
|
||||
RbacPolicy: { dto: any }
|
||||
RbacRoleInheritance: { dto: any }
|
||||
RbacRolePolicy: { dto: any }
|
||||
}>({
|
||||
export default class RbacModuleService
|
||||
extends MedusaService({
|
||||
RbacRole,
|
||||
RbacPolicy,
|
||||
RbacRoleInheritance,
|
||||
RbacRoleParent,
|
||||
RbacRolePolicy,
|
||||
}) {
|
||||
})
|
||||
implements IRbacModuleService
|
||||
{
|
||||
protected readonly rbacRepository_: RbacRepository
|
||||
|
||||
constructor({ rbacRepository }: InjectedDependencies) {
|
||||
@@ -35,6 +41,93 @@ export default class RbacModuleService extends MedusaService<{
|
||||
this.rbacRepository_ = rbacRepository
|
||||
}
|
||||
|
||||
__hooks = {
|
||||
onApplicationStart: async () => {
|
||||
this.onApplicationStart()
|
||||
},
|
||||
}
|
||||
|
||||
async onApplicationStart(): Promise<void> {
|
||||
await this.syncRegisteredPolicies()
|
||||
}
|
||||
|
||||
@InjectManager()
|
||||
private async syncRegisteredPolicies(
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<void> {
|
||||
const registeredPolicies = Object.entries(Policy).map(
|
||||
([name, { resource, operation, description }]) => ({
|
||||
key: `${resource}:${operation}`,
|
||||
name,
|
||||
resource,
|
||||
operation,
|
||||
description,
|
||||
})
|
||||
)
|
||||
|
||||
const registeredKeys = registeredPolicies.map((p) => p.key)
|
||||
|
||||
// Fetch all existing policies (including soft-deleted ones)
|
||||
const existingPolicies = await this.listRbacPolicies(
|
||||
{},
|
||||
{ withDeleted: true },
|
||||
sharedContext
|
||||
)
|
||||
|
||||
const existingPoliciesMap = new Map(existingPolicies.map((p) => [p.key, p]))
|
||||
|
||||
const policiesToCreate: any[] = []
|
||||
const policiesToUpdate: any[] = []
|
||||
const policiesToRestore: string[] = []
|
||||
|
||||
// Process registered policies
|
||||
for (const registeredPolicy of registeredPolicies) {
|
||||
const existing = existingPoliciesMap.get(registeredPolicy.key)
|
||||
|
||||
const hasChanges =
|
||||
existing &&
|
||||
(existing.name !== registeredPolicy.name ||
|
||||
existing.description !== registeredPolicy.description)
|
||||
|
||||
if (!existing) {
|
||||
policiesToCreate.push(registeredPolicy)
|
||||
} else if (existing.deleted_at) {
|
||||
policiesToRestore.push(existing.id)
|
||||
if (hasChanges) {
|
||||
policiesToUpdate.push({
|
||||
id: existing.id,
|
||||
name: registeredPolicy.name,
|
||||
description: registeredPolicy.description,
|
||||
})
|
||||
}
|
||||
} else if (hasChanges) {
|
||||
policiesToUpdate.push({
|
||||
id: existing.id,
|
||||
name: registeredPolicy.name,
|
||||
description: registeredPolicy.description,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const policiesToSoftDelete = existingPolicies
|
||||
.filter((p) => !p.deleted_at && !registeredKeys.includes(p.key))
|
||||
.map((p) => p.id)
|
||||
|
||||
// First restore any soft-deleted policies
|
||||
if (policiesToRestore.length > 0) {
|
||||
await this.restoreRbacPolicies(policiesToRestore, {}, sharedContext)
|
||||
}
|
||||
|
||||
await promiseAll([
|
||||
policiesToCreate.length > 0 &&
|
||||
this.createRbacPolicies(policiesToCreate, sharedContext),
|
||||
policiesToUpdate.length > 0 &&
|
||||
this.updateRbacPolicies(policiesToUpdate, sharedContext),
|
||||
policiesToSoftDelete.length > 0 &&
|
||||
this.softDeleteRbacPolicies(policiesToSoftDelete, {}, sharedContext),
|
||||
])
|
||||
}
|
||||
|
||||
@InjectManager()
|
||||
async listPoliciesForRole(
|
||||
roleId: string,
|
||||
@@ -46,11 +139,15 @@ export default class RbacModuleService extends MedusaService<{
|
||||
@InjectManager()
|
||||
// @ts-expect-error
|
||||
async listRbacRoles(
|
||||
filters: any = {},
|
||||
config: FindConfig<any> = {},
|
||||
filters: FilterableRbacRoleProps = {},
|
||||
config: FindConfig<RbacRoleDTO> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<any[]> {
|
||||
const roles = await super.listRbacRoles(filters, config, sharedContext)
|
||||
): Promise<RbacRoleDTO[]> {
|
||||
const roles = await super.listRbacRoles(
|
||||
filters,
|
||||
config as any,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
const shouldIncludePolicies =
|
||||
config.relations?.includes("policies") ||
|
||||
@@ -68,19 +165,19 @@ export default class RbacModuleService extends MedusaService<{
|
||||
}
|
||||
}
|
||||
|
||||
return roles
|
||||
return roles as unknown as RbacRoleDTO[]
|
||||
}
|
||||
|
||||
@InjectManager()
|
||||
// @ts-expect-error
|
||||
async listAndCountRbacRoles(
|
||||
filters: any = {},
|
||||
config: FindConfig<any> = {},
|
||||
filters: FilterableRbacRoleProps = {},
|
||||
config: FindConfig<RbacRoleDTO> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<[any[], number]> {
|
||||
): Promise<[RbacRoleDTO[], number]> {
|
||||
const [roles, count] = await super.listAndCountRbacRoles(
|
||||
filters,
|
||||
config,
|
||||
config as any,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
@@ -100,6 +197,70 @@ export default class RbacModuleService extends MedusaService<{
|
||||
}
|
||||
}
|
||||
|
||||
return [roles, count]
|
||||
return [roles as unknown as RbacRoleDTO[], count]
|
||||
}
|
||||
|
||||
@InjectManager()
|
||||
// @ts-expect-error
|
||||
async createRbacRoleParents(
|
||||
data: CreateRbacRoleParentDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<RbacRoleParentDTO[]> {
|
||||
for (const parent of data) {
|
||||
const { role_id, parent_id } = parent
|
||||
|
||||
if (role_id === parent_id) {
|
||||
throw new Error(
|
||||
`Cannot create role parent relationship: a role cannot be its own parent (role_id: ${role_id})`
|
||||
)
|
||||
}
|
||||
|
||||
const wouldCreateCycle = await this.rbacRepository_.checkForCycle(
|
||||
role_id,
|
||||
parent_id,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
if (wouldCreateCycle) {
|
||||
throw new Error(
|
||||
`Cannot create role parent relationship: this would create a circular dependency (role_id: ${role_id}, parent_id: ${parent_id})`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return await super.createRbacRoleParents(data, sharedContext)
|
||||
}
|
||||
|
||||
@InjectManager()
|
||||
// @ts-expect-error
|
||||
async updateRbacRoleParents(
|
||||
data: UpdateRbacRoleParentDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<RbacRoleParentDTO[]> {
|
||||
for (const parent of data) {
|
||||
const { role_id, parent_id } = parent
|
||||
|
||||
if (parent_id) {
|
||||
if (role_id === parent_id) {
|
||||
throw new Error(
|
||||
`Cannot update role parent relationship: a role cannot be its own parent (role_id: ${role_id})`
|
||||
)
|
||||
}
|
||||
|
||||
const wouldCreateCycle = await this.rbacRepository_.checkForCycle(
|
||||
role_id!,
|
||||
parent_id,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
if (wouldCreateCycle) {
|
||||
throw new Error(
|
||||
`Cannot update role parent relationship: this would create a circular dependency (role_id: ${role_id}, parent_id: ${parent_id})`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await super.updateRbacRoleParents(data, sharedContext)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user