feat(rbac): role-based access control module (#14310)

This commit is contained in:
Carlos R. L. Rodrigues
2026-01-07 05:36:39 -03:00
committed by GitHub
parent d6d7d14a6a
commit 1bfde8dc57
74 changed files with 4186 additions and 3 deletions

View File

@@ -0,0 +1,285 @@
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import {
adminHeaders,
createAdminUser,
} from "../../../../helpers/create-admin-user"
jest.setTimeout(60000)
medusaIntegrationTestRunner({
testSuite: ({ dbConnection, api, getContainer }) => {
let container
beforeEach(async () => {
container = getContainer()
await createAdminUser(dbConnection, adminHeaders, container)
})
describe("RBAC Policies - Admin API", () => {
describe("POST /admin/rbac/policies", () => {
it("should create a policy", async () => {
const response = await api.post(
"/admin/rbac/policies",
{
key: "read:products",
resource: "product",
operation: "read",
name: "Read Products",
description: "Permission to read products",
},
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.policy).toEqual(
expect.objectContaining({
id: expect.any(String),
key: "read:products",
resource: "product",
operation: "read",
name: "Read Products",
description: "Permission to read products",
})
)
})
it("should create a policy with metadata", async () => {
const response = await api.post(
"/admin/rbac/policies",
{
key: "write:orders",
resource: "order",
operation: "write",
name: "Write Orders",
metadata: { category: "order_management" },
},
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.policy).toEqual(
expect.objectContaining({
key: "write:orders",
resource: "order",
operation: "write",
metadata: { category: "order_management" },
})
)
})
})
describe("GET /admin/rbac/policies", () => {
beforeEach(async () => {
await api.post(
"/admin/rbac/policies",
{
key: "read:products",
resource: "product",
operation: "read",
name: "Read Products",
},
adminHeaders
)
await api.post(
"/admin/rbac/policies",
{
key: "write:products",
resource: "product",
operation: "write",
name: "Write Products",
},
adminHeaders
)
await api.post(
"/admin/rbac/policies",
{
key: "read:orders",
resource: "order",
operation: "read",
name: "Read Orders",
},
adminHeaders
)
})
it("should list all policies", async () => {
const response = await api.get("/admin/rbac/policies", adminHeaders)
expect(response.status).toEqual(200)
expect(response.data.count).toEqual(3)
expect(response.data.policies).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: "read:products",
resource: "product",
operation: "read",
}),
expect.objectContaining({
key: "write:products",
resource: "product",
operation: "write",
}),
expect.objectContaining({
key: "read:orders",
resource: "order",
operation: "read",
}),
])
)
})
it("should filter policies by resource", async () => {
const response = await api.get(
"/admin/rbac/policies?resource=product",
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.count).toEqual(2)
expect(response.data.policies).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: "read:products",
resource: "product",
}),
expect.objectContaining({
key: "write:products",
resource: "product",
}),
])
)
})
it("should filter policies by operation", async () => {
const response = await api.get(
"/admin/rbac/policies?operation=read",
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.count).toEqual(2)
expect(response.data.policies).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: "read:products",
operation: "read",
}),
expect.objectContaining({
key: "read:orders",
operation: "read",
}),
])
)
})
})
describe("GET /admin/rbac/policies/:id", () => {
it("should retrieve a policy by id", async () => {
const createResponse = await api.post(
"/admin/rbac/policies",
{
key: "delete:users",
resource: "user",
operation: "delete",
name: "Delete Users",
},
adminHeaders
)
const policyId = createResponse.data.policy.id
const response = await api.get(
`/admin/rbac/policies/${policyId}`,
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.policy).toEqual(
expect.objectContaining({
id: policyId,
key: "delete:users",
resource: "user",
operation: "delete",
name: "Delete Users",
})
)
})
})
describe("POST /admin/rbac/policies/:id", () => {
it("should update a policy", async () => {
const createResponse = await api.post(
"/admin/rbac/policies",
{
key: "admin:system",
resource: "system",
operation: "admin",
name: "System Admin",
},
adminHeaders
)
const policyId = createResponse.data.policy.id
const response = await api.post(
`/admin/rbac/policies/${policyId}`,
{
name: "System Administrator",
description: "Full system access",
},
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.policy).toEqual(
expect.objectContaining({
id: policyId,
key: "admin:system",
name: "System Administrator",
description: "Full system access",
})
)
})
})
describe("DELETE /admin/rbac/policies/:id", () => {
it("should delete a policy", async () => {
const createResponse = await api.post(
"/admin/rbac/policies",
{
key: "test:delete",
resource: "test",
operation: "delete",
name: "Test Delete",
},
adminHeaders
)
const policyId = createResponse.data.policy.id
const deleteResponse = await api.delete(
`/admin/rbac/policies/${policyId}`,
adminHeaders
)
expect(deleteResponse.status).toEqual(200)
expect(deleteResponse.data).toEqual({
id: policyId,
object: "rbac_policy",
deleted: true,
})
const listResponse = await api.get(
"/admin/rbac/policies",
adminHeaders
)
expect(
listResponse.data.policies.find((p) => p.id === policyId)
).toBeUndefined()
})
})
})
},
})

View File

@@ -0,0 +1,384 @@
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import {
adminHeaders,
createAdminUser,
} from "../../../../helpers/create-admin-user"
jest.setTimeout(60000)
medusaIntegrationTestRunner({
testSuite: ({ dbConnection, api, getContainer }) => {
let container
beforeEach(async () => {
container = getContainer()
await createAdminUser(dbConnection, adminHeaders, container)
})
describe("RBAC Roles - Admin API", () => {
describe("POST /admin/rbac/roles", () => {
it("should create a role", async () => {
const response = await api.post(
"/admin/rbac/roles",
{
name: "Viewer",
description: "Can view resources",
},
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.role).toEqual(
expect.objectContaining({
id: expect.any(String),
name: "Viewer",
description: "Can view resources",
})
)
})
it("should create a role with metadata", async () => {
const response = await api.post(
"/admin/rbac/roles",
{
name: "Editor",
description: "Can edit resources",
metadata: { department: "sales" },
},
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.role).toEqual(
expect.objectContaining({
name: "Editor",
metadata: { department: "sales" },
})
)
})
})
describe("GET /admin/rbac/roles", () => {
beforeEach(async () => {
await api.post(
"/admin/rbac/roles",
{
name: "Viewer",
description: "Can view resources",
},
adminHeaders
)
await api.post(
"/admin/rbac/roles",
{
name: "Editor",
description: "Can edit resources",
},
adminHeaders
)
await api.post(
"/admin/rbac/roles",
{
name: "Admin",
description: "Full access",
},
adminHeaders
)
})
it("should list all roles", async () => {
const response = await api.get("/admin/rbac/roles", adminHeaders)
expect(response.status).toEqual(200)
expect(response.data.count).toEqual(3)
expect(response.data.roles).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: "Viewer",
description: "Can view resources",
}),
expect.objectContaining({
name: "Editor",
description: "Can edit resources",
}),
expect.objectContaining({
name: "Admin",
description: "Full access",
}),
])
)
})
it("should filter roles by name", async () => {
const response = await api.get(
"/admin/rbac/roles?name=Viewer",
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.count).toEqual(1)
expect(response.data.roles[0]).toEqual(
expect.objectContaining({
name: "Viewer",
})
)
})
})
describe("GET /admin/rbac/roles/:id", () => {
it("should retrieve a role by id", async () => {
const createResponse = await api.post(
"/admin/rbac/roles",
{
name: "Manager",
description: "Can manage resources",
},
adminHeaders
)
const roleId = createResponse.data.role.id
const response = await api.get(
`/admin/rbac/roles/${roleId}`,
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.role).toEqual(
expect.objectContaining({
id: roleId,
name: "Manager",
description: "Can manage resources",
})
)
})
})
describe("POST /admin/rbac/roles/:id", () => {
it("should update a role", async () => {
const createResponse = await api.post(
"/admin/rbac/roles",
{
name: "Support",
description: "Support team",
},
adminHeaders
)
const roleId = createResponse.data.role.id
const response = await api.post(
`/admin/rbac/roles/${roleId}`,
{
name: "Customer Support",
description: "Customer support team with limited access",
},
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.role).toEqual(
expect.objectContaining({
id: roleId,
name: "Customer Support",
description: "Customer support team with limited access",
})
)
})
})
describe("DELETE /admin/rbac/roles/:id", () => {
it("should delete a role", async () => {
const createResponse = await api.post(
"/admin/rbac/roles",
{
name: "Temporary",
description: "Temporary role",
},
adminHeaders
)
const roleId = createResponse.data.role.id
const deleteResponse = await api.delete(
`/admin/rbac/roles/${roleId}`,
adminHeaders
)
expect(deleteResponse.status).toEqual(200)
expect(deleteResponse.data).toEqual({
id: roleId,
object: "rbac_role",
deleted: true,
})
const listResponse = await api.get("/admin/rbac/roles", adminHeaders)
expect(
listResponse.data.roles.find((r) => r.id === roleId)
).toBeUndefined()
})
})
describe("Role Policies", () => {
let policies
let viewerRole
let editorRole
beforeEach(async () => {
const policy1 = await api.post(
"/admin/rbac/policies",
{
key: "read:products",
resource: "product",
operation: "read",
name: "Read Products",
},
adminHeaders
)
const policy2 = await api.post(
"/admin/rbac/policies",
{
key: "write:products",
resource: "product",
operation: "write",
name: "Write Products",
},
adminHeaders
)
const policy3 = await api.post(
"/admin/rbac/policies",
{
key: "delete:products",
resource: "product",
operation: "delete",
name: "Delete Products",
},
adminHeaders
)
policies = [
policy1.data.policy,
policy2.data.policy,
policy3.data.policy,
]
const viewer = await api.post(
"/admin/rbac/roles",
{
name: "Product Viewer",
description: "Can view products",
},
adminHeaders
)
viewerRole = viewer.data.role
const editor = await api.post(
"/admin/rbac/roles",
{
name: "Product Editor",
description: "Can edit products",
},
adminHeaders
)
editorRole = editor.data.role
})
it("should create role-policy associations", async () => {
const response = await api.post(
"/admin/rbac/role-policies",
{
role_id: viewerRole.id,
scope_id: policies[0].id,
},
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.role_policy).toEqual(
expect.objectContaining({
role_id: viewerRole.id,
scope_id: policies[0].id,
})
)
})
it("should list role-policies for a specific role", async () => {
await api.post(
"/admin/rbac/role-policies",
{
role_id: viewerRole.id,
scope_id: policies[0].id,
},
adminHeaders
)
await api.post(
"/admin/rbac/role-policies",
{
role_id: viewerRole.id,
scope_id: policies[1].id,
},
adminHeaders
)
const response = await api.get(
`/admin/rbac/role-policies?role_id=${viewerRole.id}`,
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.count).toEqual(2)
expect(response.data.role_policies).toEqual(
expect.arrayContaining([
expect.objectContaining({
role_id: viewerRole.id,
scope_id: policies[0].id,
}),
expect.objectContaining({
role_id: viewerRole.id,
scope_id: policies[1].id,
}),
])
)
})
it("should delete a role-policy association", async () => {
const createResponse = await api.post(
"/admin/rbac/role-policies",
{
role_id: editorRole.id,
scope_id: policies[2].id,
},
adminHeaders
)
const rolePolicyId = createResponse.data.role_policy.id
const deleteResponse = await api.delete(
`/admin/rbac/role-policies/${rolePolicyId}`,
adminHeaders
)
expect(deleteResponse.status).toEqual(200)
expect(deleteResponse.data).toEqual({
id: rolePolicyId,
object: "rbac_role_policy",
deleted: true,
})
const listResponse = await api.get(
`/admin/rbac/role-policies?role_id=${editorRole.id}`,
adminHeaders
)
expect(
listResponse.data.role_policies.find((rp) => rp.id === rolePolicyId)
).toBeUndefined()
})
})
})
},
})

