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/pricing": "file:../medusa/packages/modules/pricing",
|
||||||
"@medusajs/product": "file:../medusa/packages/modules/product",
|
"@medusajs/product": "file:../medusa/packages/modules/product",
|
||||||
"@medusajs/promotion": "file:../medusa/packages/modules/promotion",
|
"@medusajs/promotion": "file:../medusa/packages/modules/promotion",
|
||||||
|
"@medusajs/rbac": "file:../medusa/packages/modules/rbac",
|
||||||
"@medusajs/region": "file:../medusa/packages/modules/region",
|
"@medusajs/region": "file:../medusa/packages/modules/region",
|
||||||
"@medusajs/sales-channel": "file:../medusa/packages/modules/sales-channel",
|
"@medusajs/sales-channel": "file:../medusa/packages/modules/sales-channel",
|
||||||
"@medusajs/stock-location": "file:../medusa/packages/modules/stock-location",
|
"@medusajs/stock-location": "file:../medusa/packages/modules/stock-location",
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
|
|
||||||
jest.setTimeout(60000)
|
jest.setTimeout(60000)
|
||||||
|
|
||||||
|
process.env.MEDUSA_FF_RBAC = "true"
|
||||||
|
|
||||||
medusaIntegrationTestRunner({
|
medusaIntegrationTestRunner({
|
||||||
testSuite: ({ dbConnection, api, getContainer }) => {
|
testSuite: ({ dbConnection, api, getContainer }) => {
|
||||||
let container
|
let container
|
||||||
@@ -15,6 +17,10 @@ medusaIntegrationTestRunner({
|
|||||||
await createAdminUser(dbConnection, adminHeaders, container)
|
await createAdminUser(dbConnection, adminHeaders, container)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
delete process.env.MEDUSA_FF_RBAC
|
||||||
|
})
|
||||||
|
|
||||||
describe("RBAC Policies - Admin API", () => {
|
describe("RBAC Policies - Admin API", () => {
|
||||||
describe("POST /admin/rbac/policies", () => {
|
describe("POST /admin/rbac/policies", () => {
|
||||||
it("should create a policy", async () => {
|
it("should create a policy", async () => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils"
|
||||||
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
||||||
import {
|
import {
|
||||||
adminHeaders,
|
adminHeaders,
|
||||||
@@ -6,6 +7,8 @@ import {
|
|||||||
|
|
||||||
jest.setTimeout(60000)
|
jest.setTimeout(60000)
|
||||||
|
|
||||||
|
process.env.MEDUSA_FF_RBAC = "true"
|
||||||
|
|
||||||
medusaIntegrationTestRunner({
|
medusaIntegrationTestRunner({
|
||||||
testSuite: ({ dbConnection, api, getContainer }) => {
|
testSuite: ({ dbConnection, api, getContainer }) => {
|
||||||
let container
|
let container
|
||||||
@@ -15,6 +18,10 @@ medusaIntegrationTestRunner({
|
|||||||
await createAdminUser(dbConnection, adminHeaders, container)
|
await createAdminUser(dbConnection, adminHeaders, container)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
delete process.env.MEDUSA_FF_RBAC
|
||||||
|
})
|
||||||
|
|
||||||
describe("RBAC Roles - Admin API", () => {
|
describe("RBAC Roles - Admin API", () => {
|
||||||
describe("POST /admin/rbac/roles", () => {
|
describe("POST /admin/rbac/roles", () => {
|
||||||
it("should create a role", async () => {
|
it("should create a role", async () => {
|
||||||
@@ -225,8 +232,13 @@ medusaIntegrationTestRunner({
|
|||||||
let policies
|
let policies
|
||||||
let viewerRole
|
let viewerRole
|
||||||
let editorRole
|
let editorRole
|
||||||
|
let adminUser
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
const userModule = container.resolve(Modules.USER)
|
||||||
|
const remoteLink = container.resolve(ContainerRegistrationKeys.LINK)
|
||||||
|
|
||||||
|
// Create policies
|
||||||
const policy1 = await api.post(
|
const policy1 = await api.post(
|
||||||
"/admin/rbac/policies",
|
"/admin/rbac/policies",
|
||||||
{
|
{
|
||||||
@@ -266,6 +278,40 @@ medusaIntegrationTestRunner({
|
|||||||
policy3.data.policy,
|
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(
|
const viewer = await api.post(
|
||||||
"/admin/rbac/roles",
|
"/admin/rbac/roles",
|
||||||
{
|
{
|
||||||
@@ -287,96 +333,91 @@ medusaIntegrationTestRunner({
|
|||||||
editorRole = editor.data.role
|
editorRole = editor.data.role
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should create role-policy associations", async () => {
|
it("should add policies to a role", async () => {
|
||||||
const response = await api.post(
|
const response = await api.post(
|
||||||
"/admin/rbac/role-policies",
|
`/admin/rbac/roles/${viewerRole.id}/policies`,
|
||||||
{
|
{
|
||||||
role_id: viewerRole.id,
|
policies: [policies[0].id],
|
||||||
scope_id: policies[0].id,
|
|
||||||
},
|
},
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
expect(response.status).toEqual(200)
|
||||||
expect(response.data.role_policy).toEqual(
|
|
||||||
expect.objectContaining({
|
expect(response.data.policies).toHaveLength(1)
|
||||||
role_id: viewerRole.id,
|
expect(response.data.policies[0]).toMatchObject({
|
||||||
scope_id: policies[0].id,
|
role_id: viewerRole.id,
|
||||||
})
|
policy_id: policies[0].id,
|
||||||
)
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should list role-policies for a specific role", async () => {
|
it("should list role-policies for a specific role", async () => {
|
||||||
|
// Add multiple policies to the role
|
||||||
await api.post(
|
await api.post(
|
||||||
"/admin/rbac/role-policies",
|
`/admin/rbac/roles/${viewerRole.id}/policies`,
|
||||||
{
|
{
|
||||||
role_id: viewerRole.id,
|
policies: [policies[0].id, policies[1].id],
|
||||||
scope_id: policies[0].id,
|
|
||||||
},
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
|
|
||||||
await api.post(
|
|
||||||
"/admin/rbac/role-policies",
|
|
||||||
{
|
|
||||||
role_id: viewerRole.id,
|
|
||||||
scope_id: policies[1].id,
|
|
||||||
},
|
},
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// List the role to get its policies
|
||||||
const response = await api.get(
|
const response = await api.get(
|
||||||
`/admin/rbac/role-policies?role_id=${viewerRole.id}`,
|
`/admin/rbac/roles/${viewerRole.id}/?fields=policies`,
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
expect(response.status).toEqual(200)
|
||||||
expect(response.data.count).toEqual(2)
|
expect(Array.isArray(response.data.role.policies)).toBe(true)
|
||||||
expect(response.data.role_policies).toEqual(
|
expect(response.data.role.policies).toHaveLength(2)
|
||||||
|
expect(response.data.role.policies).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
role_id: viewerRole.id,
|
id: policies[0].id,
|
||||||
scope_id: policies[0].id,
|
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
role_id: viewerRole.id,
|
id: policies[1].id,
|
||||||
scope_id: policies[1].id,
|
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should delete a role-policy association", async () => {
|
it("should remove a policy from a role", async () => {
|
||||||
const createResponse = await api.post(
|
// First add a policy to the role
|
||||||
"/admin/rbac/role-policies",
|
await api.post(
|
||||||
|
`/admin/rbac/roles/${editorRole.id}/policies`,
|
||||||
{
|
{
|
||||||
role_id: editorRole.id,
|
policies: [policies[2].id],
|
||||||
scope_id: policies[2].id,
|
|
||||||
},
|
},
|
||||||
adminHeaders
|
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(
|
const deleteResponse = await api.delete(
|
||||||
`/admin/rbac/role-policies/${rolePolicyId}`,
|
`/admin/rbac/roles/${editorRole.id}/policies/${policies[2].id}`,
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(deleteResponse.status).toEqual(200)
|
expect(deleteResponse.status).toEqual(200)
|
||||||
expect(deleteResponse.data).toEqual({
|
expect(deleteResponse.data).toEqual({
|
||||||
id: rolePolicyId,
|
id: expect.stringContaining("rlpl_"),
|
||||||
object: "rbac_role_policy",
|
object: "rbac_role_policy",
|
||||||
deleted: true,
|
deleted: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const listResponse = await api.get(
|
// Verify the policy was removed
|
||||||
`/admin/rbac/role-policies?role_id=${editorRole.id}`,
|
const finalResponse = await api.get(
|
||||||
|
`/admin/rbac/roles/${editorRole.id}?fields=policies`,
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
expect(
|
expect(finalResponse.data.role.policies).toHaveLength(0)
|
||||||
listResponse.data.role_policies.find((rp) => rp.id === rolePolicyId)
|
|
||||||
).toBeUndefined()
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ const modules = {
|
|||||||
},
|
},
|
||||||
[Modules.RBAC]: {
|
[Modules.RBAC]: {
|
||||||
resolve: "@medusajs/rbac",
|
resolve: "@medusajs/rbac",
|
||||||
|
disable: process.env.MEDUSA_FF_RBAC !== "true",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +90,7 @@ module.exports = defineConfig({
|
|||||||
featureFlags: {
|
featureFlags: {
|
||||||
index_engine: process.env.ENABLE_INDEX_MODULE === "true",
|
index_engine: process.env.ENABLE_INDEX_MODULE === "true",
|
||||||
translation: process.env.MEDUSA_FF_TRANSLATION === "true",
|
translation: process.env.MEDUSA_FF_TRANSLATION === "true",
|
||||||
|
rbac: process.env.MEDUSA_FF_RBAC === "true",
|
||||||
},
|
},
|
||||||
modules,
|
modules,
|
||||||
})
|
})
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,6 @@
|
|||||||
import { Modules } from "@medusajs/framework/utils"
|
import { Modules } from "@medusajs/framework/utils"
|
||||||
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
|
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
|
||||||
import { IRbacModuleService } from "@medusajs/types"
|
import { CreateRbacPolicyDTO, IRbacModuleService } from "@medusajs/types"
|
||||||
|
|
||||||
export type CreateRbacPolicyDTO = {
|
|
||||||
key: string
|
|
||||||
resource: string
|
|
||||||
operation: string
|
|
||||||
name?: string | null
|
|
||||||
description?: string | null
|
|
||||||
metadata?: Record<string, unknown> | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CreateRbacPoliciesStepInput = {
|
export type CreateRbacPoliciesStepInput = {
|
||||||
policies: CreateRbacPolicyDTO[]
|
policies: CreateRbacPolicyDTO[]
|
||||||
@@ -22,7 +13,14 @@ export const createRbacPoliciesStep = createStep(
|
|||||||
async (data: CreateRbacPoliciesStepInput, { container }) => {
|
async (data: CreateRbacPoliciesStepInput, { container }) => {
|
||||||
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
|
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(
|
return new StepResponse(
|
||||||
created,
|
created,
|
||||||
|
|||||||
@@ -2,30 +2,28 @@ import { Modules } from "@medusajs/framework/utils"
|
|||||||
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
|
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
|
||||||
import { IRbacModuleService } from "@medusajs/types"
|
import { IRbacModuleService } from "@medusajs/types"
|
||||||
|
|
||||||
export type CreateRbacRoleInheritanceDTO = {
|
export type CreateRbacRoleParentDTO = {
|
||||||
role_id: string
|
role_id: string
|
||||||
inherited_role_id: string
|
parent_id: string
|
||||||
metadata?: Record<string, unknown> | null
|
metadata?: Record<string, unknown> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CreateRbacRoleInheritancesStepInput = {
|
export type CreateRbacRoleParentsStepInput = {
|
||||||
role_inheritances: CreateRbacRoleInheritanceDTO[]
|
role_parents: CreateRbacRoleParentDTO[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createRbacRoleInheritancesStepId = "create-rbac-role-inheritances"
|
export const createRbacRoleParentsStepId = "create-rbac-role-parents"
|
||||||
|
|
||||||
export const createRbacRoleInheritancesStep = createStep(
|
export const createRbacRoleParentsStep = createStep(
|
||||||
createRbacRoleInheritancesStepId,
|
createRbacRoleParentsStepId,
|
||||||
async (data: CreateRbacRoleInheritancesStepInput, { container }) => {
|
async (data: CreateRbacRoleParentsStepInput, { container }) => {
|
||||||
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
|
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
|
||||||
|
|
||||||
if (!data.role_inheritances || data.role_inheritances.length === 0) {
|
if (!data.role_parents?.length) {
|
||||||
return new StepResponse([], [])
|
return new StepResponse([], [])
|
||||||
}
|
}
|
||||||
|
|
||||||
const created = await service.createRbacRoleInheritances(
|
const created = await service.createRbacRoleParents(data.role_parents)
|
||||||
data.role_inheritances
|
|
||||||
)
|
|
||||||
|
|
||||||
return new StepResponse(
|
return new StepResponse(
|
||||||
created,
|
created,
|
||||||
@@ -38,6 +36,6 @@ export const createRbacRoleInheritancesStep = createStep(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
|
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 { Modules } from "@medusajs/framework/utils"
|
||||||
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
|
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
|
||||||
import { IRbacModuleService } from "@medusajs/types"
|
import { CreateRbacRolePolicyDTO, IRbacModuleService } from "@medusajs/types"
|
||||||
|
|
||||||
export type CreateRbacRolePolicyDTO = {
|
|
||||||
role_id: string
|
|
||||||
scope_id: string
|
|
||||||
metadata?: Record<string, unknown> | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CreateRbacRolePoliciesStepInput = {
|
export type CreateRbacRolePoliciesStepInput = {
|
||||||
role_policies: CreateRbacRolePolicyDTO[]
|
policies: CreateRbacRolePolicyDTO[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createRbacRolePoliciesStepId = "create-rbac-role-policies"
|
export const createRbacRolePoliciesStepId = "create-rbac-role-policies"
|
||||||
@@ -19,7 +13,11 @@ export const createRbacRolePoliciesStep = createStep(
|
|||||||
async (data: CreateRbacRolePoliciesStepInput, { container }) => {
|
async (data: CreateRbacRolePoliciesStepInput, { container }) => {
|
||||||
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
|
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(
|
return new StepResponse(
|
||||||
created,
|
created,
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ export const createRbacRolesStep = createStep(
|
|||||||
async (data: CreateRbacRolesStepInput, { container }) => {
|
async (data: CreateRbacRolesStepInput, { container }) => {
|
||||||
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
|
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
|
||||||
|
|
||||||
|
if (!data.roles?.length) {
|
||||||
|
return new StepResponse([], [])
|
||||||
|
}
|
||||||
const created = await service.createRbacRoles(data.roles)
|
const created = await service.createRbacRoles(data.roles)
|
||||||
|
|
||||||
return new StepResponse(
|
return new StepResponse(
|
||||||
|
|||||||
@@ -10,8 +10,23 @@ export const deleteRbacPoliciesStep = createStep(
|
|||||||
{ name: deleteRbacPoliciesStepId, noCompensation: true },
|
{ name: deleteRbacPoliciesStepId, noCompensation: true },
|
||||||
async (ids: DeleteRbacPoliciesStepInput, { container }) => {
|
async (ids: DeleteRbacPoliciesStepInput, { container }) => {
|
||||||
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
|
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 },
|
{ name: deleteRbacRolePoliciesStepId, noCompensation: true },
|
||||||
async (ids: DeleteRbacRolePoliciesStepInput, { container }) => {
|
async (ids: DeleteRbacRolePoliciesStepInput, { container }) => {
|
||||||
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
|
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"
|
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(
|
export const deleteRbacRolesStep = createStep(
|
||||||
{ name: deleteRbacRolesStepId, noCompensation: true },
|
deleteRbacRolesStepId,
|
||||||
async (ids: DeleteRbacRolesStepInput, { container }) => {
|
async (ids: DeleteRbacRolesStepInput, { container }) => {
|
||||||
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
|
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 "./create-rbac-policies"
|
||||||
export * from "./delete-rbac-policies"
|
export * from "./create-rbac-role-parents"
|
||||||
export * from "./update-rbac-policies"
|
|
||||||
|
|
||||||
export * from "./create-rbac-role-policies"
|
export * from "./create-rbac-role-policies"
|
||||||
|
export * from "./create-rbac-roles"
|
||||||
|
export * from "./delete-rbac-policies"
|
||||||
export * from "./delete-rbac-role-policies"
|
export * from "./delete-rbac-role-policies"
|
||||||
export * from "./update-rbac-role-policies"
|
export * from "./delete-rbac-roles"
|
||||||
|
export * from "./set-role-parent"
|
||||||
export * from "./create-rbac-role-inheritances"
|
export * from "./update-rbac-policies"
|
||||||
export * from "./set-role-inheritance"
|
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 { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
|
||||||
import { IRbacModuleService } from "@medusajs/types"
|
import { IRbacModuleService } from "@medusajs/types"
|
||||||
|
|
||||||
export type SetRoleInheritanceStepInput = Array<{
|
export type SetRoleParentStepInput = Array<{
|
||||||
role_id: string
|
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(
|
export const setRoleParentStep = createStep(
|
||||||
setRoleInheritanceStepId,
|
setRoleParentStepId,
|
||||||
async (data: SetRoleInheritanceStepInput, { container }) => {
|
async (data: SetRoleParentStepInput, { container }) => {
|
||||||
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
|
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
|
||||||
|
|
||||||
const allCompensationData: Array<{
|
const allCompensationData: Array<{
|
||||||
role_id: string
|
role_id: string
|
||||||
previousInheritedRoleIds: string[]
|
previous_inherited_role_ids: string[]
|
||||||
}> = []
|
}> = []
|
||||||
|
|
||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
@@ -29,54 +29,52 @@ export const setRoleInheritanceStep = createStep(
|
|||||||
const allToRemoveIds: string[] = []
|
const allToRemoveIds: string[] = []
|
||||||
const allToCreate: Array<{
|
const allToCreate: Array<{
|
||||||
role_id: string
|
role_id: string
|
||||||
inherited_role_id: string
|
parent_id: string
|
||||||
}> = []
|
}> = []
|
||||||
|
|
||||||
for (const roleData of data) {
|
for (const roleData of data) {
|
||||||
const existingInheritance = await service.listRbacRoleInheritances({
|
const existingParent = await service.listRbacRoleParents({
|
||||||
role_id: roleData.role_id,
|
role_id: roleData.role_id,
|
||||||
})
|
})
|
||||||
|
|
||||||
const existingInheritedRoleIds = existingInheritance.map(
|
const existingInheritedRoleIds = existingParent.map((ri) => ri.parent_id)
|
||||||
(ri) => ri.inherited_role_id
|
|
||||||
)
|
|
||||||
|
|
||||||
allCompensationData.push({
|
allCompensationData.push({
|
||||||
role_id: roleData.role_id,
|
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)
|
(id) => !existingInheritedRoleIds.includes(id)
|
||||||
)
|
)
|
||||||
const toRemove = existingInheritedRoleIds.filter(
|
const toRemove = existingInheritedRoleIds.filter(
|
||||||
(id) => !roleData.inherited_role_ids.includes(id)
|
(id) => !roleData.parent_ids.includes(id)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (toRemove.length > 0) {
|
if (toRemove.length > 0) {
|
||||||
const toRemoveRecords = existingInheritance.filter((ri) =>
|
const toRemoveRecords = existingParent.filter((ri) =>
|
||||||
toRemove.includes(ri.inherited_role_id)
|
toRemove.includes(ri.parent_id)
|
||||||
)
|
)
|
||||||
allToRemoveIds.push(...toRemoveRecords.map((ri) => ri.id))
|
allToRemoveIds.push(...toRemoveRecords.map((ri) => ri.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toAdd.length > 0) {
|
if (toAdd.length > 0) {
|
||||||
allToCreate.push(
|
allToCreate.push(
|
||||||
...toAdd.map((inherited_role_id) => ({
|
...toAdd.map((parent_id) => ({
|
||||||
role_id: roleData.role_id,
|
role_id: roleData.role_id,
|
||||||
inherited_role_id,
|
parent_id,
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allToRemoveIds.length > 0) {
|
if (allToRemoveIds.length > 0) {
|
||||||
await service.deleteRbacRoleInheritances(allToRemoveIds)
|
await service.deleteRbacRoleParents(allToRemoveIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
let created: any[] = []
|
let created: any[] = []
|
||||||
if (allToCreate.length > 0) {
|
if (allToCreate.length > 0) {
|
||||||
created = await service.createRbacRoleInheritances(allToCreate)
|
created = await service.createRbacRoleParents(allToCreate)
|
||||||
}
|
}
|
||||||
|
|
||||||
return new StepResponse(
|
return new StepResponse(
|
||||||
@@ -86,7 +84,7 @@ export const setRoleInheritanceStep = createStep(
|
|||||||
},
|
},
|
||||||
async (
|
async (
|
||||||
compensationData:
|
compensationData:
|
||||||
| Array<{ role_id: string; previousInheritedRoleIds: string[] }>
|
| Array<{ role_id: string; previous_inherited_role_ids: string[] }>
|
||||||
| undefined,
|
| undefined,
|
||||||
{ container }
|
{ container }
|
||||||
) => {
|
) => {
|
||||||
@@ -97,24 +95,20 @@ export const setRoleInheritanceStep = createStep(
|
|||||||
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
|
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
|
||||||
|
|
||||||
for (const roleCompensation of compensationData) {
|
for (const roleCompensation of compensationData) {
|
||||||
const currentInheritance = await service.listRbacRoleInheritances({
|
const currentParent = await service.listRbacRoleParents({
|
||||||
role_id: roleCompensation.role_id,
|
role_id: roleCompensation.role_id,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (currentInheritance.length > 0) {
|
if (currentParent.length > 0) {
|
||||||
await service.deleteRbacRoleInheritances(
|
await service.deleteRbacRoleParents(currentParent.map((ri) => ri.id))
|
||||||
currentInheritance.map((ri) => ri.id)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (roleCompensation.previousInheritedRoleIds.length > 0) {
|
if (roleCompensation.previous_inherited_role_ids.length > 0) {
|
||||||
await service.createRbacRoleInheritances(
|
await service.createRbacRoleParents(
|
||||||
roleCompensation.previousInheritedRoleIds.map(
|
roleCompensation.previous_inherited_role_ids.map((parent_id) => ({
|
||||||
(inherited_role_id) => ({
|
role_id: roleCompensation.role_id,
|
||||||
role_id: roleCompensation.role_id,
|
parent_id,
|
||||||
inherited_role_id,
|
}))
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,9 +26,18 @@ export const updateRbacPoliciesStep = createStep(
|
|||||||
relations,
|
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) => ({
|
const updates = (prevData ?? []).map((p) => ({
|
||||||
id: p.id,
|
id: p.id,
|
||||||
...data.update,
|
...normalizedUpdate,
|
||||||
})) as UpdateRbacPolicyDTO[]
|
})) as UpdateRbacPolicyDTO[]
|
||||||
|
|
||||||
const updated = await service.updateRbacPolicies(updates)
|
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,
|
WorkflowData,
|
||||||
WorkflowResponse,
|
WorkflowResponse,
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
|
transform,
|
||||||
|
when,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import { createRbacRolePoliciesStep } from "../steps"
|
import { createRbacRolePoliciesStep } from "../steps"
|
||||||
|
import { validateUserPermissionsStep } from "../steps/validate-user-permissions"
|
||||||
|
|
||||||
export type CreateRbacRolePoliciesWorkflowInput = {
|
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"
|
export const createRbacRolePoliciesWorkflowId = "create-rbac-role-policies"
|
||||||
@@ -14,6 +22,31 @@ export const createRbacRolePoliciesWorkflowId = "create-rbac-role-policies"
|
|||||||
export const createRbacRolePoliciesWorkflow = createWorkflow(
|
export const createRbacRolePoliciesWorkflow = createWorkflow(
|
||||||
createRbacRolePoliciesWorkflowId,
|
createRbacRolePoliciesWorkflowId,
|
||||||
(input: WorkflowData<CreateRbacRolePoliciesWorkflowInput>) => {
|
(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 {
|
import {
|
||||||
WorkflowData,
|
|
||||||
WorkflowResponse,
|
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
transform,
|
transform,
|
||||||
|
when,
|
||||||
|
WorkflowData,
|
||||||
|
WorkflowResponse,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import {
|
import {
|
||||||
createRbacRoleInheritancesStep,
|
createRbacRoleParentsStep,
|
||||||
createRbacRolePoliciesStep,
|
createRbacRolePoliciesStep,
|
||||||
createRbacRolesStep,
|
createRbacRolesStep,
|
||||||
} from "../steps"
|
} from "../steps"
|
||||||
|
import { validateUserPermissionsStep } from "../steps/validate-user-permissions"
|
||||||
|
|
||||||
export type CreateRbacRolesWorkflowInput = {
|
export type CreateRbacRolesWorkflowInput = {
|
||||||
|
actor_id?: string
|
||||||
|
actor?: string
|
||||||
roles: {
|
roles: {
|
||||||
name: string
|
name: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
metadata?: Record<string, unknown> | null
|
metadata?: Record<string, unknown> | null
|
||||||
inherited_role_ids?: string[]
|
parent_ids?: string[]
|
||||||
policy_ids?: string[]
|
policy_ids?: string[]
|
||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
@@ -25,6 +29,24 @@ export const createRbacRolesWorkflowId = "create-rbac-roles"
|
|||||||
export const createRbacRolesWorkflow = createWorkflow(
|
export const createRbacRolesWorkflow = createWorkflow(
|
||||||
createRbacRolesWorkflowId,
|
createRbacRolesWorkflowId,
|
||||||
(input: WorkflowData<CreateRbacRolesWorkflowInput>) => {
|
(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 }) => ({
|
const roleData = transform({ input }, ({ input }) => ({
|
||||||
roles: input.roles.map((r) => ({
|
roles: input.roles.map((r) => ({
|
||||||
name: r.name,
|
name: r.name,
|
||||||
@@ -35,22 +57,22 @@ export const createRbacRolesWorkflow = createWorkflow(
|
|||||||
|
|
||||||
const createdRoles = createRbacRolesStep(roleData)
|
const createdRoles = createRbacRolesStep(roleData)
|
||||||
|
|
||||||
const inheritanceData = transform(
|
const parentData = transform(
|
||||||
{ input, createdRoles },
|
{ input, createdRoles },
|
||||||
({ input, createdRoles }) => {
|
({ input, createdRoles }) => {
|
||||||
const inheritances: any[] = []
|
const parents: any[] = []
|
||||||
|
|
||||||
createdRoles.forEach((role, index) => {
|
createdRoles.forEach((role, index) => {
|
||||||
const inheritedRoleIds = input.roles[index].inherited_role_ids || []
|
const inheritedRoleIds = input.roles[index].parent_ids || []
|
||||||
inheritedRoleIds.forEach((inheritedRoleId) => {
|
inheritedRoleIds.forEach((inheritedRoleId) => {
|
||||||
inheritances.push({
|
parents.push({
|
||||||
role_id: role.id,
|
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) => {
|
policyIds.forEach((policy_id) => {
|
||||||
allPolicies.push({
|
allPolicies.push({
|
||||||
role_id: role.id,
|
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)
|
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"
|
import { deleteRbacRolePoliciesStep } from "../steps"
|
||||||
|
|
||||||
export type DeleteRbacRolePoliciesWorkflowInput = {
|
export type DeleteRbacRolePoliciesWorkflowInput = {
|
||||||
ids: string[]
|
role_policy_ids: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deleteRbacRolePoliciesWorkflowId = "delete-rbac-role-policies"
|
export const deleteRbacRolePoliciesWorkflowId = "delete-rbac-role-policies"
|
||||||
|
|
||||||
export const deleteRbacRolePoliciesWorkflow = createWorkflow(
|
export const deleteRbacRolePoliciesWorkflow = createWorkflow(
|
||||||
deleteRbacRolePoliciesWorkflowId,
|
deleteRbacRolePoliciesWorkflowId,
|
||||||
(
|
(input: WorkflowData<DeleteRbacRolePoliciesWorkflowInput>) => {
|
||||||
input: WorkflowData<DeleteRbacRolePoliciesWorkflowInput>
|
const deletedRolePolicies = deleteRbacRolePoliciesStep(
|
||||||
): WorkflowData<void> => {
|
input.role_policy_ids
|
||||||
deleteRbacRolePoliciesStep(input.ids)
|
)
|
||||||
|
|
||||||
|
return new WorkflowResponse(deletedRolePolicies)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,4 +8,3 @@ export * from "./update-rbac-policies"
|
|||||||
|
|
||||||
export * from "./create-rbac-role-policies"
|
export * from "./create-rbac-role-policies"
|
||||||
export * from "./delete-rbac-role-policies"
|
export * from "./delete-rbac-role-policies"
|
||||||
export * from "./update-rbac-role-policies"
|
|
||||||
|
|||||||
@@ -4,15 +4,19 @@ import {
|
|||||||
WorkflowResponse,
|
WorkflowResponse,
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
transform,
|
transform,
|
||||||
|
when,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import { UpdateRbacRoleDTO } from "@medusajs/types"
|
import { UpdateRbacRoleDTO } from "@medusajs/types"
|
||||||
import { createRbacRolePoliciesStep, setRoleInheritanceStep } from "../steps"
|
import { createRbacRolePoliciesStep, setRoleParentStep } from "../steps"
|
||||||
import { updateRbacRolesStep } from "../steps/update-rbac-roles"
|
import { updateRbacRolesStep } from "../steps/update-rbac-roles"
|
||||||
|
import { validateUserPermissionsStep } from "../steps/validate-user-permissions"
|
||||||
|
|
||||||
export type UpdateRbacRolesWorkflowInput = {
|
export type UpdateRbacRolesWorkflowInput = {
|
||||||
|
actor_id?: string
|
||||||
|
actor?: string
|
||||||
selector: Record<string, any>
|
selector: Record<string, any>
|
||||||
update: Omit<UpdateRbacRoleDTO, "id"> & {
|
update: Omit<UpdateRbacRoleDTO, "id"> & {
|
||||||
inherited_role_ids?: string[]
|
parent_ids?: string[]
|
||||||
policy_ids?: string[]
|
policy_ids?: string[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,6 +26,21 @@ export const updateRbacRolesWorkflowId = "update-rbac-roles"
|
|||||||
export const updateRbacRolesWorkflow = createWorkflow(
|
export const updateRbacRolesWorkflow = createWorkflow(
|
||||||
updateRbacRolesWorkflowId,
|
updateRbacRolesWorkflowId,
|
||||||
(input: WorkflowData<UpdateRbacRolesWorkflowInput>) => {
|
(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 }) => ({
|
const roleUpdateData = transform({ input }, ({ input }) => ({
|
||||||
selector: input.selector,
|
selector: input.selector,
|
||||||
update: {
|
update: {
|
||||||
@@ -33,40 +52,40 @@ export const updateRbacRolesWorkflow = createWorkflow(
|
|||||||
|
|
||||||
const updatedRoles = updateRbacRolesStep(roleUpdateData)
|
const updatedRoles = updateRbacRolesStep(roleUpdateData)
|
||||||
|
|
||||||
const inheritanceUpdateData = transform(
|
const parentUpdateData = transform(
|
||||||
{ input, updatedRoles },
|
{ input, updatedRoles },
|
||||||
({ input, updatedRoles }) => {
|
({ input, updatedRoles }) => {
|
||||||
if (!isDefined(input.update.inherited_role_ids)) {
|
if (!isDefined(input.update.parent_ids)) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
return updatedRoles.map((role) => ({
|
return updatedRoles.map((role) => ({
|
||||||
role_id: role.id,
|
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(
|
const policiesUpdateData = transform(
|
||||||
{ input, updatedRoles },
|
{ input, updatedRoles },
|
||||||
({ input, updatedRoles }) => {
|
({ input, updatedRoles }) => {
|
||||||
if (!isDefined(input.update.policy_ids)) {
|
if (!isDefined(input.update.policy_ids)) {
|
||||||
return { role_policies: [] }
|
return { policies: [] }
|
||||||
}
|
}
|
||||||
|
|
||||||
const allPolicies: any[] = []
|
const allPolicies: any[] = []
|
||||||
updatedRoles.forEach((role) => {
|
updatedRoles.forEach((role) => {
|
||||||
const policyIds = input.update.policy_ids || []
|
const policyIds = input.update.policy_ids || []
|
||||||
policyIds.forEach((policy_id) => {
|
policyIds.forEach((policyId) => {
|
||||||
allPolicies.push({
|
allPolicies.push({
|
||||||
role_id: role.id,
|
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 "./links"
|
||||||
export * from "./logger"
|
export * from "./logger"
|
||||||
export * from "./medusa-app-loader"
|
export * from "./medusa-app-loader"
|
||||||
export * from "./subscribers"
|
|
||||||
export * from "./workflows"
|
|
||||||
export * from "./telemetry"
|
|
||||||
export * from "./zod"
|
|
||||||
export * from "./migrations"
|
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")
|
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
|
name: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
metadata?: Record<string, unknown> | null
|
metadata?: Record<string, unknown> | null
|
||||||
|
policies?: RbacPolicyDTO[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FilterableRbacRoleProps = {
|
export type FilterableRbacRoleProps = {
|
||||||
@@ -19,11 +20,12 @@ export type RbacPolicyDTO = {
|
|||||||
name?: string | null
|
name?: string | null
|
||||||
description?: string | null
|
description?: string | null
|
||||||
metadata?: Record<string, unknown> | null
|
metadata?: Record<string, unknown> | null
|
||||||
|
deleted_at?: Date | string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FilterableRbacPolicyProps = {
|
export type FilterableRbacPolicyProps = {
|
||||||
id?: string | string[]
|
id?: string | string[]
|
||||||
key?: string
|
key?: string | string[]
|
||||||
resource?: string
|
resource?: string
|
||||||
operation?: string
|
operation?: string
|
||||||
q?: string
|
q?: string
|
||||||
@@ -32,25 +34,25 @@ export type FilterableRbacPolicyProps = {
|
|||||||
export type RbacRolePolicyDTO = {
|
export type RbacRolePolicyDTO = {
|
||||||
id: string
|
id: string
|
||||||
role_id: string
|
role_id: string
|
||||||
scope_id: string
|
policy_id: string
|
||||||
metadata?: Record<string, unknown> | null
|
metadata?: Record<string, unknown> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FilterableRbacRolePolicyProps = {
|
export type FilterableRbacRolePolicyProps = {
|
||||||
id?: string | string[]
|
id?: string | string[]
|
||||||
role_id?: string | string[]
|
role_id?: string | string[]
|
||||||
scope_id?: string | string[]
|
policy_id?: string | string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RbacRoleInheritanceDTO = {
|
export type RbacRoleParentDTO = {
|
||||||
id: string
|
id: string
|
||||||
role_id: string
|
role_id: string
|
||||||
inherited_role_id: string
|
parent_id: string
|
||||||
metadata?: Record<string, unknown> | null
|
metadata?: Record<string, unknown> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FilterableRbacRoleInheritanceProps = {
|
export type FilterableRbacRoleParentProps = {
|
||||||
id?: string | string[]
|
id?: string | string[]
|
||||||
role_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 = {
|
export type CreateRbacRolePolicyDTO = {
|
||||||
role_id: string
|
role_id: string
|
||||||
scope_id: string
|
policy_id: string
|
||||||
metadata?: Record<string, unknown> | null
|
metadata?: Record<string, unknown> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,13 +31,12 @@ export type UpdateRbacRolePolicyDTO = Partial<CreateRbacRolePolicyDTO> & {
|
|||||||
id: string
|
id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CreateRbacRoleInheritanceDTO = {
|
export type CreateRbacRoleParentDTO = {
|
||||||
role_id: string
|
role_id: string
|
||||||
inherited_role_id: string
|
parent_id: string
|
||||||
metadata?: Record<string, unknown> | null
|
metadata?: Record<string, unknown> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UpdateRbacRoleInheritanceDTO =
|
export type UpdateRbacRoleParentDTO = Partial<CreateRbacRoleParentDTO> & {
|
||||||
Partial<CreateRbacRoleInheritanceDTO> & {
|
id: string
|
||||||
id: string
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
import { FindConfig } from "../common"
|
import { FindConfig } from "../common"
|
||||||
|
import { RestoreReturn, SoftDeleteReturn } from "../dal"
|
||||||
import { IModuleService } from "../modules-sdk"
|
import { IModuleService } from "../modules-sdk"
|
||||||
import { Context } from "../shared-context"
|
import { Context } from "../shared-context"
|
||||||
import {
|
import {
|
||||||
FilterableRbacPolicyProps,
|
FilterableRbacPolicyProps,
|
||||||
FilterableRbacRoleInheritanceProps,
|
FilterableRbacRoleParentProps,
|
||||||
FilterableRbacRolePolicyProps,
|
FilterableRbacRolePolicyProps,
|
||||||
FilterableRbacRoleProps,
|
FilterableRbacRoleProps,
|
||||||
RbacPolicyDTO,
|
RbacPolicyDTO,
|
||||||
RbacRoleDTO,
|
RbacRoleDTO,
|
||||||
RbacRoleInheritanceDTO,
|
RbacRoleParentDTO,
|
||||||
RbacRolePolicyDTO,
|
RbacRolePolicyDTO,
|
||||||
} from "./common"
|
} from "./common"
|
||||||
import {
|
import {
|
||||||
CreateRbacPolicyDTO,
|
CreateRbacPolicyDTO,
|
||||||
CreateRbacRoleDTO,
|
CreateRbacRoleDTO,
|
||||||
CreateRbacRoleInheritanceDTO,
|
CreateRbacRoleParentDTO,
|
||||||
CreateRbacRolePolicyDTO,
|
CreateRbacRolePolicyDTO,
|
||||||
UpdateRbacPolicyDTO,
|
UpdateRbacPolicyDTO,
|
||||||
UpdateRbacRoleDTO,
|
UpdateRbacRoleDTO,
|
||||||
UpdateRbacRoleInheritanceDTO,
|
UpdateRbacRoleParentDTO,
|
||||||
UpdateRbacRolePolicyDTO,
|
UpdateRbacRolePolicyDTO,
|
||||||
} from "./mutations"
|
} from "./mutations"
|
||||||
|
|
||||||
@@ -146,49 +147,90 @@ export interface IRbacModuleService extends IModuleService {
|
|||||||
sharedContext?: Context
|
sharedContext?: Context
|
||||||
): Promise<[RbacRolePolicyDTO[], number]>
|
): Promise<[RbacRolePolicyDTO[], number]>
|
||||||
|
|
||||||
createRbacRoleInheritances(
|
createRbacRoleParents(
|
||||||
data: CreateRbacRoleInheritanceDTO,
|
data: CreateRbacRoleParentDTO,
|
||||||
sharedContext?: Context
|
sharedContext?: Context
|
||||||
): Promise<RbacRoleInheritanceDTO>
|
): Promise<RbacRoleParentDTO>
|
||||||
createRbacRoleInheritances(
|
createRbacRoleParents(
|
||||||
data: CreateRbacRoleInheritanceDTO[],
|
data: CreateRbacRoleParentDTO[],
|
||||||
sharedContext?: Context
|
sharedContext?: Context
|
||||||
): Promise<RbacRoleInheritanceDTO[]>
|
): Promise<RbacRoleParentDTO[]>
|
||||||
|
|
||||||
updateRbacRoleInheritances(
|
updateRbacRoleParents(
|
||||||
data: UpdateRbacRoleInheritanceDTO,
|
data: UpdateRbacRoleParentDTO,
|
||||||
sharedContext?: Context
|
sharedContext?: Context
|
||||||
): Promise<RbacRoleInheritanceDTO>
|
): Promise<RbacRoleParentDTO>
|
||||||
updateRbacRoleInheritances(
|
updateRbacRoleParents(
|
||||||
data: UpdateRbacRoleInheritanceDTO[],
|
data: UpdateRbacRoleParentDTO[],
|
||||||
sharedContext?: Context
|
sharedContext?: Context
|
||||||
): Promise<RbacRoleInheritanceDTO[]>
|
): Promise<RbacRoleParentDTO[]>
|
||||||
|
|
||||||
deleteRbacRoleInheritances(
|
deleteRbacRoleParents(
|
||||||
ids: string | string[],
|
ids: string | string[],
|
||||||
sharedContext?: Context
|
sharedContext?: Context
|
||||||
): Promise<void>
|
): Promise<void>
|
||||||
|
|
||||||
retrieveRbacRoleInheritance(
|
retrieveRbacRoleParent(
|
||||||
id: string,
|
id: string,
|
||||||
config?: FindConfig<RbacRoleInheritanceDTO>,
|
config?: FindConfig<RbacRoleParentDTO>,
|
||||||
sharedContext?: Context
|
sharedContext?: Context
|
||||||
): Promise<RbacRoleInheritanceDTO>
|
): Promise<RbacRoleParentDTO>
|
||||||
|
|
||||||
listRbacRoleInheritances(
|
listRbacRoleParents(
|
||||||
filters?: FilterableRbacRoleInheritanceProps,
|
filters?: FilterableRbacRoleParentProps,
|
||||||
config?: FindConfig<RbacRoleInheritanceDTO>,
|
config?: FindConfig<RbacRoleParentDTO>,
|
||||||
sharedContext?: Context
|
sharedContext?: Context
|
||||||
): Promise<RbacRoleInheritanceDTO[]>
|
): Promise<RbacRoleParentDTO[]>
|
||||||
|
|
||||||
listAndCountRbacRoleInheritances(
|
listAndCountRbacRoleParents(
|
||||||
filters?: FilterableRbacRoleInheritanceProps,
|
filters?: FilterableRbacRoleParentProps,
|
||||||
config?: FindConfig<RbacRoleInheritanceDTO>,
|
config?: FindConfig<RbacRoleParentDTO>,
|
||||||
sharedContext?: Context
|
sharedContext?: Context
|
||||||
): Promise<[RbacRoleInheritanceDTO[], number]>
|
): Promise<[RbacRoleParentDTO[], number]>
|
||||||
|
|
||||||
listPoliciesForRole(
|
listPoliciesForRole(
|
||||||
roleId: string,
|
roleId: string,
|
||||||
sharedContext?: Context
|
sharedContext?: Context
|
||||||
): Promise<RbacPolicyDTO[]>
|
): 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-metadata"
|
||||||
export * from "./merge-plugin-modules"
|
export * from "./merge-plugin-modules"
|
||||||
export * from "./normalize-csv-value"
|
export * from "./normalize-csv-value"
|
||||||
export * from "./normalize-locale"
|
|
||||||
export * from "./normalize-import-path-with-source"
|
export * from "./normalize-import-path-with-source"
|
||||||
|
export * from "./normalize-locale"
|
||||||
export * from "./object-from-string-path"
|
export * from "./object-from-string-path"
|
||||||
export * from "./object-to-string-path"
|
export * from "./object-to-string-path"
|
||||||
export * from "./omit-deep"
|
export * from "./omit-deep"
|
||||||
@@ -85,6 +85,7 @@ export * from "./to-camel-case"
|
|||||||
export * from "./to-handle"
|
export * from "./to-handle"
|
||||||
export * from "./to-kebab-case"
|
export * from "./to-kebab-case"
|
||||||
export * from "./to-pascal-case"
|
export * from "./to-pascal-case"
|
||||||
|
export * from "./to-snake-case"
|
||||||
export * from "./to-unix-slash"
|
export * from "./to-unix-slash"
|
||||||
export * from "./trim-zeros"
|
export * from "./trim-zeros"
|
||||||
export * from "./try-convert-to-boolean"
|
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 "./analytics"
|
||||||
|
export * from "./api-key"
|
||||||
export * from "./auth"
|
export * from "./auth"
|
||||||
export * from "./bundles"
|
export * from "./bundles"
|
||||||
|
export * from "./caching"
|
||||||
export * from "./common"
|
export * from "./common"
|
||||||
export * from "./core-flows"
|
export * from "./core-flows"
|
||||||
export * from "./dal"
|
export * from "./dal"
|
||||||
export * from "./defaults"
|
export * from "./defaults"
|
||||||
|
export * from "./dev-server"
|
||||||
export * from "./dml"
|
export * from "./dml"
|
||||||
export * from "./event-bus"
|
export * from "./event-bus"
|
||||||
export * from "./exceptions"
|
export * from "./exceptions"
|
||||||
@@ -21,6 +23,7 @@ export * from "./orchestration"
|
|||||||
export * from "./order"
|
export * from "./order"
|
||||||
export * from "./payment"
|
export * from "./payment"
|
||||||
export * from "./pg"
|
export * from "./pg"
|
||||||
|
export * from "./policies"
|
||||||
export * from "./pricing"
|
export * from "./pricing"
|
||||||
export * from "./product"
|
export * from "./product"
|
||||||
export * from "./promotion"
|
export * from "./promotion"
|
||||||
@@ -28,10 +31,8 @@ export * from "./search"
|
|||||||
export * from "./shipping"
|
export * from "./shipping"
|
||||||
export * from "./totals"
|
export * from "./totals"
|
||||||
export * from "./totals/big-number"
|
export * from "./totals/big-number"
|
||||||
export * from "./user"
|
|
||||||
export * from "./caching"
|
|
||||||
export * from "./translations"
|
export * from "./translations"
|
||||||
export * from "./dev-server"
|
export * from "./user"
|
||||||
|
|
||||||
export const MedusaModuleType = Symbol.for("MedusaModule")
|
export const MedusaModuleType = Symbol.for("MedusaModule")
|
||||||
export const MedusaModuleProviderType = Symbol.for("MedusaModuleProvider")
|
export const MedusaModuleProviderType = Symbol.for("MedusaModuleProvider")
|
||||||
|
|||||||
@@ -128,4 +128,10 @@ export const LINKS = {
|
|||||||
Modules.PAYMENT,
|
Modules.PAYMENT,
|
||||||
"account_holder_id"
|
"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 "./build-query"
|
||||||
|
export * from "./create-medusa-mikro-orm-event-subscriber"
|
||||||
export * from "./create-pg-connection"
|
export * from "./create-pg-connection"
|
||||||
export * from "./decorators"
|
export * from "./decorators"
|
||||||
export * from "./define-link"
|
export * from "./define-link"
|
||||||
|
export * from "./define-policies"
|
||||||
export * from "./definition"
|
export * from "./definition"
|
||||||
export * from "./event-builder-factory"
|
export * from "./event-builder-factory"
|
||||||
export * from "./joiner-config-builder"
|
export * from "./joiner-config-builder"
|
||||||
@@ -16,9 +18,9 @@ export * from "./migration-scripts"
|
|||||||
export * from "./mikro-orm-cli-config-builder"
|
export * from "./mikro-orm-cli-config-builder"
|
||||||
export * from "./module"
|
export * from "./module"
|
||||||
export * from "./module-provider"
|
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 "./query-context"
|
||||||
export * from "./types/links-config"
|
export * from "./types/links-config"
|
||||||
export * from "./types/medusa-service"
|
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 { MiddlewareRoute } from "@medusajs/framework/http"
|
||||||
|
|
||||||
import { adminRbacPolicyRoutesMiddlewares } from "./policies/middlewares"
|
import { adminRbacPolicyRoutesMiddlewares } from "./policies/middlewares"
|
||||||
import { adminRbacRolePolicyRoutesMiddlewares } from "./role-policies/middlewares"
|
|
||||||
import { adminRbacRoleRoutesMiddlewares } from "./roles/middlewares"
|
import { adminRbacRoleRoutesMiddlewares } from "./roles/middlewares"
|
||||||
|
|
||||||
export const adminRbacRoutesMiddlewares: MiddlewareRoute[] = [
|
export const adminRbacRoutesMiddlewares: MiddlewareRoute[] = [
|
||||||
...adminRbacRoleRoutesMiddlewares,
|
...adminRbacRoleRoutesMiddlewares,
|
||||||
...adminRbacPolicyRoutesMiddlewares,
|
...adminRbacPolicyRoutesMiddlewares,
|
||||||
...adminRbacRolePolicyRoutesMiddlewares,
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ import {
|
|||||||
AuthenticatedMedusaRequest,
|
AuthenticatedMedusaRequest,
|
||||||
MedusaResponse,
|
MedusaResponse,
|
||||||
} from "@medusajs/framework/http"
|
} 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"
|
import { AdminCreateRbacPolicyType } from "./validators"
|
||||||
|
|
||||||
export const GET = async (
|
export const GET = async (
|
||||||
@@ -48,3 +53,7 @@ export const POST = async (
|
|||||||
|
|
||||||
res.status(200).json({ policy })
|
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({
|
const { result } = await updateRbacRolesWorkflow(req.scope).run({
|
||||||
input: {
|
input: {
|
||||||
|
actor_id: req.auth_context.actor_id,
|
||||||
|
actor: req.auth_context.actor_type,
|
||||||
selector: { id: req.params.id },
|
selector: { id: req.params.id },
|
||||||
update: req.validatedBody,
|
update: req.validatedBody,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export {}
|
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
import { MiddlewareRoute } from "@medusajs/framework/http"
|
import { MiddlewareRoute } from "@medusajs/framework/http"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
AdminAddRolePoliciesType,
|
||||||
AdminCreateRbacRole,
|
AdminCreateRbacRole,
|
||||||
AdminGetRbacRoleParams,
|
AdminGetRbacRoleParams,
|
||||||
AdminGetRbacRolesParams,
|
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"],
|
method: ["DELETE"],
|
||||||
matcher: "/admin/rbac/roles/:id",
|
matcher: "/admin/rbac/roles/:id",
|
||||||
|
|||||||
@@ -19,3 +19,24 @@ export const listTransformQueryConfig = {
|
|||||||
defaultLimit: 20,
|
defaultLimit: 20,
|
||||||
isList: true,
|
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,
|
AuthenticatedMedusaRequest,
|
||||||
MedusaResponse,
|
MedusaResponse,
|
||||||
} from "@medusajs/framework/http"
|
} 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"
|
import { AdminCreateRbacRoleType } from "./validators"
|
||||||
|
|
||||||
export const GET = async (
|
export const GET = async (
|
||||||
@@ -34,7 +39,11 @@ export const POST = async (
|
|||||||
const input = [req.validatedBody]
|
const input = [req.validatedBody]
|
||||||
|
|
||||||
const { result } = await createRbacRolesWorkflow(req.scope).run({
|
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)
|
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
|
||||||
@@ -48,3 +57,7 @@ export const POST = async (
|
|||||||
|
|
||||||
res.status(200).json({ role })
|
res.status(200).json({ role })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineFileConfig({
|
||||||
|
isDisabled: () => !FeatureFlag.isFeatureEnabled(RbacFeatureFlag.key),
|
||||||
|
})
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ import {
|
|||||||
} from "../../../utils/validators"
|
} from "../../../utils/validators"
|
||||||
|
|
||||||
export type AdminGetRbacRoleParamsType = z.infer<typeof AdminGetRbacRoleParams>
|
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({
|
export const AdminGetRbacRolesParamsFields = z.object({
|
||||||
q: z.string().optional(),
|
q: z.string().optional(),
|
||||||
@@ -48,3 +52,9 @@ export const AdminUpdateRbacRole = z
|
|||||||
metadata: z.record(z.unknown()).nullish(),
|
metadata: z.record(z.unknown()).nullish(),
|
||||||
})
|
})
|
||||||
.strict()
|
.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 {
|
import {
|
||||||
AuthenticationInput,
|
AuthenticationInput,
|
||||||
ConfigModule,
|
ConfigModule,
|
||||||
@@ -8,7 +9,6 @@ import {
|
|||||||
MedusaError,
|
MedusaError,
|
||||||
Modules,
|
Modules,
|
||||||
} from "@medusajs/framework/utils"
|
} from "@medusajs/framework/utils"
|
||||||
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
||||||
import { generateJwtTokenForAuthIdentity } from "../../../utils/generate-jwt-token"
|
import { generateJwtTokenForAuthIdentity } from "../../../utils/generate-jwt-token"
|
||||||
|
|
||||||
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||||
@@ -35,8 +35,13 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|||||||
if (success && authIdentity) {
|
if (success && authIdentity) {
|
||||||
const { http } = config.projectConfig
|
const { http } = config.projectConfig
|
||||||
|
|
||||||
const token = generateJwtTokenForAuthIdentity(
|
const token = await generateJwtTokenForAuthIdentity(
|
||||||
{ authIdentity, actorType: actor_type, authProvider: auth_provider },
|
{
|
||||||
|
authIdentity,
|
||||||
|
actorType: actor_type,
|
||||||
|
authProvider: auth_provider,
|
||||||
|
container: req.scope,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
secret: http.jwtSecret!,
|
secret: http.jwtSecret!,
|
||||||
expiresIn: http.jwtExpiresIn,
|
expiresIn: http.jwtExpiresIn,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||||
import {
|
import {
|
||||||
AuthenticationInput,
|
AuthenticationInput,
|
||||||
ConfigModule,
|
ConfigModule,
|
||||||
@@ -8,7 +9,6 @@ import {
|
|||||||
MedusaError,
|
MedusaError,
|
||||||
Modules,
|
Modules,
|
||||||
} from "@medusajs/framework/utils"
|
} from "@medusajs/framework/utils"
|
||||||
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
||||||
import { generateJwtTokenForAuthIdentity } from "../../../utils/generate-jwt-token"
|
import { generateJwtTokenForAuthIdentity } from "../../../utils/generate-jwt-token"
|
||||||
|
|
||||||
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
|
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||||
@@ -35,11 +35,12 @@ export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
|
|||||||
if (success && authIdentity) {
|
if (success && authIdentity) {
|
||||||
const { http } = config.projectConfig
|
const { http } = config.projectConfig
|
||||||
|
|
||||||
const token = generateJwtTokenForAuthIdentity(
|
const token = await generateJwtTokenForAuthIdentity(
|
||||||
{
|
{
|
||||||
authIdentity,
|
authIdentity,
|
||||||
actorType: actor_type,
|
actorType: actor_type,
|
||||||
authProvider: auth_provider,
|
authProvider: auth_provider,
|
||||||
|
container: req.scope,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
secret: http.jwtSecret!,
|
secret: http.jwtSecret!,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||||
import {
|
import {
|
||||||
AuthenticationInput,
|
AuthenticationInput,
|
||||||
ConfigModule,
|
ConfigModule,
|
||||||
@@ -8,7 +9,6 @@ import {
|
|||||||
MedusaError,
|
MedusaError,
|
||||||
Modules,
|
Modules,
|
||||||
} from "@medusajs/framework/utils"
|
} from "@medusajs/framework/utils"
|
||||||
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
||||||
import { generateJwtTokenForAuthIdentity } from "../../utils/generate-jwt-token"
|
import { generateJwtTokenForAuthIdentity } from "../../utils/generate-jwt-token"
|
||||||
|
|
||||||
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||||
@@ -39,11 +39,12 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|||||||
if (success && authIdentity) {
|
if (success && authIdentity) {
|
||||||
const { http } = config.projectConfig
|
const { http } = config.projectConfig
|
||||||
|
|
||||||
const token = generateJwtTokenForAuthIdentity(
|
const token = await generateJwtTokenForAuthIdentity(
|
||||||
{
|
{
|
||||||
authIdentity,
|
authIdentity,
|
||||||
actorType: actor_type,
|
actorType: actor_type,
|
||||||
authProvider: auth_provider,
|
authProvider: auth_provider,
|
||||||
|
container: req.scope,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
secret: http.jwtSecret!,
|
secret: http.jwtSecret!,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { IAuthModuleService } from "@medusajs/framework/types"
|
|
||||||
import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils"
|
|
||||||
import {
|
import {
|
||||||
AuthenticatedMedusaRequest,
|
AuthenticatedMedusaRequest,
|
||||||
MedusaResponse,
|
MedusaResponse,
|
||||||
} from "@medusajs/framework/http"
|
} from "@medusajs/framework/http"
|
||||||
|
import { IAuthModuleService } from "@medusajs/framework/types"
|
||||||
|
import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils"
|
||||||
import { generateJwtTokenForAuthIdentity } from "../../utils/generate-jwt-token"
|
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.
|
// 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
|
ContainerRegistrationKeys.CONFIG_MODULE
|
||||||
).projectConfig
|
).projectConfig
|
||||||
|
|
||||||
const token = generateJwtTokenForAuthIdentity(
|
const token = await generateJwtTokenForAuthIdentity(
|
||||||
{ authIdentity, actorType: req.auth_context.actor_type },
|
{
|
||||||
|
authIdentity,
|
||||||
|
actorType: req.auth_context.actor_type,
|
||||||
|
container: req.scope,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
secret: http.jwtSecret!,
|
secret: http.jwtSecret!,
|
||||||
expiresIn: http.jwtExpiresIn,
|
expiresIn: http.jwtExpiresIn,
|
||||||
|
|||||||
@@ -1,19 +1,27 @@
|
|||||||
import {
|
import {
|
||||||
AuthIdentityDTO,
|
AuthIdentityDTO,
|
||||||
|
MedusaContainer,
|
||||||
ProjectConfigOptions,
|
ProjectConfigOptions,
|
||||||
} from "@medusajs/framework/types"
|
} from "@medusajs/framework/types"
|
||||||
import { generateJwtToken } from "@medusajs/framework/utils"
|
import {
|
||||||
|
ContainerRegistrationKeys,
|
||||||
|
FeatureFlag,
|
||||||
|
generateJwtToken,
|
||||||
|
} from "@medusajs/framework/utils"
|
||||||
import { type Secret } from "jsonwebtoken"
|
import { type Secret } from "jsonwebtoken"
|
||||||
|
import RbacFeatureFlag from "../../../feature-flags/rbac"
|
||||||
|
|
||||||
export function generateJwtTokenForAuthIdentity(
|
export async function generateJwtTokenForAuthIdentity(
|
||||||
{
|
{
|
||||||
authIdentity,
|
authIdentity,
|
||||||
actorType,
|
actorType,
|
||||||
authProvider,
|
authProvider,
|
||||||
|
container,
|
||||||
}: {
|
}: {
|
||||||
authIdentity: AuthIdentityDTO
|
authIdentity: AuthIdentityDTO
|
||||||
actorType: string
|
actorType: string
|
||||||
authProvider?: string
|
authProvider?: string
|
||||||
|
container?: MedusaContainer
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
secret,
|
secret,
|
||||||
@@ -37,6 +45,29 @@ export function generateJwtTokenForAuthIdentity(
|
|||||||
(identity) => identity.provider === authProvider
|
(identity) => identity.provider === authProvider
|
||||||
)[0]
|
)[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(
|
return generateJwtToken(
|
||||||
{
|
{
|
||||||
actor_id: entityId ?? "",
|
actor_id: entityId ?? "",
|
||||||
@@ -44,6 +75,7 @@ export function generateJwtTokenForAuthIdentity(
|
|||||||
auth_identity_id: authIdentity?.id ?? "",
|
auth_identity_id: authIdentity?.id ?? "",
|
||||||
app_metadata: {
|
app_metadata: {
|
||||||
[entityIdKey]: entityId,
|
[entityIdKey]: entityId,
|
||||||
|
roles,
|
||||||
},
|
},
|
||||||
user_metadata: providerIdentity?.user_metadata ?? {},
|
user_metadata: providerIdentity?.user_metadata ?? {},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ import {
|
|||||||
dynamicImport,
|
dynamicImport,
|
||||||
FileSystem,
|
FileSystem,
|
||||||
generateContainerTypes,
|
generateContainerTypes,
|
||||||
|
generatePolicyTypes,
|
||||||
gqlSchemaToTypes,
|
gqlSchemaToTypes,
|
||||||
GracefulShutdownServer,
|
GracefulShutdownServer,
|
||||||
isFileSkipped,
|
isFileSkipped,
|
||||||
isPresent,
|
isPresent,
|
||||||
|
promiseAll,
|
||||||
} from "@medusajs/framework/utils"
|
} from "@medusajs/framework/utils"
|
||||||
|
|
||||||
import { MedusaModule } from "@medusajs/framework/modules-sdk"
|
import { MedusaModule } from "@medusajs/framework/modules-sdk"
|
||||||
@@ -274,27 +276,35 @@ async function start(args: {
|
|||||||
if (generateTypes) {
|
if (generateTypes) {
|
||||||
const typesDirectory = path.join(directory, ".medusa/types")
|
const typesDirectory = path.join(directory, ".medusa/types")
|
||||||
|
|
||||||
/**
|
const fileGenPromises: Promise<void>[] = []
|
||||||
* Cleanup existing types directory before creating new artifacts
|
|
||||||
*/
|
|
||||||
await new FileSystem(typesDirectory).cleanup({ recursive: true })
|
|
||||||
|
|
||||||
await generateContainerTypes(modules, {
|
fileGenPromises.push(
|
||||||
outputDir: typesDirectory,
|
generateContainerTypes(modules, {
|
||||||
interfaceName: "ModuleImplementations",
|
outputDir: typesDirectory,
|
||||||
})
|
interfaceName: "ModuleImplementations",
|
||||||
logger.debug("Generated container types")
|
})
|
||||||
|
)
|
||||||
|
|
||||||
if (gqlSchema) {
|
if (gqlSchema) {
|
||||||
await gqlSchemaToTypes({
|
fileGenPromises.push(
|
||||||
outputDir: typesDirectory,
|
gqlSchemaToTypes({
|
||||||
filename: "query-entry-points",
|
outputDir: typesDirectory,
|
||||||
interfaceName: "RemoteQueryEntryPoints",
|
filename: "query-entry-points",
|
||||||
schema: gqlSchema,
|
interfaceName: "RemoteQueryEntryPoints",
|
||||||
joinerConfigs: MedusaModule.getAllJoinerConfigs(),
|
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.
|
// 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 { configLoader } from "@medusajs/framework/config"
|
||||||
import { pgConnectionLoader } from "@medusajs/framework/database"
|
import { pgConnectionLoader } from "@medusajs/framework/database"
|
||||||
import { featureFlagsLoader } from "@medusajs/framework/feature-flags"
|
import { featureFlagsLoader } from "@medusajs/framework/feature-flags"
|
||||||
@@ -22,7 +23,6 @@ import {
|
|||||||
validateModuleName,
|
validateModuleName,
|
||||||
} from "@medusajs/framework/utils"
|
} from "@medusajs/framework/utils"
|
||||||
import { WorkflowLoader } from "@medusajs/framework/workflows"
|
import { WorkflowLoader } from "@medusajs/framework/workflows"
|
||||||
import { asValue } from "@medusajs/framework/awilix"
|
|
||||||
import { Express, NextFunction, Request, Response } from "express"
|
import { Express, NextFunction, Request, Response } from "express"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
import requestIp from "request-ip"
|
import requestIp from "request-ip"
|
||||||
@@ -182,6 +182,12 @@ export default async ({
|
|||||||
)
|
)
|
||||||
await new LinkLoader(linksSourcePaths, logger).load()
|
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 {
|
const {
|
||||||
onApplicationStart,
|
onApplicationStart,
|
||||||
onApplicationShutdown,
|
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-payment-collection"
|
||||||
export * from "./cart-promotion"
|
export * from "./cart-promotion"
|
||||||
|
export * from "./customer-account-holder"
|
||||||
export * from "./fulfillment-provider-location"
|
export * from "./fulfillment-provider-location"
|
||||||
export * from "./fulfillment-set-location"
|
export * from "./fulfillment-set-location"
|
||||||
export * from "./order-cart"
|
export * from "./order-cart"
|
||||||
@@ -8,6 +9,7 @@ export * from "./order-payment-collection"
|
|||||||
export * from "./order-promotion"
|
export * from "./order-promotion"
|
||||||
export * from "./order-return-fulfillment"
|
export * from "./order-return-fulfillment"
|
||||||
export * from "./product-sales-channel"
|
export * from "./product-sales-channel"
|
||||||
|
export * from "./product-shipping-profile"
|
||||||
export * from "./product-variant-inventory-item"
|
export * from "./product-variant-inventory-item"
|
||||||
export * from "./product-variant-price-set"
|
export * from "./product-variant-price-set"
|
||||||
export * from "./publishable-api-key-sales-channel"
|
export * from "./publishable-api-key-sales-channel"
|
||||||
@@ -15,5 +17,4 @@ export * from "./readonly"
|
|||||||
export * from "./region-payment-provider"
|
export * from "./region-payment-provider"
|
||||||
export * from "./sales-channel-location"
|
export * from "./sales-channel-location"
|
||||||
export * from "./shipping-option-price-set"
|
export * from "./shipping-option-price-set"
|
||||||
export * from "./product-shipping-profile"
|
export * from "./user-rbac-role"
|
||||||
export * from "./customer-account-holder"
|
|
||||||
|
|||||||
@@ -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"
|
import { RbacModuleService } from "@services"
|
||||||
|
|
||||||
export default Module("rbac", {
|
export default Module(Modules.RBAC, {
|
||||||
service: RbacModuleService,
|
service: RbacModuleService,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
{
|
{
|
||||||
"namespaces": [
|
"namespaces": ["public"],
|
||||||
"public"
|
|
||||||
],
|
|
||||||
"name": "public",
|
"name": "public",
|
||||||
"tables": [
|
"tables": [
|
||||||
{
|
{
|
||||||
@@ -143,9 +141,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"keyName": "rbac_policy_pkey",
|
"keyName": "rbac_policy_pkey",
|
||||||
"columnNames": [
|
"columnNames": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"constraint": true,
|
"constraint": true,
|
||||||
"primary": true,
|
"primary": true,
|
||||||
@@ -250,9 +246,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"keyName": "rbac_role_pkey",
|
"keyName": "rbac_role_pkey",
|
||||||
"columnNames": [
|
"columnNames": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"constraint": true,
|
"constraint": true,
|
||||||
"primary": true,
|
"primary": true,
|
||||||
@@ -283,8 +277,8 @@
|
|||||||
"nullable": false,
|
"nullable": false,
|
||||||
"mappedType": "text"
|
"mappedType": "text"
|
||||||
},
|
},
|
||||||
"inherited_role_id": {
|
"parent_id": {
|
||||||
"name": "inherited_role_id",
|
"name": "parent_id",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"unsigned": false,
|
"unsigned": false,
|
||||||
"autoincrement": false,
|
"autoincrement": false,
|
||||||
@@ -334,50 +328,48 @@
|
|||||||
"mappedType": "datetime"
|
"mappedType": "datetime"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "rbac_role_inheritance",
|
"name": "rbac_role_parent",
|
||||||
"schema": "public",
|
"schema": "public",
|
||||||
"indexes": [
|
"indexes": [
|
||||||
{
|
{
|
||||||
"keyName": "IDX_rbac_role_inheritance_role_id",
|
"keyName": "IDX_rbac_role_parent_role_id",
|
||||||
"columnNames": [],
|
"columnNames": [],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"constraint": false,
|
"constraint": false,
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"unique": 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": [],
|
"columnNames": [],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"constraint": false,
|
"constraint": false,
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"unique": 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": [],
|
"columnNames": [],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"constraint": false,
|
"constraint": false,
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"unique": 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": [],
|
"columnNames": [],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"constraint": false,
|
"constraint": false,
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"unique": 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",
|
"keyName": "rbac_role_parent_pkey",
|
||||||
"columnNames": [
|
"columnNames": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"constraint": true,
|
"constraint": true,
|
||||||
"primary": true,
|
"primary": true,
|
||||||
@@ -386,27 +378,19 @@
|
|||||||
],
|
],
|
||||||
"checks": [],
|
"checks": [],
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"rbac_role_inheritance_role_id_foreign": {
|
"rbac_role_parent_role_id_foreign": {
|
||||||
"constraintName": "rbac_role_inheritance_role_id_foreign",
|
"constraintName": "rbac_role_parent_role_id_foreign",
|
||||||
"columnNames": [
|
"columnNames": ["role_id"],
|
||||||
"role_id"
|
"localTableName": "public.rbac_role_parent",
|
||||||
],
|
"referencedColumnNames": ["id"],
|
||||||
"localTableName": "public.rbac_role_inheritance",
|
|
||||||
"referencedColumnNames": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"referencedTableName": "public.rbac_role",
|
"referencedTableName": "public.rbac_role",
|
||||||
"updateRule": "cascade"
|
"updateRule": "cascade"
|
||||||
},
|
},
|
||||||
"rbac_role_inheritance_inherited_role_id_foreign": {
|
"rbac_role_parent_parent_id_foreign": {
|
||||||
"constraintName": "rbac_role_inheritance_inherited_role_id_foreign",
|
"constraintName": "rbac_role_parent_parent_id_foreign",
|
||||||
"columnNames": [
|
"columnNames": ["parent_id"],
|
||||||
"inherited_role_id"
|
"localTableName": "public.rbac_role_parent",
|
||||||
],
|
"referencedColumnNames": ["id"],
|
||||||
"localTableName": "public.rbac_role_inheritance",
|
|
||||||
"referencedColumnNames": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"referencedTableName": "public.rbac_role",
|
"referencedTableName": "public.rbac_role",
|
||||||
"updateRule": "cascade"
|
"updateRule": "cascade"
|
||||||
}
|
}
|
||||||
@@ -433,8 +417,8 @@
|
|||||||
"nullable": false,
|
"nullable": false,
|
||||||
"mappedType": "text"
|
"mappedType": "text"
|
||||||
},
|
},
|
||||||
"scope_id": {
|
"policy_id": {
|
||||||
"name": "scope_id",
|
"name": "policy_id",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"unsigned": false,
|
"unsigned": false,
|
||||||
"autoincrement": 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"
|
"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": [],
|
"columnNames": [],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"constraint": false,
|
"constraint": false,
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"unique": 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",
|
"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"
|
"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": [],
|
"columnNames": [],
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"constraint": false,
|
"constraint": false,
|
||||||
"primary": false,
|
"primary": false,
|
||||||
"unique": 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",
|
"keyName": "rbac_role_policy_pkey",
|
||||||
"columnNames": [
|
"columnNames": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"constraint": true,
|
"constraint": true,
|
||||||
"primary": true,
|
"primary": true,
|
||||||
@@ -538,25 +520,17 @@
|
|||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"rbac_role_policy_role_id_foreign": {
|
"rbac_role_policy_role_id_foreign": {
|
||||||
"constraintName": "rbac_role_policy_role_id_foreign",
|
"constraintName": "rbac_role_policy_role_id_foreign",
|
||||||
"columnNames": [
|
"columnNames": ["role_id"],
|
||||||
"role_id"
|
|
||||||
],
|
|
||||||
"localTableName": "public.rbac_role_policy",
|
"localTableName": "public.rbac_role_policy",
|
||||||
"referencedColumnNames": [
|
"referencedColumnNames": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"referencedTableName": "public.rbac_role",
|
"referencedTableName": "public.rbac_role",
|
||||||
"updateRule": "cascade"
|
"updateRule": "cascade"
|
||||||
},
|
},
|
||||||
"rbac_role_policy_scope_id_foreign": {
|
"rbac_role_policy_policy_id_foreign": {
|
||||||
"constraintName": "rbac_role_policy_scope_id_foreign",
|
"constraintName": "rbac_role_policy_policy_id_foreign",
|
||||||
"columnNames": [
|
"columnNames": ["policy_id"],
|
||||||
"scope_id"
|
|
||||||
],
|
|
||||||
"localTableName": "public.rbac_role_policy",
|
"localTableName": "public.rbac_role_policy",
|
||||||
"referencedColumnNames": [
|
"referencedColumnNames": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"referencedTableName": "public.rbac_policy",
|
"referencedTableName": "public.rbac_policy",
|
||||||
"updateRule": "cascade"
|
"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> {
|
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_policy" drop constraint if exists "rbac_role_policy_role_id_policy_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_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_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(`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"));`);
|
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 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 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 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_inheritance_role_id" ON "rbac_role_inheritance" ("role_id") WHERE deleted_at IS NULL;`);
|
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_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_parent_parent_id" ON "rbac_role_parent" ("parent_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 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_inheritance_role_id_inherited_role_id_unique" ON "rbac_role_inheritance" ("role_id", "inherited_role_id") 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_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 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_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_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_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_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 RbacPolicy } from "./rbac-policy"
|
||||||
export { default as RbacRole } from "./rbac-role"
|
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"
|
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", {
|
.define("rbac_role_policy", {
|
||||||
id: model.id({ prefix: "rlpl" }).primaryKey(),
|
id: model.id({ prefix: "rlpl" }).primaryKey(),
|
||||||
role: model.belongsTo(() => RbacRole),
|
role: model.belongsTo(() => RbacRole),
|
||||||
scope: model.belongsTo(() => RbacPolicy),
|
policy: model.belongsTo(() => RbacPolicy),
|
||||||
metadata: model.json().nullable(),
|
metadata: model.json().nullable(),
|
||||||
})
|
})
|
||||||
.indexes([
|
.indexes([
|
||||||
@@ -15,11 +15,11 @@ const RbacRolePolicy = model
|
|||||||
where: "deleted_at IS NULL",
|
where: "deleted_at IS NULL",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
on: ["scope_id"],
|
on: ["policy_id"],
|
||||||
where: "deleted_at IS NULL",
|
where: "deleted_at IS NULL",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
on: ["role_id", "scope_id"],
|
on: ["role_id", "policy_id"],
|
||||||
unique: true,
|
unique: true,
|
||||||
where: "deleted_at IS NULL",
|
where: "deleted_at IS NULL",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { model } from "@medusajs/framework/utils"
|
import { model } from "@medusajs/framework/utils"
|
||||||
|
import RbacRoleParent from "./rbac-role-parent"
|
||||||
|
import RbacRolePolicy from "./rbac-role-policy"
|
||||||
|
|
||||||
const RbacRole = model
|
const RbacRole = model
|
||||||
.define("rbac_role", {
|
.define("rbac_role", {
|
||||||
@@ -6,6 +8,12 @@ const RbacRole = model
|
|||||||
name: model.text().searchable(),
|
name: model.text().searchable(),
|
||||||
description: model.text().nullable(),
|
description: model.text().nullable(),
|
||||||
metadata: model.json().nullable(),
|
metadata: model.json().nullable(),
|
||||||
|
policies: model.hasMany(() => RbacRolePolicy, {
|
||||||
|
mappedBy: "role",
|
||||||
|
}),
|
||||||
|
parents: model.hasMany(() => RbacRoleParent, {
|
||||||
|
mappedBy: "role",
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
.indexes([
|
.indexes([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -35,17 +35,19 @@ export class RbacRepository extends MikroOrmBase {
|
|||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
WITH RECURSIVE role_hierarchy AS (
|
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
|
FROM rbac_role
|
||||||
WHERE id IN (${placeholders}) AND deleted_at IS NULL
|
WHERE id IN (${placeholders}) AND deleted_at IS NULL
|
||||||
|
|
||||||
UNION ALL
|
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
|
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
|
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
|
SELECT DISTINCT
|
||||||
rh.original_role_id,
|
rh.original_role_id,
|
||||||
@@ -60,7 +62,7 @@ export class RbacRepository extends MikroOrmBase {
|
|||||||
p.updated_at,
|
p.updated_at,
|
||||||
CASE WHEN rp.role_id = rh.original_role_id THEN NULL ELSE rp.role_id END as inherited_from_role_id
|
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
|
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
|
INNER JOIN role_hierarchy rh ON rh.id = rp.role_id
|
||||||
WHERE p.deleted_at IS NULL AND rp.deleted_at IS NULL
|
WHERE p.deleted_at IS NULL AND rp.deleted_at IS NULL
|
||||||
ORDER BY rh.original_role_id, p.resource, p.operation, p.key
|
ORDER BY rh.original_role_id, p.resource, p.operation, p.key
|
||||||
@@ -85,4 +87,40 @@ export class RbacRepository extends MikroOrmBase {
|
|||||||
|
|
||||||
return policiesByRole
|
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 {
|
import {
|
||||||
InjectManager,
|
InjectManager,
|
||||||
MedusaContext,
|
MedusaContext,
|
||||||
MedusaService,
|
MedusaService,
|
||||||
|
Policy,
|
||||||
|
promiseAll,
|
||||||
} from "@medusajs/framework/utils"
|
} from "@medusajs/framework/utils"
|
||||||
import {
|
import {
|
||||||
RbacPolicy,
|
CreateRbacRoleParentDTO,
|
||||||
RbacRole,
|
IRbacModuleService,
|
||||||
RbacRoleInheritance,
|
RbacRoleParentDTO,
|
||||||
RbacRolePolicy,
|
UpdateRbacRoleParentDTO,
|
||||||
} from "@models"
|
} from "@medusajs/types"
|
||||||
|
import { RbacPolicy, RbacRole, RbacRoleParent, RbacRolePolicy } from "@models"
|
||||||
import { RbacRepository } from "../repositories"
|
import { RbacRepository } from "../repositories"
|
||||||
|
|
||||||
type InjectedDependencies = {
|
type InjectedDependencies = {
|
||||||
rbacRepository: RbacRepository
|
rbacRepository: RbacRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class RbacModuleService extends MedusaService<{
|
export default class RbacModuleService
|
||||||
RbacRole: { dto: any }
|
extends MedusaService({
|
||||||
RbacPolicy: { dto: any }
|
RbacRole,
|
||||||
RbacRoleInheritance: { dto: any }
|
RbacPolicy,
|
||||||
RbacRolePolicy: { dto: any }
|
RbacRoleParent,
|
||||||
}>({
|
RbacRolePolicy,
|
||||||
RbacRole,
|
})
|
||||||
RbacPolicy,
|
implements IRbacModuleService
|
||||||
RbacRoleInheritance,
|
{
|
||||||
RbacRolePolicy,
|
|
||||||
}) {
|
|
||||||
protected readonly rbacRepository_: RbacRepository
|
protected readonly rbacRepository_: RbacRepository
|
||||||
|
|
||||||
constructor({ rbacRepository }: InjectedDependencies) {
|
constructor({ rbacRepository }: InjectedDependencies) {
|
||||||
@@ -35,6 +41,93 @@ export default class RbacModuleService extends MedusaService<{
|
|||||||
this.rbacRepository_ = rbacRepository
|
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()
|
@InjectManager()
|
||||||
async listPoliciesForRole(
|
async listPoliciesForRole(
|
||||||
roleId: string,
|
roleId: string,
|
||||||
@@ -46,41 +139,13 @@ export default class RbacModuleService extends MedusaService<{
|
|||||||
@InjectManager()
|
@InjectManager()
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
async listRbacRoles(
|
async listRbacRoles(
|
||||||
filters: any = {},
|
filters: FilterableRbacRoleProps = {},
|
||||||
config: FindConfig<any> = {},
|
config: FindConfig<RbacRoleDTO> = {},
|
||||||
@MedusaContext() sharedContext: Context = {}
|
@MedusaContext() sharedContext: Context = {}
|
||||||
): Promise<any[]> {
|
): Promise<RbacRoleDTO[]> {
|
||||||
const roles = await super.listRbacRoles(filters, config, sharedContext)
|
const roles = await super.listRbacRoles(
|
||||||
|
|
||||||
const shouldIncludePolicies =
|
|
||||||
config.relations?.includes("policies") ||
|
|
||||||
config.select?.includes("policies")
|
|
||||||
|
|
||||||
if (shouldIncludePolicies && roles.length > 0) {
|
|
||||||
const roleIds = roles.map((role) => role.id)
|
|
||||||
const policiesByRole = await this.rbacRepository_.listPoliciesForRoles(
|
|
||||||
roleIds,
|
|
||||||
sharedContext
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const role of roles) {
|
|
||||||
role.policies = policiesByRole.get(role.id) || []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return roles
|
|
||||||
}
|
|
||||||
|
|
||||||
@InjectManager()
|
|
||||||
// @ts-expect-error
|
|
||||||
async listAndCountRbacRoles(
|
|
||||||
filters: any = {},
|
|
||||||
config: FindConfig<any> = {},
|
|
||||||
@MedusaContext() sharedContext: Context = {}
|
|
||||||
): Promise<[any[], number]> {
|
|
||||||
const [roles, count] = await super.listAndCountRbacRoles(
|
|
||||||
filters,
|
filters,
|
||||||
config,
|
config as any,
|
||||||
sharedContext
|
sharedContext
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -100,6 +165,102 @@ export default class RbacModuleService extends MedusaService<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [roles, count]
|
return roles as unknown as RbacRoleDTO[]
|
||||||
|
}
|
||||||
|
|
||||||
|
@InjectManager()
|
||||||
|
// @ts-expect-error
|
||||||
|
async listAndCountRbacRoles(
|
||||||
|
filters: FilterableRbacRoleProps = {},
|
||||||
|
config: FindConfig<RbacRoleDTO> = {},
|
||||||
|
@MedusaContext() sharedContext: Context = {}
|
||||||
|
): Promise<[RbacRoleDTO[], number]> {
|
||||||
|
const [roles, count] = await super.listAndCountRbacRoles(
|
||||||
|
filters,
|
||||||
|
config as any,
|
||||||
|
sharedContext
|
||||||
|
)
|
||||||
|
|
||||||
|
const shouldIncludePolicies =
|
||||||
|
config.relations?.includes("policies") ||
|
||||||
|
config.select?.includes("policies")
|
||||||
|
|
||||||
|
if (shouldIncludePolicies && roles.length > 0) {
|
||||||
|
const roleIds = roles.map((role) => role.id)
|
||||||
|
const policiesByRole = await this.rbacRepository_.listPoliciesForRoles(
|
||||||
|
roleIds,
|
||||||
|
sharedContext
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const role of roles) {
|
||||||
|
role.policies = policiesByRole.get(role.id) || []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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