View File

@@ -66,6 +66,9 @@ const modules = {
resolve: "@medusajs/index",
disable: process.env.ENABLE_INDEX_MODULE !== "true",
},
[Modules.RBAC]: {
resolve: "@medusajs/rbac",
},
}
if (process.env.MEDUSA_FF_TRANSLATION === "true") {

View File

@@ -0,0 +1,516 @@
import {
createRbacPoliciesWorkflow,
createRbacRolesWorkflow,
} from "@medusajs/core-flows"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { IRbacModuleService, MedusaContainer } from "@medusajs/types"
import { Modules } from "@medusajs/utils"
jest.setTimeout(50000)
medusaIntegrationTestRunner({
env: {},
testSuite: ({ getContainer }) => {
describe("Workflows: RBAC", () => {
let appContainer: MedusaContainer
let rbacService: IRbacModuleService
beforeAll(async () => {
appContainer = getContainer()
rbacService = appContainer.resolve(Modules.RBAC)
})
describe("Role Inheritance and Policy Management", () => {
it("should create roles with inheritance and policies, then list all inherited policies", async () => {
// Step 1: Create base policies
const policiesWorkflow = createRbacPoliciesWorkflow(appContainer)
const { result: createdPolicies } = await policiesWorkflow.run({
input: {
policies: [
{
key: "read:products",
resource: "product",
operation: "read",
name: "Read Products",
description: "Permission to read products",
},
{
key: "write:products",
resource: "product",
operation: "write",
name: "Write Products",
description: "Permission to write products",
},
{
key: "read:orders",
resource: "order",
operation: "read",
name: "Read Orders",
description: "Permission to read orders",
},
{
key: "write:orders",
resource: "order",
operation: "write",
name: "Write Orders",
description: "Permission to write orders",
},
{
key: "delete:users",
resource: "user",
operation: "delete",
name: "Delete Users",
description: "Permission to delete users",
},
],
},
})
expect(createdPolicies).toHaveLength(5)
// Step 2: Create base roles with their policies
const rolesWorkflow = createRbacRolesWorkflow(appContainer)
// Create "Viewer" role with read permissions
const { result: viewerRoles } = await rolesWorkflow.run({
input: {
roles: [
{
name: "Viewer",
description: "Can view products and orders",
policy_ids: [
createdPolicies[0].id, // read:products
createdPolicies[2].id, // read:orders
],
},
],
},
})
expect(viewerRoles).toHaveLength(1)
const viewerRole = viewerRoles[0]
// Create "Editor" role with write permissions
const { result: editorRoles } = await rolesWorkflow.run({
input: {
roles: [
{
name: "Editor",
description: "Can write products and orders",
policy_ids: [
createdPolicies[1].id, // write:products
createdPolicies[3].id, // write:orders
],
},
],
},
})
expect(editorRoles).toHaveLength(1)
const editorRole = editorRoles[0]
// Step 3: Create "Admin" role that inherits from both Viewer and Editor, plus additional permissions
const { result: adminRoles } = await rolesWorkflow.run({
input: {
roles: [
{
name: "Admin",
description:
"Inherits from Viewer and Editor, plus can delete users",
inherited_role_ids: [viewerRole.id, editorRole.id],
policy_ids: [
createdPolicies[4].id, // delete:users
],
},
],
},
})
expect(adminRoles).toHaveLength(1)
const adminRole = adminRoles[0]
// Step 4: Verify role inheritance was created
const inheritances = await rbacService.listRbacRoleInheritances({
role_id: adminRole.id,
})
expect(inheritances).toHaveLength(2)
expect(inheritances).toEqual(
expect.arrayContaining([
expect.objectContaining({
role_id: adminRole.id,
inherited_role_id: viewerRole.id,
}),
expect.objectContaining({
role_id: adminRole.id,
inherited_role_id: editorRole.id,
}),
])
)
// Step 5: Verify direct policies for Admin role
const adminDirectPolicies = await rbacService.listRbacRolePolicies({
role_id: adminRole.id,
})
expect(adminDirectPolicies).toHaveLength(1)
expect(adminDirectPolicies[0]).toEqual(
expect.objectContaining({
role_id: adminRole.id,
scope_id: createdPolicies[4].id, // delete:users
})
)
// Step 6: List ALL policies for Admin role (including inherited)
const allAdminPolicies = await rbacService.listPoliciesForRole(
adminRole.id
)
// Admin should have:
// - read:products (from Viewer)
// - read:orders (from Viewer)
// - write:products (from Editor)
// - write:orders (from Editor)
// - delete:users (direct)
expect(allAdminPolicies).toHaveLength(5)
const policyKeys = allAdminPolicies.map((p) => p.key).sort()
expect(policyKeys).toEqual([
"delete:users",
"read:orders",
"read:products",
"write:orders",
"write:products",
])
// Verify each policy has correct details
expect(allAdminPolicies).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: "read:products",
resource: "product",
operation: "read",
}),
expect.objectContaining({
key: "write:products",
resource: "product",
operation: "write",
}),
expect.objectContaining({
key: "read:orders",
resource: "order",
operation: "read",
}),
expect.objectContaining({
key: "write:orders",
resource: "order",
operation: "write",
}),
expect.objectContaining({
key: "delete:users",
resource: "user",
operation: "delete",
}),
])
)
// Step 7: Verify Viewer role only has its direct policies
const viewerPolicies = await rbacService.listPoliciesForRole(
viewerRole.id
)
expect(viewerPolicies).toHaveLength(2)
expect(viewerPolicies).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: "read:products",
}),
expect.objectContaining({
key: "read:orders",
}),
])
)
// Step 8: Verify Editor role only has its direct policies
const editorPolicies = await rbacService.listPoliciesForRole(
editorRole.id
)
expect(editorPolicies).toHaveLength(2)
expect(editorPolicies).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: "write:products",
}),
expect.objectContaining({
key: "write:orders",
}),
])
)
})
it("should handle multi-level role inheritance", async () => {
// Create policies
const policiesWorkflow = createRbacPoliciesWorkflow(appContainer)
const { result: policies } = await policiesWorkflow.run({
input: {
policies: [
{
key: "read:catalog",
resource: "catalog",
operation: "read",
name: "Read Catalog",
},
{
key: "write:catalog",
resource: "catalog",
operation: "write",
name: "Write Catalog",
},
{
key: "admin:system",
resource: "system",
operation: "admin",
name: "System Admin",
},
],
},
})
// Create role hierarchy: Basic -> Manager -> SuperAdmin
const rolesWorkflow = createRbacRolesWorkflow(appContainer)
// Level 1: Basic role
const { result: basicRoles } = await rolesWorkflow.run({
input: {
roles: [
{
name: "Basic User",
description: "Basic read access",
policy_ids: [policies[0].id], // read:catalog
},
],
},
})
const basicRole = basicRoles[0]
// Level 2: Manager inherits from Basic
const { result: managerRoles } = await rolesWorkflow.run({
input: {
roles: [
{
name: "Manager",
description: "Manager with write access",
inherited_role_ids: [basicRole.id],
policy_ids: [policies[1].id], // write:catalog
},
],
},
})
const managerRole = managerRoles[0]
// Level 3: SuperAdmin inherits from Manager
const { result: superAdminRoles } = await rolesWorkflow.run({
input: {
roles: [
{
name: "SuperAdmin",
description: "Super admin with all access",
inherited_role_ids: [managerRole.id],
policy_ids: [policies[2].id], // admin:system
},
],
},
})
const superAdminRole = superAdminRoles[0]
// Verify SuperAdmin has all policies through inheritance chain
const superAdminPolicies = await rbacService.listPoliciesForRole(
superAdminRole.id
)
expect(superAdminPolicies).toHaveLength(3)
expect(superAdminPolicies.map((p) => p.key).sort()).toEqual([
"admin:system",
"read:catalog",
"write:catalog",
])
// Verify Manager has policies from Basic + its own
const managerPolicies = await rbacService.listPoliciesForRole(
managerRole.id
)
expect(managerPolicies).toHaveLength(2)
expect(managerPolicies.map((p) => p.key).sort()).toEqual([
"read:catalog",
"write:catalog",
])
// Verify Basic only has its own policy
const basicPolicies = await rbacService.listPoliciesForRole(
basicRole.id
)
expect(basicPolicies).toHaveLength(1)
expect(basicPolicies[0].key).toBe("read:catalog")
})
it("should propagate policy deletion from inherited role", async () => {
// Create policies
const policiesWorkflow = createRbacPoliciesWorkflow(appContainer)
const { result: policies } = await policiesWorkflow.run({
input: {
policies: [
{
key: "read:inventory",
resource: "inventory",
operation: "read",
name: "Read Inventory",
},
{
key: "write:inventory",
resource: "inventory",
operation: "write",
name: "Write Inventory",
},
{
key: "admin:inventory",
resource: "inventory",
operation: "admin",
name: "Admin Inventory",
},
],
},
})
// Create role hierarchy: Basic -> Manager -> SuperAdmin
const rolesWorkflow = createRbacRolesWorkflow(appContainer)
// Level 1: Basic role with read permission
const { result: basicRoles } = await rolesWorkflow.run({
input: {
roles: [
{
name: "Basic Inventory User",
description: "Basic inventory read access",
policy_ids: [policies[0].id], // read:inventory
},
],
},
})
const basicRole = basicRoles[0]
// Level 2: Manager inherits from Basic + write permission
const { result: managerRoles } = await rolesWorkflow.run({
input: {
roles: [
{
name: "Inventory Manager",
description: "Manager with write access",
inherited_role_ids: [basicRole.id],
policy_ids: [policies[1].id], // write:inventory
},
],
},
})
const managerRole = managerRoles[0]
// Level 3: SuperAdmin inherits from Manager + admin permission
const { result: superAdminRoles } = await rolesWorkflow.run({
input: {
roles: [
{
name: "Inventory SuperAdmin",
description: "Super admin with all access",
inherited_role_ids: [managerRole.id],
policy_ids: [policies[2].id], // admin:inventory
},
],
},
})
const superAdminRole = superAdminRoles[0]
// Verify initial state: SuperAdmin has all 3 policies
let superAdminPolicies = await rbacService.listPoliciesForRole(
superAdminRole.id
)
expect(superAdminPolicies).toHaveLength(3)
expect(superAdminPolicies.map((p) => p.key).sort()).toEqual([
"admin:inventory",
"read:inventory",
"write:inventory",
])
// Verify Manager has 2 policies (Basic's + its own)
let managerPolicies = await rbacService.listPoliciesForRole(
managerRole.id
)
expect(managerPolicies).toHaveLength(2)
expect(managerPolicies.map((p) => p.key).sort()).toEqual([
"read:inventory",
"write:inventory",
])
// Delete the read:inventory policy from Basic role
const basicRolePolicies = await rbacService.listRbacRolePolicies({
role_id: basicRole.id,
})
const readPolicyAssociation = basicRolePolicies.find(
(rp) => rp.scope_id === policies[0].id
)
await rbacService.deleteRbacRolePolicies([readPolicyAssociation!.id])
// Verify Basic role no longer has the read policy
const basicPoliciesAfterDelete =
await rbacService.listPoliciesForRole(basicRole.id)
expect(basicPoliciesAfterDelete).toHaveLength(0)
// Verify Manager role no longer inherits the read policy
managerPolicies = await rbacService.listPoliciesForRole(
managerRole.id
)
expect(managerPolicies).toHaveLength(1)
expect(managerPolicies[0].key).toBe("write:inventory")
// Verify SuperAdmin role also lost the read policy through inheritance chain
superAdminPolicies = await rbacService.listPoliciesForRole(
superAdminRole.id
)
expect(superAdminPolicies).toHaveLength(2)
expect(superAdminPolicies.map((p) => p.key).sort()).toEqual([
"admin:inventory",
"write:inventory",
])
})
it("should handle role with no inherited roles or policies", async () => {
const rolesWorkflow = createRbacRolesWorkflow(appContainer)
const { result: emptyRoles } = await rolesWorkflow.run({
input: {
roles: [
{
name: "Empty Role",
description: "Role with no permissions",
},
],
},
})
const emptyRole = emptyRoles[0]
// Verify no policies
const policies = await rbacService.listPoliciesForRole(emptyRole.id)
expect(policies).toHaveLength(0)
// Verify no inheritance
const inheritances = await rbacService.listRbacRoleInheritances({
role_id: emptyRole.id,
})
expect(inheritances).toHaveLength(0)
})
})
})
},
})

View File

@@ -189,5 +189,9 @@ module.exports = defineConfig({
key: "brand",
resolve: "src/modules/brand",
},
{
key: Modules.RBAC,
resolve: "@medusajs/rbac",
},
],
})

View File

@@ -21,6 +21,7 @@ export * from "./pricing"
export * from "./product"
export * from "./product-category"
export * from "./promotion"
export * from "./rbac"
export * from "./region"
export * from "./reservation"
export * from "./return-reason"

View File

@@ -0,0 +1,2 @@
export * from "./steps"
export * from "./workflows"

View File

@@ -0,0 +1,40 @@
import { Modules } from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
import { IRbacModuleService } from "@medusajs/types"
export type CreateRbacPolicyDTO = {
key: string
resource: string
operation: string
name?: string | null
description?: string | null
metadata?: Record<string, unknown> | null
}
export type CreateRbacPoliciesStepInput = {
policies: CreateRbacPolicyDTO[]
}
export const createRbacPoliciesStepId = "create-rbac-policies"
export const createRbacPoliciesStep = createStep(
createRbacPoliciesStepId,
async (data: CreateRbacPoliciesStepInput, { container }) => {
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
const created = await service.createRbacPolicies(data.policies)
return new StepResponse(
created,
(created ?? []).map((p) => p.id)
)
},
async (createdIds: string[] | undefined, { container }) => {
if (!createdIds?.length) {
return
}
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
await service.deleteRbacPolicies(createdIds)
}
)

View File

@@ -0,0 +1,43 @@
import { Modules } from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
import { IRbacModuleService } from "@medusajs/types"
export type CreateRbacRoleInheritanceDTO = {
role_id: string
inherited_role_id: string
metadata?: Record<string, unknown> | null
}
export type CreateRbacRoleInheritancesStepInput = {
role_inheritances: CreateRbacRoleInheritanceDTO[]
}
export const createRbacRoleInheritancesStepId = "create-rbac-role-inheritances"
export const createRbacRoleInheritancesStep = createStep(
createRbacRoleInheritancesStepId,
async (data: CreateRbacRoleInheritancesStepInput, { container }) => {
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
if (!data.role_inheritances || data.role_inheritances.length === 0) {
return new StepResponse([], [])
}
const created = await service.createRbacRoleInheritances(
data.role_inheritances
)
return new StepResponse(
created,
(created ?? []).map((ri) => ri.id)
)
},
async (createdIds: string[] | undefined, { container }) => {
if (!createdIds?.length) {
return
}
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
await service.deleteRbacRoleInheritances(createdIds)
}
)

View File

@@ -0,0 +1,37 @@
import { Modules } from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
import { IRbacModuleService } from "@medusajs/types"
export type CreateRbacRolePolicyDTO = {
role_id: string
scope_id: string
metadata?: Record<string, unknown> | null
}
export type CreateRbacRolePoliciesStepInput = {
role_policies: CreateRbacRolePolicyDTO[]
}
export const createRbacRolePoliciesStepId = "create-rbac-role-policies"
export const createRbacRolePoliciesStep = createStep(
createRbacRolePoliciesStepId,
async (data: CreateRbacRolePoliciesStepInput, { container }) => {
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
const created = await service.createRbacRolePolicies(data.role_policies)
return new StepResponse(
created,
(created ?? []).map((rp) => rp.id)
)
},
async (createdIds: string[] | undefined, { container }) => {
if (!createdIds?.length) {
return
}
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
await service.deleteRbacRolePolicies(createdIds)
}
)

View File

@@ -0,0 +1,37 @@
import { Modules } from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
import { IRbacModuleService } from "@medusajs/types"
export type CreateRbacRoleDTO = {
name: string
description?: string | null
metadata?: Record<string, unknown> | null
}
export type CreateRbacRolesStepInput = {
roles: CreateRbacRoleDTO[]
}
export const createRbacRolesStepId = "create-rbac-roles"
export const createRbacRolesStep = createStep(
createRbacRolesStepId,
async (data: CreateRbacRolesStepInput, { container }) => {
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
const created = await service.createRbacRoles(data.roles)
return new StepResponse(
created,
(created ?? []).map((r) => r.id)
)
},
async (createdIds: string[] | undefined, { container }) => {
if (!createdIds?.length) {
return
}
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
await service.deleteRbacRoles(createdIds)
}
)

View File

@@ -0,0 +1,17 @@
import { Modules } from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
import { IRbacModuleService } from "@medusajs/types"
export type DeleteRbacPoliciesStepInput = string[]
export const deleteRbacPoliciesStepId = "delete-rbac-policies"
export const deleteRbacPoliciesStep = createStep(
{ name: deleteRbacPoliciesStepId, noCompensation: true },
async (ids: DeleteRbacPoliciesStepInput, { container }) => {
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
await service.deleteRbacPolicies(ids)
return new StepResponse(void 0)
},
async () => {}
)

View File

@@ -0,0 +1,17 @@
import { Modules } from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
import { IRbacModuleService } from "@medusajs/types"
export type DeleteRbacRolePoliciesStepInput = string[]
export const deleteRbacRolePoliciesStepId = "delete-rbac-role-policies"
export const deleteRbacRolePoliciesStep = createStep(
{ name: deleteRbacRolePoliciesStepId, noCompensation: true },
async (ids: DeleteRbacRolePoliciesStepInput, { container }) => {
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
await service.deleteRbacRolePolicies(ids)
return new StepResponse(void 0)
},
async () => {}
)

View File

@@ -0,0 +1,17 @@
import { Modules } from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
import { IRbacModuleService } from "@medusajs/types"
export type DeleteRbacRolesStepInput = string[]
export const deleteRbacRolesStepId = "delete-rbac-roles"
export const deleteRbacRolesStep = createStep(
{ name: deleteRbacRolesStepId, noCompensation: true },
async (ids: DeleteRbacRolesStepInput, { container }) => {
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
await service.deleteRbacRoles(ids)
return new StepResponse(void 0)
},
async () => {}
)

View File

@@ -0,0 +1,14 @@
export * from "./create-rbac-roles"
export * from "./delete-rbac-roles"
export * from "./update-rbac-roles"
export * from "./create-rbac-policies"
export * from "./delete-rbac-policies"
export * from "./update-rbac-policies"
export * from "./create-rbac-role-policies"
export * from "./delete-rbac-role-policies"
export * from "./update-rbac-role-policies"
export * from "./create-rbac-role-inheritances"
export * from "./set-role-inheritance"

View File

@@ -0,0 +1,122 @@
import { Modules } from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
import { IRbacModuleService } from "@medusajs/types"
export type SetRoleInheritanceStepInput = Array<{
role_id: string
inherited_role_ids: string[]
}>
export const setRoleInheritanceStepId = "set-role-inheritance"
export const setRoleInheritanceStep = createStep(
setRoleInheritanceStepId,
async (data: SetRoleInheritanceStepInput, { container }) => {
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
const allCompensationData: Array<{
role_id: string
previousInheritedRoleIds: string[]
}> = []
if (!data || data.length === 0) {
return new StepResponse(
{ created: [], removedCount: 0 },
allCompensationData
)
}
const allToRemoveIds: string[] = []
const allToCreate: Array<{
role_id: string
inherited_role_id: string
}> = []
for (const roleData of data) {
const existingInheritance = await service.listRbacRoleInheritances({
role_id: roleData.role_id,
})
const existingInheritedRoleIds = existingInheritance.map(
(ri) => ri.inherited_role_id
)
allCompensationData.push({
role_id: roleData.role_id,
previousInheritedRoleIds: existingInheritedRoleIds,
})
const toAdd = roleData.inherited_role_ids.filter(
(id) => !existingInheritedRoleIds.includes(id)
)
const toRemove = existingInheritedRoleIds.filter(
(id) => !roleData.inherited_role_ids.includes(id)
)
if (toRemove.length > 0) {
const toRemoveRecords = existingInheritance.filter((ri) =>
toRemove.includes(ri.inherited_role_id)
)
allToRemoveIds.push(...toRemoveRecords.map((ri) => ri.id))
}
if (toAdd.length > 0) {
allToCreate.push(
...toAdd.map((inherited_role_id) => ({
role_id: roleData.role_id,
inherited_role_id,
}))
)
}
}
if (allToRemoveIds.length > 0) {
await service.deleteRbacRoleInheritances(allToRemoveIds)
}
let created: any[] = []
if (allToCreate.length > 0) {
created = await service.createRbacRoleInheritances(allToCreate)
}
return new StepResponse(
{ created, removedCount: allToRemoveIds.length },
allCompensationData
)
},
async (
compensationData:
| Array<{ role_id: string; previousInheritedRoleIds: string[] }>
| undefined,
{ container }
) => {
if (!compensationData || compensationData.length === 0) {
return
}
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
for (const roleCompensation of compensationData) {
const currentInheritance = await service.listRbacRoleInheritances({
role_id: roleCompensation.role_id,
})
if (currentInheritance.length > 0) {
await service.deleteRbacRoleInheritances(
currentInheritance.map((ri) => ri.id)
)
}
if (roleCompensation.previousInheritedRoleIds.length > 0) {
await service.createRbacRoleInheritances(
roleCompensation.previousInheritedRoleIds.map(
(inherited_role_id) => ({
role_id: roleCompensation.role_id,
inherited_role_id,
})
)
)
}
}
}
)

View File

@@ -0,0 +1,61 @@
import {
getSelectsAndRelationsFromObjectArray,
Modules,
} from "@medusajs/framework/utils"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { IRbacModuleService, UpdateRbacPolicyDTO } from "@medusajs/types"
export type UpdateRbacPoliciesStepInput = {
selector: Record<string, any>
update: Omit<UpdateRbacPolicyDTO, "id">
}
export const updateRbacPoliciesStepId = "update-rbac-policies"
export const updateRbacPoliciesStep = createStep(
updateRbacPoliciesStepId,
async (data: UpdateRbacPoliciesStepInput, { container }) => {
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
const { selects, relations } = getSelectsAndRelationsFromObjectArray([
data.update,
])
const prevData = await service.listRbacPolicies(data.selector, {
select: selects,
relations,
})
const updates = (prevData ?? []).map((p) => ({
id: p.id,
...data.update,
})) as UpdateRbacPolicyDTO[]
const updated = await service.updateRbacPolicies(updates)
return new StepResponse(updated, {
prevData,
updateKeys: Object.keys(data.update ?? {}),
})
},
async (
compensationData: { prevData: any[]; updateKeys: string[] } | undefined,
{ container }
) => {
if (!compensationData?.prevData?.length) {
return
}
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
const updates = compensationData.prevData.map((p) => {
const payload: Record<string, any> = { id: p.id }
for (const key of compensationData.updateKeys) {
payload[key] = p[key]
}
return payload
}) as UpdateRbacPolicyDTO[]
await service.updateRbacPolicies(updates)
}
)

View File

@@ -0,0 +1,61 @@
import {
getSelectsAndRelationsFromObjectArray,
Modules,
} from "@medusajs/framework/utils"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { IRbacModuleService, UpdateRbacRolePolicyDTO } from "@medusajs/types"
export type UpdateRbacRolePoliciesStepInput = {
selector: Record<string, any>
update: Omit<UpdateRbacRolePolicyDTO, "id">
}
export const updateRbacRolePoliciesStepId = "update-rbac-role-policies"
export const updateRbacRolePoliciesStep = createStep(
updateRbacRolePoliciesStepId,
async (data: UpdateRbacRolePoliciesStepInput, { container }) => {
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
const { selects, relations } = getSelectsAndRelationsFromObjectArray([
data.update,
])
const prevData = await service.listRbacRolePolicies(data.selector, {
select: selects,
relations,
})
const updates = (prevData ?? []).map((rp) => ({
id: rp.id,
...data.update,
})) as UpdateRbacRolePolicyDTO[]
const updated = await service.updateRbacRolePolicies(updates)
return new StepResponse(updated, {
prevData,
updateKeys: Object.keys(data.update ?? {}),
})
},
async (
compensationData: { prevData: any[]; updateKeys: string[] } | undefined,
{ container }
) => {
if (!compensationData?.prevData?.length) {
return
}
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
const updates = compensationData.prevData.map((rp) => {
const payload: Record<string, any> = { id: rp.id }
for (const key of compensationData.updateKeys) {
payload[key] = rp[key]
}
return payload
}) as UpdateRbacRolePolicyDTO[]
await service.updateRbacRolePolicies(updates)
}
)

View File

@@ -0,0 +1,61 @@
import {
getSelectsAndRelationsFromObjectArray,
Modules,
} from "@medusajs/framework/utils"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { IRbacModuleService, UpdateRbacRoleDTO } from "@medusajs/types"
export type UpdateRbacRolesStepInput = {
selector: Record<string, any>
update: Omit<UpdateRbacRoleDTO, "id">
}
export const updateRbacRolesStepId = "update-rbac-roles"
export const updateRbacRolesStep = createStep(
updateRbacRolesStepId,
async (data: UpdateRbacRolesStepInput, { container }) => {
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
const { selects, relations } = getSelectsAndRelationsFromObjectArray([
data.update,
])
const prevData = await service.listRbacRoles(data.selector, {
select: selects,
relations,
})
const updates = (prevData ?? []).map((r) => ({
id: r.id,
...data.update,
})) as UpdateRbacRoleDTO[]
const updated = await service.updateRbacRoles(updates)
return new StepResponse(updated, {
prevData,
updateKeys: Object.keys(data.update ?? {}),
})
},
async (
compensationData: { prevData: any[]; updateKeys: string[] } | undefined,
{ container }
) => {
if (!compensationData?.prevData?.length) {
return
}
const service = container.resolve<IRbacModuleService>(Modules.RBAC)
const updates = compensationData.prevData.map((r) => {
const payload: Record<string, any> = { id: r.id }
for (const key of compensationData.updateKeys) {
payload[key] = r[key]
}
return payload
}) as UpdateRbacRoleDTO[]
await service.updateRbacRoles(updates)
}
)

View File

@@ -0,0 +1,19 @@
import {
WorkflowData,
WorkflowResponse,
createWorkflow,
} from "@medusajs/framework/workflows-sdk"
import { createRbacPoliciesStep } from "../steps"
export type CreateRbacPoliciesWorkflowInput = {
policies: any[]
}
export const createRbacPoliciesWorkflowId = "create-rbac-policies"
export const createRbacPoliciesWorkflow = createWorkflow(
createRbacPoliciesWorkflowId,
(input: WorkflowData<CreateRbacPoliciesWorkflowInput>) => {
return new WorkflowResponse(createRbacPoliciesStep(input))
}
)

View File

@@ -0,0 +1,19 @@
import {
WorkflowData,
WorkflowResponse,
createWorkflow,
} from "@medusajs/framework/workflows-sdk"
import { createRbacRolePoliciesStep } from "../steps"
export type CreateRbacRolePoliciesWorkflowInput = {
role_policies: any[]
}
export const createRbacRolePoliciesWorkflowId = "create-rbac-role-policies"
export const createRbacRolePoliciesWorkflow = createWorkflow(
createRbacRolePoliciesWorkflowId,
(input: WorkflowData<CreateRbacRolePoliciesWorkflowInput>) => {
return new WorkflowResponse(createRbacRolePoliciesStep(input))
}
)

View File

@@ -0,0 +1,80 @@
import {
WorkflowData,
WorkflowResponse,
createWorkflow,
transform,
} from "@medusajs/framework/workflows-sdk"
import {
createRbacRoleInheritancesStep,
createRbacRolePoliciesStep,
createRbacRolesStep,
} from "../steps"
export type CreateRbacRolesWorkflowInput = {
roles: {
name: string
description?: string | null
metadata?: Record<string, unknown> | null
inherited_role_ids?: string[]
policy_ids?: string[]
}[]
}
export const createRbacRolesWorkflowId = "create-rbac-roles"
export const createRbacRolesWorkflow = createWorkflow(
createRbacRolesWorkflowId,
(input: WorkflowData<CreateRbacRolesWorkflowInput>) => {
const roleData = transform({ input }, ({ input }) => ({
roles: input.roles.map((r) => ({
name: r.name,
description: r.description,
metadata: r.metadata,
})),
}))
const createdRoles = createRbacRolesStep(roleData)
const inheritanceData = transform(
{ input, createdRoles },
({ input, createdRoles }) => {
const inheritances: any[] = []
createdRoles.forEach((role, index) => {
const inheritedRoleIds = input.roles[index].inherited_role_ids || []
inheritedRoleIds.forEach((inheritedRoleId) => {
inheritances.push({
role_id: role.id,
inherited_role_id: inheritedRoleId,
})
})
})
return { role_inheritances: inheritances }
}
)
const policiesData = transform(
{ input, createdRoles },
({ input, createdRoles }) => {
const allPolicies: any[] = []
createdRoles.forEach((role, index) => {
const policyIds = input.roles[index].policy_ids || []
policyIds.forEach((policy_id) => {
allPolicies.push({
role_id: role.id,
scope_id: policy_id,
})
})
})
return { role_policies: allPolicies }
}
)
createRbacRoleInheritancesStep(inheritanceData)
createRbacRolePoliciesStep(policiesData)
return new WorkflowResponse(createdRoles)
}
)

View File

@@ -0,0 +1,17 @@
import { WorkflowData, createWorkflow } from "@medusajs/framework/workflows-sdk"
import { deleteRbacPoliciesStep } from "../steps"
export type DeleteRbacPoliciesWorkflowInput = {
ids: string[]
}
export const deleteRbacPoliciesWorkflowId = "delete-rbac-policies"
export const deleteRbacPoliciesWorkflow = createWorkflow(
deleteRbacPoliciesWorkflowId,
(
input: WorkflowData<DeleteRbacPoliciesWorkflowInput>
): WorkflowData<void> => {
deleteRbacPoliciesStep(input.ids)
}
)

View File

@@ -0,0 +1,17 @@
import { WorkflowData, createWorkflow } from "@medusajs/framework/workflows-sdk"
import { deleteRbacRolePoliciesStep } from "../steps"
export type DeleteRbacRolePoliciesWorkflowInput = {
ids: string[]
}
export const deleteRbacRolePoliciesWorkflowId = "delete-rbac-role-policies"
export const deleteRbacRolePoliciesWorkflow = createWorkflow(
deleteRbacRolePoliciesWorkflowId,
(
input: WorkflowData<DeleteRbacRolePoliciesWorkflowInput>
): WorkflowData<void> => {
deleteRbacRolePoliciesStep(input.ids)
}
)

View File

@@ -0,0 +1,15 @@
import { WorkflowData, createWorkflow } from "@medusajs/framework/workflows-sdk"
import { deleteRbacRolesStep } from "../steps"
export type DeleteRbacRolesWorkflowInput = {
ids: string[]
}
export const deleteRbacRolesWorkflowId = "delete-rbac-roles"
export const deleteRbacRolesWorkflow = createWorkflow(
deleteRbacRolesWorkflowId,
(input: WorkflowData<DeleteRbacRolesWorkflowInput>): WorkflowData<void> => {
deleteRbacRolesStep(input.ids)
}
)

View File

@@ -0,0 +1,11 @@
export * from "./create-rbac-roles"
export * from "./delete-rbac-roles"
export * from "./update-rbac-roles"
export * from "./create-rbac-policies"
export * from "./delete-rbac-policies"
export * from "./update-rbac-policies"
export * from "./create-rbac-role-policies"
export * from "./delete-rbac-role-policies"
export * from "./update-rbac-role-policies"

View File

@@ -0,0 +1,21 @@
import {
WorkflowData,
WorkflowResponse,
createWorkflow,
} from "@medusajs/framework/workflows-sdk"
import { UpdateRbacPolicyDTO } from "@medusajs/types"
import { updateRbacPoliciesStep } from "../steps/update-rbac-policies"
export type UpdateRbacPoliciesWorkflowInput = {
selector: Record<string, any>
update: Omit<UpdateRbacPolicyDTO, "id">
}
export const updateRbacPoliciesWorkflowId = "update-rbac-policies"
export const updateRbacPoliciesWorkflow = createWorkflow(
updateRbacPoliciesWorkflowId,
(input: WorkflowData<UpdateRbacPoliciesWorkflowInput>) => {
return new WorkflowResponse(updateRbacPoliciesStep(input))
}
)

View File

@@ -0,0 +1,21 @@
import {
WorkflowData,
WorkflowResponse,
createWorkflow,
} from "@medusajs/framework/workflows-sdk"
import { UpdateRbacRolePolicyDTO } from "@medusajs/types"
import { updateRbacRolePoliciesStep } from "../steps/update-rbac-role-policies"
export type UpdateRbacRolePoliciesWorkflowInput = {
selector: Record<string, any>
update: Omit<UpdateRbacRolePolicyDTO, "id">
}
export const updateRbacRolePoliciesWorkflowId = "update-rbac-role-policies"
export const updateRbacRolePoliciesWorkflow = createWorkflow(
updateRbacRolePoliciesWorkflowId,
(input: WorkflowData<UpdateRbacRolePoliciesWorkflowInput>) => {
return new WorkflowResponse(updateRbacRolePoliciesStep(input))
}
)

View File

@@ -0,0 +1,77 @@
import { isDefined } from "@medusajs/framework/utils"
import {
WorkflowData,
WorkflowResponse,
createWorkflow,
transform,
} from "@medusajs/framework/workflows-sdk"
import { UpdateRbacRoleDTO } from "@medusajs/types"
import { createRbacRolePoliciesStep, setRoleInheritanceStep } from "../steps"
import { updateRbacRolesStep } from "../steps/update-rbac-roles"
export type UpdateRbacRolesWorkflowInput = {
selector: Record<string, any>
update: Omit<UpdateRbacRoleDTO, "id"> & {
inherited_role_ids?: string[]
policy_ids?: string[]
}
}
export const updateRbacRolesWorkflowId = "update-rbac-roles"
export const updateRbacRolesWorkflow = createWorkflow(
updateRbacRolesWorkflowId,
(input: WorkflowData<UpdateRbacRolesWorkflowInput>) => {
const roleUpdateData = transform({ input }, ({ input }) => ({
selector: input.selector,
update: {
name: input.update.name,
description: input.update.description,
metadata: input.update.metadata,
},
}))
const updatedRoles = updateRbacRolesStep(roleUpdateData)
const inheritanceUpdateData = transform(
{ input, updatedRoles },
({ input, updatedRoles }) => {
if (!isDefined(input.update.inherited_role_ids)) {
return []
}
return updatedRoles.map((role) => ({
role_id: role.id,
inherited_role_ids: input.update.inherited_role_ids || [],
}))
}
)
setRoleInheritanceStep(inheritanceUpdateData)
const policiesUpdateData = transform(
{ input, updatedRoles },
({ input, updatedRoles }) => {
if (!isDefined(input.update.policy_ids)) {
return { role_policies: [] }
}
const allPolicies: any[] = []
updatedRoles.forEach((role) => {
const policyIds = input.update.policy_ids || []
policyIds.forEach((policy_id) => {
allPolicies.push({
role_id: role.id,
scope_id: policy_id,
})
})
})
return { role_policies: allPolicies }
}
)
createRbacRolePoliciesStep(policiesUpdateData)
return new WorkflowResponse(updatedRoles)
}
)

View File

@@ -21,6 +21,7 @@ import {
IPricingModuleService,
IProductModuleService,
IPromotionModuleService,
IRbacModuleService,
IRegionModuleService,
ISalesChannelModuleService,
ISettingsModuleService,
@@ -82,6 +83,7 @@ declare module "@medusajs/types" {
[Modules.CACHING]: ICachingModuleService
[Modules.INDEX]: IIndexService
[Modules.TRANSLATION]: ITranslationModuleService
[Modules.RBAC]: IRbacModuleService
}
}

View File

@@ -32,6 +32,7 @@ export * from "./pricing"
export * from "./product"
export * from "./product-category"
export * from "./promotion"
export * from "./rbac"
export * from "./region"
export * from "./sales-channel"
export * from "./search"

View File

@@ -0,0 +1,56 @@
export type RbacRoleDTO = {
id: string
name: string
description?: string | null
metadata?: Record<string, unknown> | null
}
export type FilterableRbacRoleProps = {
id?: string | string[]
name?: string
q?: string
}
export type RbacPolicyDTO = {
id: string
key: string
resource: string
operation: string
name?: string | null
description?: string | null
metadata?: Record<string, unknown> | null
}
export type FilterableRbacPolicyProps = {
id?: string | string[]
key?: string
resource?: string
operation?: string
q?: string
}
export type RbacRolePolicyDTO = {
id: string
role_id: string
scope_id: string
metadata?: Record<string, unknown> | null
}
export type FilterableRbacRolePolicyProps = {
id?: string | string[]
role_id?: string | string[]
scope_id?: string | string[]
}
export type RbacRoleInheritanceDTO = {
id: string
role_id: string
inherited_role_id: string
metadata?: Record<string, unknown> | null
}
export type FilterableRbacRoleInheritanceProps = {
id?: string | string[]
role_id?: string | string[]
inherited_role_id?: string | string[]
}

View File

@@ -0,0 +1,3 @@
export * from "./common"
export * from "./mutations"
export * from "./service"

View File

@@ -0,0 +1,43 @@
export type CreateRbacRoleDTO = {
name: string
description?: string | null
metadata?: Record<string, unknown> | null
}
export type UpdateRbacRoleDTO = Partial<CreateRbacRoleDTO> & {
id: string
}
export type CreateRbacPolicyDTO = {
key: string
resource: string
operation: string
name?: string | null
description?: string | null
metadata?: Record<string, unknown> | null
}
export type UpdateRbacPolicyDTO = Partial<CreateRbacPolicyDTO> & {
id: string
}
export type CreateRbacRolePolicyDTO = {
role_id: string
scope_id: string
metadata?: Record<string, unknown> | null
}
export type UpdateRbacRolePolicyDTO = Partial<CreateRbacRolePolicyDTO> & {
id: string
}
export type CreateRbacRoleInheritanceDTO = {
role_id: string
inherited_role_id: string
metadata?: Record<string, unknown> | null
}
export type UpdateRbacRoleInheritanceDTO =
Partial<CreateRbacRoleInheritanceDTO> & {
id: string
}

View File

@@ -0,0 +1,194 @@
import { FindConfig } from "../common"
import { IModuleService } from "../modules-sdk"
import { Context } from "../shared-context"
import {
FilterableRbacPolicyProps,
FilterableRbacRoleInheritanceProps,
FilterableRbacRolePolicyProps,
FilterableRbacRoleProps,
RbacPolicyDTO,
RbacRoleDTO,
RbacRoleInheritanceDTO,
RbacRolePolicyDTO,
} from "./common"
import {
CreateRbacPolicyDTO,
CreateRbacRoleDTO,
CreateRbacRoleInheritanceDTO,
CreateRbacRolePolicyDTO,
UpdateRbacPolicyDTO,
UpdateRbacRoleDTO,
UpdateRbacRoleInheritanceDTO,
UpdateRbacRolePolicyDTO,
} from "./mutations"
export interface IRbacModuleService extends IModuleService {
createRbacRoles(
data: CreateRbacRoleDTO,
sharedContext?: Context
): Promise<RbacRoleDTO>
createRbacRoles(
data: CreateRbacRoleDTO[],
sharedContext?: Context
): Promise<RbacRoleDTO[]>
updateRbacRoles(
data: UpdateRbacRoleDTO,
sharedContext?: Context
): Promise<RbacRoleDTO>
updateRbacRoles(
data: UpdateRbacRoleDTO[],
sharedContext?: Context
): Promise<RbacRoleDTO[]>
deleteRbacRoles(
ids: string | string[],
sharedContext?: Context
): Promise<void>
retrieveRbacRole(
id: string,
config?: FindConfig<RbacRoleDTO>,
sharedContext?: Context
): Promise<RbacRoleDTO>
listRbacRoles(
filters?: FilterableRbacRoleProps,
config?: FindConfig<RbacRoleDTO>,
sharedContext?: Context
): Promise<RbacRoleDTO[]>
listAndCountRbacRoles(
filters?: FilterableRbacRoleProps,
config?: FindConfig<RbacRoleDTO>,
sharedContext?: Context
): Promise<[RbacRoleDTO[], number]>
createRbacPolicies(
data: CreateRbacPolicyDTO,
sharedContext?: Context
): Promise<RbacPolicyDTO>
createRbacPolicies(
data: CreateRbacPolicyDTO[],
sharedContext?: Context
): Promise<RbacPolicyDTO[]>
updateRbacPolicies(
data: UpdateRbacPolicyDTO,
sharedContext?: Context
): Promise<RbacPolicyDTO>
updateRbacPolicies(
data: UpdateRbacPolicyDTO[],
sharedContext?: Context
): Promise<RbacPolicyDTO[]>
deleteRbacPolicies(
ids: string | string[],
sharedContext?: Context
): Promise<void>
retrieveRbacPolicy(
id: string,
config?: FindConfig<RbacPolicyDTO>,
sharedContext?: Context
): Promise<RbacPolicyDTO>
listRbacPolicies(
filters?: FilterableRbacPolicyProps,
config?: FindConfig<RbacPolicyDTO>,
sharedContext?: Context
): Promise<RbacPolicyDTO[]>
listAndCountRbacPolicies(
filters?: FilterableRbacPolicyProps,
config?: FindConfig<RbacPolicyDTO>,
sharedContext?: Context
): Promise<[RbacPolicyDTO[], number]>
createRbacRolePolicies(
data: CreateRbacRolePolicyDTO,
sharedContext?: Context
): Promise<RbacRolePolicyDTO>
createRbacRolePolicies(
data: CreateRbacRolePolicyDTO[],
sharedContext?: Context
): Promise<RbacRolePolicyDTO[]>
updateRbacRolePolicies(
data: UpdateRbacRolePolicyDTO,
sharedContext?: Context
): Promise<RbacRolePolicyDTO>
updateRbacRolePolicies(
data: UpdateRbacRolePolicyDTO[],
sharedContext?: Context
): Promise<RbacRolePolicyDTO[]>
deleteRbacRolePolicies(
ids: string | string[],
sharedContext?: Context
): Promise<void>
retrieveRbacRolePolicy(
id: string,
config?: FindConfig<RbacRolePolicyDTO>,
sharedContext?: Context
): Promise<RbacRolePolicyDTO>
listRbacRolePolicies(
filters?: FilterableRbacRolePolicyProps,
config?: FindConfig<RbacRolePolicyDTO>,
sharedContext?: Context
): Promise<RbacRolePolicyDTO[]>
listAndCountRbacRolePolicies(
filters?: FilterableRbacRolePolicyProps,
config?: FindConfig<RbacRolePolicyDTO>,
sharedContext?: Context
): Promise<[RbacRolePolicyDTO[], number]>
createRbacRoleInheritances(
data: CreateRbacRoleInheritanceDTO,
sharedContext?: Context
): Promise<RbacRoleInheritanceDTO>
createRbacRoleInheritances(
data: CreateRbacRoleInheritanceDTO[],
sharedContext?: Context
): Promise<RbacRoleInheritanceDTO[]>
updateRbacRoleInheritances(
data: UpdateRbacRoleInheritanceDTO,
sharedContext?: Context
): Promise<RbacRoleInheritanceDTO>
updateRbacRoleInheritances(
data: UpdateRbacRoleInheritanceDTO[],
sharedContext?: Context
): Promise<RbacRoleInheritanceDTO[]>
deleteRbacRoleInheritances(
ids: string | string[],
sharedContext?: Context
): Promise<void>
retrieveRbacRoleInheritance(
id: string,
config?: FindConfig<RbacRoleInheritanceDTO>,
sharedContext?: Context
): Promise<RbacRoleInheritanceDTO>
listRbacRoleInheritances(
filters?: FilterableRbacRoleInheritanceProps,
config?: FindConfig<RbacRoleInheritanceDTO>,
sharedContext?: Context
): Promise<RbacRoleInheritanceDTO[]>
listAndCountRbacRoleInheritances(
filters?: FilterableRbacRoleInheritanceProps,
config?: FindConfig<RbacRoleInheritanceDTO>,
sharedContext?: Context
): Promise<[RbacRoleInheritanceDTO[], number]>
listPoliciesForRole(
roleId: string,
sharedContext?: Context
): Promise<RbacPolicyDTO[]>
}

View File

@@ -29,6 +29,7 @@ export const Modules = {
SETTINGS: "settings",
CACHING: "caching",
TRANSLATION: "translation",
RBAC: "rbac",
} as const
export const MODULE_PACKAGE_NAMES = {
@@ -62,6 +63,7 @@ export const MODULE_PACKAGE_NAMES = {
[Modules.SETTINGS]: "@medusajs/medusa/settings",
[Modules.CACHING]: "@medusajs/caching",
[Modules.TRANSLATION]: "@medusajs/translation",
[Modules.RBAC]: "@medusajs/medusa/rbac",
}
export const REVERSED_MODULE_PACKAGE_NAMES = Object.entries(

View File

@@ -1,10 +1,10 @@
import { join } from "path"
import { Modules } from "./definition"
import type { LoadedModule } from "@medusajs/types"
import { join } from "path"
import { FileSystem } from "../common/file-system"
import { toUnixSlash } from "../common/to-unix-slash"
import { toCamelCase } from "../common/to-camel-case"
import { toUnixSlash } from "../common/to-unix-slash"
import { upperCaseFirst } from "../common/upper-case-first"
import { Modules } from "./definition"
/**
* For known services that has interfaces, we will set the container
@@ -37,6 +37,10 @@ const SERVICES_INTERFACES = {
[Modules.FILE]: "IFileModuleService",
[Modules.NOTIFICATION]: "INotificationModuleService",
[Modules.LOCKING]: "ILockingModule",
[Modules.SETTINGS]: "ISettingsModuleService",
[Modules.CACHING]: "ICachingModuleService",
[Modules.TRANSLATION]: "ITranslationModuleService",
[Modules.RBAC]: "IRbacModuleService",
}
/**

View File

@@ -0,0 +1,11 @@
import { MiddlewareRoute } from "@medusajs/framework/http"
import { adminRbacPolicyRoutesMiddlewares } from "./policies/middlewares"
import { adminRbacRolePolicyRoutesMiddlewares } from "./role-policies/middlewares"
import { adminRbacRoleRoutesMiddlewares } from "./roles/middlewares"
export const adminRbacRoutesMiddlewares: MiddlewareRoute[] = [
...adminRbacRoleRoutesMiddlewares,
...adminRbacPolicyRoutesMiddlewares,
...adminRbacRolePolicyRoutesMiddlewares,
]

View File

@@ -0,0 +1,91 @@
import {
deleteRbacPoliciesWorkflow,
updateRbacPoliciesWorkflow,
} from "@medusajs/core-flows"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import {
ContainerRegistrationKeys,
MedusaError,
} from "@medusajs/framework/utils"
import { AdminUpdateRbacPolicyType } from "../validators"
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
const { data: policies } = await query.graph({
entity: "rbac_policy",
filters: { id: req.params.id },
fields: req.queryConfig.fields,
})
const policy = policies[0]
if (!policy) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Policy with id: ${req.params.id} not found`
)
}
res.status(200).json({ policy })
}
export const POST = async (
req: AuthenticatedMedusaRequest<AdminUpdateRbacPolicyType>,
res: MedusaResponse
) => {
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
const { data: existing } = await query.graph({
entity: "rbac_policy",
filters: { id: req.params.id },
fields: ["id"],
})
const existingPolicy = existing[0]
if (!existingPolicy) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Policy with id "${req.params.id}" not found`
)
}
const { result } = await updateRbacPoliciesWorkflow(req.scope).run({
input: {
selector: { id: req.params.id },
update: req.validatedBody,
},
})
const { data: policies } = await query.graph({
entity: "rbac_policy",
filters: { id: result[0].id },
fields: req.queryConfig.fields,
})
const policy = policies[0]
res.status(200).json({ policy })
}
export const DELETE = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const id = req.params.id
await deleteRbacPoliciesWorkflow(req.scope).run({
input: { ids: [id] },
})
res.status(200).json({
id,
object: "rbac_policy",
deleted: true,
})
}

View File

@@ -0,0 +1,64 @@
import * as QueryConfig from "./query-config"
import {
validateAndTransformBody,
validateAndTransformQuery,
} from "@medusajs/framework"
import { MiddlewareRoute } from "@medusajs/framework/http"
import {
AdminCreateRbacPolicy,
AdminGetRbacPoliciesParams,
AdminGetRbacPolicyParams,
AdminUpdateRbacPolicy,
} from "./validators"
export const adminRbacPolicyRoutesMiddlewares: MiddlewareRoute[] = [
{
method: ["GET"],
matcher: "/admin/rbac/policies",
middlewares: [
validateAndTransformQuery(
AdminGetRbacPoliciesParams,
QueryConfig.listTransformQueryConfig
),
],
},
{
method: ["GET"],
matcher: "/admin/rbac/policies/:id",
middlewares: [
validateAndTransformQuery(
AdminGetRbacPolicyParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
{
method: ["POST"],
matcher: "/admin/rbac/policies",
middlewares: [
validateAndTransformBody(AdminCreateRbacPolicy),
validateAndTransformQuery(
AdminGetRbacPolicyParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
{
method: ["POST"],
matcher: "/admin/rbac/policies/:id",
middlewares: [
validateAndTransformBody(AdminUpdateRbacPolicy),
validateAndTransformQuery(
AdminGetRbacPolicyParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
{
method: ["DELETE"],
matcher: "/admin/rbac/policies/:id",
middlewares: [],
},
]

View File

@@ -0,0 +1,23 @@
export const defaultAdminRbacPolicyFields = [
"id",
"key",
"resource",
"operation",
"name",
"description",
"metadata",
"created_at",
"updated_at",
"deleted_at",
]
export const retrieveTransformQueryConfig = {
defaults: defaultAdminRbacPolicyFields,
isList: false,
}
export const listTransformQueryConfig = {
...retrieveTransformQueryConfig,
defaultLimit: 20,
isList: true,
}

View File

@@ -0,0 +1,50 @@
import { createRbacPoliciesWorkflow } from "@medusajs/core-flows"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
import { AdminCreateRbacPolicyType } from "./validators"
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
const { data: policies, metadata } = await query.graph({
entity: "rbac_policy",
fields: req.queryConfig.fields,
filters: req.filterableFields,
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<AdminCreateRbacPolicyType>,
res: MedusaResponse
) => {
const input = [req.validatedBody]
const { result } = await createRbacPoliciesWorkflow(req.scope).run({
input: { policies: input },
})
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
const { data: policies } = await query.graph({
entity: "rbac_policy",
fields: req.queryConfig.fields,
filters: { id: result[0].id },
})
const policy = policies[0]
res.status(200).json({ policy })
}

View File

@@ -0,0 +1,57 @@
import { z } from "zod"
import { applyAndAndOrOperators } from "../../../utils/common-validators"
import {
createFindParams,
createOperatorMap,
createSelectParams,
} from "../../../utils/validators"
export type AdminGetRbacPolicyParamsType = z.infer<
typeof AdminGetRbacPolicyParams
>
export const AdminGetRbacPolicyParams = createSelectParams()
export const AdminGetRbacPoliciesParamsFields = z.object({
q: z.string().optional(),
id: z.union([z.string(), z.array(z.string())]).optional(),
key: z.union([z.string(), z.array(z.string())]).optional(),
resource: z.union([z.string(), z.array(z.string())]).optional(),
operation: z.union([z.string(), z.array(z.string())]).optional(),
created_at: createOperatorMap().optional(),
updated_at: createOperatorMap().optional(),
deleted_at: createOperatorMap().optional(),
})
export type AdminGetRbacPoliciesParamsType = z.infer<
typeof AdminGetRbacPoliciesParams
>
export const AdminGetRbacPoliciesParams = createFindParams({
limit: 50,
offset: 0,
})
.merge(AdminGetRbacPoliciesParamsFields)
.merge(applyAndAndOrOperators(AdminGetRbacPoliciesParamsFields))
export type AdminCreateRbacPolicyType = z.infer<typeof AdminCreateRbacPolicy>
export const AdminCreateRbacPolicy = z
.object({
key: z.string(),
resource: z.string(),
operation: z.string(),
name: z.string().nullish(),
description: z.string().nullish(),
metadata: z.record(z.unknown()).nullish(),
})
.strict()
export type AdminUpdateRbacPolicyType = z.infer<typeof AdminUpdateRbacPolicy>
export const AdminUpdateRbacPolicy = z
.object({
key: z.string().optional(),
resource: z.string().optional(),
operation: z.string().optional(),
name: z.string().nullish(),
description: z.string().nullish(),
metadata: z.record(z.unknown()).nullish(),
})
.strict()

View File

@@ -0,0 +1,91 @@
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,
})
}

View File

@@ -0,0 +1,64 @@
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: [],
},
]

View File

@@ -0,0 +1,20 @@
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,
}

View File

@@ -0,0 +1,50 @@
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 })
}

View File

@@ -0,0 +1,52 @@
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()

View File

@@ -0,0 +1,91 @@
import {
deleteRbacRolesWorkflow,
updateRbacRolesWorkflow,
} from "@medusajs/core-flows"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import {
ContainerRegistrationKeys,
MedusaError,
} from "@medusajs/framework/utils"
import { AdminUpdateRbacRoleType } from "../validators"
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
const { data: roles } = await query.graph({
entity: "rbac_role",
filters: { id: req.params.id },
fields: req.queryConfig.fields,
})
const role = roles[0]
if (!role) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Role with id: ${req.params.id} not found`
)
}
res.status(200).json({ role })
}
export const POST = async (
req: AuthenticatedMedusaRequest<AdminUpdateRbacRoleType>,
res: MedusaResponse
) => {
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
const { data: existing } = await query.graph({
entity: "rbac_role",
filters: { id: req.params.id },
fields: ["id"],
})
const existingRole = existing[0]
if (!existingRole) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Role with id "${req.params.id}" not found`
)
}
const { result } = await updateRbacRolesWorkflow(req.scope).run({
input: {
selector: { id: req.params.id },
update: req.validatedBody,
},
})
const { data: roles } = await query.graph({
entity: "rbac_role",
filters: { id: result[0].id },
fields: req.queryConfig.fields,
})
const role = roles[0]
res.status(200).json({ role })
}
export const DELETE = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const id = req.params.id
await deleteRbacRolesWorkflow(req.scope).run({
input: { ids: [id] },
})
res.status(200).json({
id,
object: "rbac_role",
deleted: true,
})
}

View File

@@ -0,0 +1 @@
export {}

View File

@@ -0,0 +1,64 @@
import * as QueryConfig from "./query-config"
import {
validateAndTransformBody,
validateAndTransformQuery,
} from "@medusajs/framework"
import { MiddlewareRoute } from "@medusajs/framework/http"
import {
AdminCreateRbacRole,
AdminGetRbacRoleParams,
AdminGetRbacRolesParams,
AdminUpdateRbacRole,
} from "./validators"
export const adminRbacRoleRoutesMiddlewares: MiddlewareRoute[] = [
{
method: ["GET"],
matcher: "/admin/rbac/roles",
middlewares: [
validateAndTransformQuery(
AdminGetRbacRolesParams,
QueryConfig.listTransformQueryConfig
),
],
},
{
method: ["GET"],
matcher: "/admin/rbac/roles/:id",
middlewares: [
validateAndTransformQuery(
AdminGetRbacRoleParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
{
method: ["POST"],
matcher: "/admin/rbac/roles",
middlewares: [
validateAndTransformBody(AdminCreateRbacRole),
validateAndTransformQuery(
AdminGetRbacRoleParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
{
method: ["POST"],
matcher: "/admin/rbac/roles/:id",
middlewares: [
validateAndTransformBody(AdminUpdateRbacRole),
validateAndTransformQuery(
AdminGetRbacRoleParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
{
method: ["DELETE"],
matcher: "/admin/rbac/roles/:id",
middlewares: [],
},
]

View File

@@ -0,0 +1,21 @@
export const defaultAdminRbacRoleFields = [
"id",
"name",
"parent_id",
"description",
"metadata",
"created_at",
"updated_at",
"deleted_at",
]
export const retrieveTransformQueryConfig = {
defaults: defaultAdminRbacRoleFields,
isList: false,
}
export const listTransformQueryConfig = {
...retrieveTransformQueryConfig,
defaultLimit: 20,
isList: true,
}

View File

@@ -0,0 +1,50 @@
import { createRbacRolesWorkflow } from "@medusajs/core-flows"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
import { AdminCreateRbacRoleType } from "./validators"
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
const { data: roles, metadata } = await query.graph({
entity: "rbac_role",
fields: req.queryConfig.fields,
filters: req.filterableFields,
pagination: req.queryConfig.pagination,
})
res.status(200).json({
roles,
count: metadata?.count ?? 0,
offset: metadata?.skip ?? 0,
limit: metadata?.take ?? 0,
})
}
export const POST = async (
req: AuthenticatedMedusaRequest<AdminCreateRbacRoleType>,
res: MedusaResponse
) => {
const input = [req.validatedBody]
const { result } = await createRbacRolesWorkflow(req.scope).run({
input: { roles: input },
})
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
const { data: roles } = await query.graph({
entity: "rbac_role",
fields: req.queryConfig.fields,
filters: { id: result[0].id },
})
const role = roles[0]
res.status(200).json({ role })
}

View File

@@ -0,0 +1,50 @@
import { z } from "zod"
import { applyAndAndOrOperators } from "../../../utils/common-validators"
import {
createFindParams,
createOperatorMap,
createSelectParams,
} from "../../../utils/validators"
export type AdminGetRbacRoleParamsType = z.infer<typeof AdminGetRbacRoleParams>
export const AdminGetRbacRoleParams = createSelectParams()
export const AdminGetRbacRolesParamsFields = z.object({
q: z.string().optional(),
id: z.union([z.string(), z.array(z.string())]).optional(),
name: z.union([z.string(), z.array(z.string())]).optional(),
parent_id: z.union([z.string(), z.array(z.string())]).optional(),
created_at: createOperatorMap().optional(),
updated_at: createOperatorMap().optional(),
deleted_at: createOperatorMap().optional(),
})
export type AdminGetRbacRolesParamsType = z.infer<
typeof AdminGetRbacRolesParams
>
export const AdminGetRbacRolesParams = createFindParams({
limit: 50,
offset: 0,
})
.merge(AdminGetRbacRolesParamsFields)
.merge(applyAndAndOrOperators(AdminGetRbacRolesParamsFields))
export type AdminCreateRbacRoleType = z.infer<typeof AdminCreateRbacRole>
export const AdminCreateRbacRole = z
.object({
name: z.string(),
parent_id: z.string().nullish(),
description: z.string().nullish(),
metadata: z.record(z.unknown()).nullish(),
})
.strict()
export type AdminUpdateRbacRoleType = z.infer<typeof AdminUpdateRbacRole>
export const AdminUpdateRbacRole = z
.object({
name: z.string().optional(),
parent_id: z.string().nullish(),
description: z.string().nullish(),
metadata: z.record(z.unknown()).nullish(),
})
.strict()

View File

@@ -27,6 +27,7 @@ import { adminProductTypeRoutesMiddlewares } from "./admin/product-types/middlew
import { adminProductVariantRoutesMiddlewares } from "./admin/product-variants/middlewares"
import { adminProductRoutesMiddlewares } from "./admin/products/middlewares"
import { adminPromotionRoutesMiddlewares } from "./admin/promotions/middlewares"
import { adminRbacRoutesMiddlewares } from "./admin/rbac/middlewares"
import { adminRefundReasonsRoutesMiddlewares } from "./admin/refund-reasons/middlewares"
import { adminRegionRoutesMiddlewares } from "./admin/regions/middlewares"
import { adminReservationRoutesMiddlewares } from "./admin/reservations/middlewares"
@@ -92,6 +93,7 @@ export default defineMiddlewares([
...adminReturnRoutesMiddlewares,
...storeRegionRoutesMiddlewares,
...adminRegionRoutesMiddlewares,
...adminRbacRoutesMiddlewares,
...adminReturnRoutesMiddlewares,
...adminUserRoutesMiddlewares,
...adminInviteRoutesMiddlewares,

6
packages/modules/rbac/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
/dist
node_modules
.DS_store
.env*
.env
*.sql

View File

@@ -0,0 +1,10 @@
const defineJestConfig = require("../../../define_jest_config")
module.exports = defineJestConfig({
moduleNameMapper: {
"^@models": "<rootDir>/src/models",
"^@services": "<rootDir>/src/services",
"^@repositories": "<rootDir>/src/repositories",
"^@types": "<rootDir>/src/types",
"^@utils": "<rootDir>/src/utils",
},
})

View File

@@ -0,0 +1,6 @@
import { defineMikroOrmCliConfig } from "@medusajs/framework/utils"
import * as entities from "./src/models"
export default defineMikroOrmCliConfig("rbac", {
entities: Object.values(entities),
})

View File

@@ -0,0 +1,45 @@
{
"name": "@medusajs/rbac",
"version": "2.12.4",
"description": "Medusa RBAC module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"!dist/**/__tests__",
"!dist/**/__mocks__",
"!dist/**/__fixtures__"
],
"engines": {
"node": ">=20"
},
"repository": {
"type": "git",
"url": "https://github.com/medusajs/medusa",
"directory": "packages/modules/rbac"
},
"publishConfig": {
"access": "public"
},
"author": "Medusa",
"license": "MIT",
"scripts": {
"watch": "yarn run -T tsc --build --watch",
"watch:test": "yarn run -T tsc --build tsconfig.spec.json --watch",
"resolve:aliases": "yarn run -T tsc --showConfig -p tsconfig.json > tsconfig.resolved.json && yarn run -T tsc-alias -p tsconfig.resolved.json && yarn run -T rimraf tsconfig.resolved.json",
"build": "yarn run -T rimraf dist && yarn run -T tsc --build && npm run resolve:aliases",
"test": "../../../node_modules/.bin/jest --passWithNoTests --bail --forceExit --testPathPattern=src",
"test:integration": "../../../node_modules/.bin/jest --passWithNoTests --forceExit --testPathPattern=\"integration-tests/__tests__/.*\\.ts\"",
"migration:initial": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts MIKRO_ORM_ALLOW_GLOBAL_CLI=true medusa-mikro-orm migration:create --initial",
"migration:create": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts MIKRO_ORM_ALLOW_GLOBAL_CLI=true medusa-mikro-orm migration:create",
"migration:up": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts MIKRO_ORM_ALLOW_GLOBAL_CLI=true medusa-mikro-orm migration:up",
"orm:cache:clear": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts MIKRO_ORM_ALLOW_GLOBAL_CLI=true medusa-mikro-orm cache:clear"
},
"devDependencies": {
"@medusajs/framework": "2.12.4",
"@medusajs/test-utils": "2.12.4"
},
"peerDependencies": {
"@medusajs/framework": "2.12.4"
}
}

View File

@@ -0,0 +1,6 @@
import { Module } from "@medusajs/framework/utils"
import { RbacModuleService } from "@services"
export default Module("rbac", {
service: RbacModuleService,
})

View File

@@ -0,0 +1,568 @@
{
"namespaces": [
"public"
],
"name": "public",
"tables": [
{
"columns": {
"id": {
"name": "id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"key": {
"name": "key",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"resource": {
"name": "resource",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"operation": {
"name": "operation",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"name": {
"name": "name",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"description": {
"name": "description",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"metadata": {
"name": "metadata",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "json"
},
"created_at": {
"name": "created_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"updated_at": {
"name": "updated_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"deleted_at": {
"name": "deleted_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"length": 6,
"mappedType": "datetime"
}
},
"name": "rbac_policy",
"schema": "public",
"indexes": [
{
"keyName": "IDX_rbac_policy_deleted_at",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_rbac_policy_deleted_at\" ON \"rbac_policy\" (\"deleted_at\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_rbac_policy_key_unique",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_rbac_policy_key_unique\" ON \"rbac_policy\" (\"key\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_rbac_policy_resource",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_rbac_policy_resource\" ON \"rbac_policy\" (\"resource\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_rbac_policy_operation",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_rbac_policy_operation\" ON \"rbac_policy\" (\"operation\") WHERE deleted_at IS NULL"
},
{
"keyName": "rbac_policy_pkey",
"columnNames": [
"id"
],
"composite": false,
"constraint": true,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {},
"nativeEnums": {}
},
{
"columns": {
"id": {
"name": "id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"name": {
"name": "name",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"description": {
"name": "description",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"metadata": {
"name": "metadata",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "json"
},
"created_at": {
"name": "created_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"updated_at": {
"name": "updated_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"deleted_at": {
"name": "deleted_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"length": 6,
"mappedType": "datetime"
}
},
"name": "rbac_role",
"schema": "public",
"indexes": [
{
"keyName": "IDX_rbac_role_deleted_at",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_rbac_role_deleted_at\" ON \"rbac_role\" (\"deleted_at\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_rbac_role_name_unique",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_rbac_role_name_unique\" ON \"rbac_role\" (\"name\") WHERE deleted_at IS NULL"
},
{
"keyName": "rbac_role_pkey",
"columnNames": [
"id"
],
"composite": false,
"constraint": true,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {},
"nativeEnums": {}
},
{
"columns": {
"id": {
"name": "id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"role_id": {
"name": "role_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"inherited_role_id": {
"name": "inherited_role_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"metadata": {
"name": "metadata",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "json"
},
"created_at": {
"name": "created_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"updated_at": {
"name": "updated_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"deleted_at": {
"name": "deleted_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"length": 6,
"mappedType": "datetime"
}
},
"name": "rbac_role_inheritance",
"schema": "public",
"indexes": [
{
"keyName": "IDX_rbac_role_inheritance_role_id",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_rbac_role_inheritance_role_id\" ON \"rbac_role_inheritance\" (\"role_id\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_rbac_role_inheritance_inherited_role_id",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_rbac_role_inheritance_inherited_role_id\" ON \"rbac_role_inheritance\" (\"inherited_role_id\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_rbac_role_inheritance_deleted_at",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_rbac_role_inheritance_deleted_at\" ON \"rbac_role_inheritance\" (\"deleted_at\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_rbac_role_inheritance_role_id_inherited_role_id_unique",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_rbac_role_inheritance_role_id_inherited_role_id_unique\" ON \"rbac_role_inheritance\" (\"role_id\", \"inherited_role_id\") WHERE deleted_at IS NULL"
},
{
"keyName": "rbac_role_inheritance_pkey",
"columnNames": [
"id"
],
"composite": false,
"constraint": true,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {
"rbac_role_inheritance_role_id_foreign": {
"constraintName": "rbac_role_inheritance_role_id_foreign",
"columnNames": [
"role_id"
],
"localTableName": "public.rbac_role_inheritance",
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.rbac_role",
"updateRule": "cascade"
},
"rbac_role_inheritance_inherited_role_id_foreign": {
"constraintName": "rbac_role_inheritance_inherited_role_id_foreign",
"columnNames": [
"inherited_role_id"
],
"localTableName": "public.rbac_role_inheritance",
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.rbac_role",
"updateRule": "cascade"
}
},
"nativeEnums": {}
},
{
"columns": {
"id": {
"name": "id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"role_id": {
"name": "role_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"scope_id": {
"name": "scope_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"metadata": {
"name": "metadata",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "json"
},
"created_at": {
"name": "created_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"updated_at": {
"name": "updated_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"deleted_at": {
"name": "deleted_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"length": 6,
"mappedType": "datetime"
}
},
"name": "rbac_role_policy",
"schema": "public",
"indexes": [
{
"keyName": "IDX_rbac_role_policy_role_id",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"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",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_rbac_role_policy_scope_id\" ON \"rbac_role_policy\" (\"scope_id\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_rbac_role_policy_deleted_at",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"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",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_rbac_role_policy_role_id_scope_id_unique\" ON \"rbac_role_policy\" (\"role_id\", \"scope_id\") WHERE deleted_at IS NULL"
},
{
"keyName": "rbac_role_policy_pkey",
"columnNames": [
"id"
],
"composite": false,
"constraint": true,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {
"rbac_role_policy_role_id_foreign": {
"constraintName": "rbac_role_policy_role_id_foreign",
"columnNames": [
"role_id"
],
"localTableName": "public.rbac_role_policy",
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.rbac_role",
"updateRule": "cascade"
},
"rbac_role_policy_scope_id_foreign": {
"constraintName": "rbac_role_policy_scope_id_foreign",
"columnNames": [
"scope_id"
],
"localTableName": "public.rbac_role_policy",
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.rbac_policy",
"updateRule": "cascade"
}
},
"nativeEnums": {}
}
],
"nativeEnums": {}
}

View File

@@ -0,0 +1,39 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20251215113723 extends Migration {
override async up(): Promise<void> {
this.addSql(`alter table if exists "rbac_role_policy" drop constraint if exists "rbac_role_policy_role_id_scope_id_unique";`);
this.addSql(`alter table if exists "rbac_role_inheritance" drop constraint if exists "rbac_role_inheritance_role_id_inherited_role_id_unique";`);
this.addSql(`alter table if exists "rbac_role" drop constraint if exists "rbac_role_name_unique";`);
this.addSql(`alter table if exists "rbac_policy" drop constraint if exists "rbac_policy_key_unique";`);
this.addSql(`create table if not exists "rbac_policy" ("id" text not null, "key" text not null, "resource" text not null, "operation" text not null, "name" text null, "description" text null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "rbac_policy_pkey" primary key ("id"));`);
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_rbac_policy_deleted_at" ON "rbac_policy" ("deleted_at") WHERE deleted_at IS NULL;`);
this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_rbac_policy_key_unique" ON "rbac_policy" ("key") WHERE deleted_at IS NULL;`);
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_rbac_policy_resource" ON "rbac_policy" ("resource") WHERE deleted_at IS NULL;`);
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_rbac_policy_operation" ON "rbac_policy" ("operation") WHERE deleted_at IS NULL;`);
this.addSql(`create table if not exists "rbac_role" ("id" text not null, "name" text not 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_role_pkey" primary key ("id"));`);
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_rbac_role_deleted_at" ON "rbac_role" ("deleted_at") WHERE deleted_at IS NULL;`);
this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_rbac_role_name_unique" ON "rbac_role" ("name") WHERE deleted_at IS NULL;`);
this.addSql(`create table if not exists "rbac_role_inheritance" ("id" text not null, "role_id" text not null, "inherited_role_id" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "rbac_role_inheritance_pkey" primary key ("id"));`);
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_rbac_role_inheritance_role_id" ON "rbac_role_inheritance" ("role_id") WHERE deleted_at IS NULL;`);
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_rbac_role_inheritance_inherited_role_id" ON "rbac_role_inheritance" ("inherited_role_id") WHERE deleted_at IS NULL;`);
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_rbac_role_inheritance_deleted_at" ON "rbac_role_inheritance" ("deleted_at") WHERE deleted_at IS NULL;`);
this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_rbac_role_inheritance_role_id_inherited_role_id_unique" ON "rbac_role_inheritance" ("role_id", "inherited_role_id") WHERE deleted_at IS NULL;`);
this.addSql(`create table if not exists "rbac_role_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 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_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(`alter table if exists "rbac_role_inheritance" add constraint "rbac_role_inheritance_role_id_foreign" foreign key ("role_id") references "rbac_role" ("id") on update cascade;`);
this.addSql(`alter table if exists "rbac_role_inheritance" add constraint "rbac_role_inheritance_inherited_role_id_foreign" foreign key ("inherited_role_id") references "rbac_role" ("id") on update cascade;`);
this.addSql(`alter table if exists "rbac_role_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;`);
}
}

View File

@@ -0,0 +1,4 @@
export { default as RbacPolicy } from "./rbac-policy"
export { default as RbacRole } from "./rbac-role"
export { default as RbacRoleInheritance } from "./rbac-role-inheritance"
export { default as RbacRolePolicy } from "./rbac-role-policy"

View File

@@ -0,0 +1,29 @@
import { model } from "@medusajs/framework/utils"
const RbacPolicy = model
.define("rbac_policy", {
id: model.id({ prefix: "rpol" }).primaryKey(),
key: model.text().searchable(),
resource: model.text().searchable(),
operation: model.text().searchable(),
name: model.text().searchable().nullable(),
description: model.text().nullable(),
metadata: model.json().nullable(),
})
.indexes([
{
on: ["key"],
unique: true,
where: "deleted_at IS NULL",
},
{
on: ["resource"],
where: "deleted_at IS NULL",
},
{
on: ["operation"],
where: "deleted_at IS NULL",
},
])
export default RbacPolicy

View File

@@ -0,0 +1,29 @@
import { model } from "@medusajs/framework/utils"
import RbacRole from "./rbac-role"
const RbacRoleInheritance = model
.define("rbac_role_inheritance", {
id: model.id({ prefix: "rlin" }).primaryKey(),
role: model.belongsTo(() => RbacRole, { mappedBy: "inherited_roles" }),
inherited_role: model.belongsTo(() => RbacRole, {
mappedBy: "inheritedBy",
}),
metadata: model.json().nullable(),
})
.indexes([
{
on: ["role_id"],
where: "deleted_at IS NULL",
},
{
on: ["inherited_role_id"],
where: "deleted_at IS NULL",
},
{
on: ["role_id", "inherited_role_id"],
unique: true,
where: "deleted_at IS NULL",
},
])
export default RbacRoleInheritance

View File

@@ -0,0 +1,28 @@
import { model } from "@medusajs/framework/utils"
import RbacPolicy from "./rbac-policy"
import RbacRole from "./rbac-role"
const RbacRolePolicy = model
.define("rbac_role_policy", {
id: model.id({ prefix: "rlpl" }).primaryKey(),
role: model.belongsTo(() => RbacRole),
scope: model.belongsTo(() => RbacPolicy),
metadata: model.json().nullable(),
})
.indexes([
{
on: ["role_id"],
where: "deleted_at IS NULL",
},
{
on: ["scope_id"],
where: "deleted_at IS NULL",
},
{
on: ["role_id", "scope_id"],
unique: true,
where: "deleted_at IS NULL",
},
])
export default RbacRolePolicy

View File

@@ -0,0 +1,18 @@
import { model } from "@medusajs/framework/utils"
const RbacRole = model
.define("rbac_role", {
id: model.id({ prefix: "role" }).primaryKey(),
name: model.text().searchable(),
description: model.text().nullable(),
metadata: model.json().nullable(),
})
.indexes([
{
on: ["name"],
unique: true,
where: "deleted_at IS NULL",
},
])
export default RbacRole

View File

@@ -0,0 +1 @@
export * from "./rbac"

View File

@@ -0,0 +1,88 @@
import { SqlEntityManager } from "@medusajs/framework/mikro-orm/postgresql"
import { Context } from "@medusajs/framework/types"
import { MikroOrmBase } from "@medusajs/framework/utils"
export class RbacRepository extends MikroOrmBase {
constructor() {
// @ts-ignore
// eslint-disable-next-line prefer-rest-params
super(...arguments)
}
async listPoliciesForRole(
roleId: string,
sharedContext: Context = {}
): Promise<any[]> {
const policiesByRole = await this.listPoliciesForRoles(
[roleId],
sharedContext
)
return policiesByRole.get(roleId) || []
}
async listPoliciesForRoles(
roleIds: string[],
sharedContext: Context = {}
): Promise<Map<string, any[]>> {
const manager = this.getActiveManager<SqlEntityManager>(sharedContext)
const knex = manager.getKnex()
if (!roleIds?.length) {
return new Map()
}
const placeholders = roleIds.map(() => "?").join(",")
const query = `
WITH RECURSIVE role_hierarchy AS (
SELECT id, name, id as original_role_id
FROM rbac_role
WHERE id IN (${placeholders}) AND deleted_at IS NULL
UNION ALL
SELECT r.id, r.name, rh.original_role_id
FROM rbac_role r
INNER JOIN rbac_role_inheritance ri ON ri.inherited_role_id = r.id
INNER JOIN role_hierarchy rh ON rh.id = ri.role_id
WHERE r.deleted_at IS NULL AND ri.deleted_at IS NULL
)
SELECT DISTINCT
rh.original_role_id,
p.id,
p.key,
p.resource,
p.operation,
p.name,
p.description,
p.metadata,
p.created_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
FROM rbac_policy p
INNER JOIN rbac_role_policy rp ON rp.scope_id = p.id
INNER JOIN role_hierarchy rh ON rh.id = rp.role_id
WHERE p.deleted_at IS NULL AND rp.deleted_at IS NULL
ORDER BY rh.original_role_id, p.resource, p.operation, p.key
`
const result = await knex.raw(query, roleIds)
const rows = result.rows || []
// Group policies by role_id
const policiesByRole = new Map<string, any[]>()
for (const row of rows) {
const roleId = row.original_role_id
delete row.original_role_id
if (!policiesByRole.has(roleId)) {
policiesByRole.set(roleId, [])
}
policiesByRole.get(roleId)!.push(row)
}
return policiesByRole
}
}

View File

@@ -0,0 +1 @@
export { default as RbacModuleService } from "./rbac-module-service"

View File

@@ -0,0 +1,105 @@
import { Context, FindConfig } from "@medusajs/framework/types"
import {
InjectManager,
MedusaContext,
MedusaService,
} from "@medusajs/framework/utils"
import {
RbacPolicy,
RbacRole,
RbacRoleInheritance,
RbacRolePolicy,
} from "@models"
import { RbacRepository } from "../repositories"
type InjectedDependencies = {
rbacRepository: RbacRepository
}
export default class RbacModuleService extends MedusaService<{
RbacRole: { dto: any }
RbacPolicy: { dto: any }
RbacRoleInheritance: { dto: any }
RbacRolePolicy: { dto: any }
}>({
RbacRole,
RbacPolicy,
RbacRoleInheritance,
RbacRolePolicy,
}) {
protected readonly rbacRepository_: RbacRepository
constructor({ rbacRepository }: InjectedDependencies) {
// @ts-ignore
super(...arguments)
this.rbacRepository_ = rbacRepository
}
@InjectManager()
async listPoliciesForRole(
roleId: string,
@MedusaContext() sharedContext: Context = {}
): Promise<any[]> {
return await this.rbacRepository_.listPoliciesForRole(roleId, sharedContext)
}
@InjectManager()
// @ts-expect-error
async listRbacRoles(
filters: any = {},
config: FindConfig<any> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<any[]> {
const roles = await super.listRbacRoles(filters, config, 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
}
@InjectManager()
// @ts-expect-error
async listAndCountRbacRoles(
filters: any = {},
config: FindConfig<any> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[any[], number]> {
const [roles, count] = await super.listAndCountRbacRoles(
filters,
config,
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, count]
}
}

View File

@@ -0,0 +1 @@
export type RbacModuleOptions = Record<string, unknown>

View File

@@ -0,0 +1,12 @@
{
"extends": "../../../_tsconfig.base.json",
"compilerOptions": {
"paths": {
"@models": ["./src/models"],
"@services": ["./src/services"],
"@repositories": ["./src/repositories"],
"@types": ["./src/types"],
"@utils": ["./src/utils"]
}
}
}

View File

@@ -4013,6 +4013,17 @@ __metadata:
languageName: unknown
linkType: soft
"@medusajs/rbac@workspace:packages/modules/rbac":
version: 0.0.0-use.local
resolution: "@medusajs/rbac@workspace:packages/modules/rbac"
dependencies:
"@medusajs/framework": 2.12.4
"@medusajs/test-utils": 2.12.4
peerDependencies:
"@medusajs/framework": 2.12.4
languageName: unknown
linkType: soft
"@medusajs/region@2.12.4, @medusajs/region@workspace:^, @medusajs/region@workspace:packages/modules/region":
version: 0.0.0-use.local
resolution: "@medusajs/region@workspace:packages/modules/region"