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

This commit is contained in:
Carlos R. L. Rodrigues
2026-01-07 10:40:15 -03:00
committed by GitHub
parent 7161cf1903
commit b2245cc672
72 changed files with 2746 additions and 703 deletions

View File

@@ -1,5 +1,6 @@
export * from "./cart-payment-collection"
export * from "./cart-promotion"
export * from "./customer-account-holder"
export * from "./fulfillment-provider-location"
export * from "./fulfillment-set-location"
export * from "./order-cart"
@@ -8,6 +9,7 @@ export * from "./order-payment-collection"
export * from "./order-promotion"
export * from "./order-return-fulfillment"
export * from "./product-sales-channel"
export * from "./product-shipping-profile"
export * from "./product-variant-inventory-item"
export * from "./product-variant-price-set"
export * from "./publishable-api-key-sales-channel"
@@ -15,5 +17,4 @@ export * from "./readonly"
export * from "./region-payment-provider"
export * from "./sales-channel-location"
export * from "./shipping-option-price-set"
export * from "./product-shipping-profile"
export * from "./customer-account-holder"
export * from "./user-rbac-role"

View File

@@ -0,0 +1,74 @@
import { ModuleJoinerConfig } from "@medusajs/framework/types"
import { LINKS, Modules } from "@medusajs/framework/utils"
export const UserRbacRole: ModuleJoinerConfig = {
serviceName: LINKS.UserRbacRole,
isLink: true,
databaseConfig: {
tableName: "user_rbac_role",
idPrefix: "userrole",
},
alias: [
{
name: "user_rbac_role",
},
{
name: "user_rbac_roles",
},
],
primaryKeys: ["id", "user_id", "rbac_role_id"],
relationships: [
{
serviceName: Modules.USER,
entity: "User",
primaryKey: "id",
foreignKey: "user_id",
alias: "user",
args: {
methodSuffix: "Users",
},
hasMany: true,
},
{
serviceName: Modules.RBAC,
entity: "RbacRole",
primaryKey: "id",
foreignKey: "rbac_role_id",
alias: "rbac_role",
args: {
methodSuffix: "RbacRoles",
},
hasMany: true,
},
],
extends: [
{
serviceName: Modules.USER,
entity: "User",
fieldAlias: {
rbac_roles: {
path: "rbac_roles_link.rbac_role",
isList: true,
},
},
relationship: {
serviceName: LINKS.UserRbacRole,
primaryKey: "user_id",
foreignKey: "id",
alias: "rbac_roles_link",
isList: true,
},
},
{
serviceName: Modules.RBAC,
entity: "RbacRole",
relationship: {
serviceName: LINKS.UserRbacRole,
primaryKey: "rbac_role_id",
foreignKey: "id",
alias: "users_link",
isList: true,
},
},
],
}

View File

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

View File

@@ -1,7 +1,5 @@
{
"namespaces": [
"public"
],
"namespaces": ["public"],
"name": "public",
"tables": [
{
@@ -143,9 +141,7 @@
},
{
"keyName": "rbac_policy_pkey",
"columnNames": [
"id"
],
"columnNames": ["id"],
"composite": false,
"constraint": true,
"primary": true,
@@ -250,9 +246,7 @@
},
{
"keyName": "rbac_role_pkey",
"columnNames": [
"id"
],
"columnNames": ["id"],
"composite": false,
"constraint": true,
"primary": true,
@@ -283,8 +277,8 @@
"nullable": false,
"mappedType": "text"
},
"inherited_role_id": {
"name": "inherited_role_id",
"parent_id": {
"name": "parent_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
@@ -334,50 +328,48 @@
"mappedType": "datetime"
}
},
"name": "rbac_role_inheritance",
"name": "rbac_role_parent",
"schema": "public",
"indexes": [
{
"keyName": "IDX_rbac_role_inheritance_role_id",
"keyName": "IDX_rbac_role_parent_role_id",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_rbac_role_inheritance_role_id\" ON \"rbac_role_inheritance\" (\"role_id\") WHERE deleted_at IS NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_rbac_role_parent_role_id\" ON \"rbac_role_parent\" (\"role_id\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_rbac_role_inheritance_inherited_role_id",
"keyName": "IDX_rbac_role_parent_parent_id",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_rbac_role_inheritance_inherited_role_id\" ON \"rbac_role_inheritance\" (\"inherited_role_id\") WHERE deleted_at IS NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_rbac_role_parent_parent_id\" ON \"rbac_role_parent\" (\"parent_id\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_rbac_role_inheritance_deleted_at",
"keyName": "IDX_rbac_role_parent_deleted_at",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_rbac_role_inheritance_deleted_at\" ON \"rbac_role_inheritance\" (\"deleted_at\") WHERE deleted_at IS NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_rbac_role_parent_deleted_at\" ON \"rbac_role_parent\" (\"deleted_at\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_rbac_role_inheritance_role_id_inherited_role_id_unique",
"keyName": "IDX_rbac_role_parent_role_id_parent_id_unique",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_rbac_role_inheritance_role_id_inherited_role_id_unique\" ON \"rbac_role_inheritance\" (\"role_id\", \"inherited_role_id\") WHERE deleted_at IS NULL"
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_rbac_role_parent_role_id_parent_id_unique\" ON \"rbac_role_parent\" (\"role_id\", \"parent_id\") WHERE deleted_at IS NULL"
},
{
"keyName": "rbac_role_inheritance_pkey",
"columnNames": [
"id"
],
"keyName": "rbac_role_parent_pkey",
"columnNames": ["id"],
"composite": false,
"constraint": true,
"primary": true,
@@ -386,27 +378,19 @@
],
"checks": [],
"foreignKeys": {
"rbac_role_inheritance_role_id_foreign": {
"constraintName": "rbac_role_inheritance_role_id_foreign",
"columnNames": [
"role_id"
],
"localTableName": "public.rbac_role_inheritance",
"referencedColumnNames": [
"id"
],
"rbac_role_parent_role_id_foreign": {
"constraintName": "rbac_role_parent_role_id_foreign",
"columnNames": ["role_id"],
"localTableName": "public.rbac_role_parent",
"referencedColumnNames": ["id"],
"referencedTableName": "public.rbac_role",
"updateRule": "cascade"
},
"rbac_role_inheritance_inherited_role_id_foreign": {
"constraintName": "rbac_role_inheritance_inherited_role_id_foreign",
"columnNames": [
"inherited_role_id"
],
"localTableName": "public.rbac_role_inheritance",
"referencedColumnNames": [
"id"
],
"rbac_role_parent_parent_id_foreign": {
"constraintName": "rbac_role_parent_parent_id_foreign",
"columnNames": ["parent_id"],
"localTableName": "public.rbac_role_parent",
"referencedColumnNames": ["id"],
"referencedTableName": "public.rbac_role",
"updateRule": "cascade"
}
@@ -433,8 +417,8 @@
"nullable": false,
"mappedType": "text"
},
"scope_id": {
"name": "scope_id",
"policy_id": {
"name": "policy_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
@@ -497,13 +481,13 @@
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_rbac_role_policy_role_id\" ON \"rbac_role_policy\" (\"role_id\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_rbac_role_policy_scope_id",
"keyName": "IDX_rbac_role_policy_policy_id",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_rbac_role_policy_scope_id\" ON \"rbac_role_policy\" (\"scope_id\") WHERE deleted_at IS NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_rbac_role_policy_policy_id\" ON \"rbac_role_policy\" (\"policy_id\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_rbac_role_policy_deleted_at",
@@ -515,19 +499,17 @@
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_rbac_role_policy_deleted_at\" ON \"rbac_role_policy\" (\"deleted_at\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_rbac_role_policy_role_id_scope_id_unique",
"keyName": "IDX_rbac_role_policy_role_id_policy_id_unique",
"columnNames": [],
"composite": false,
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_rbac_role_policy_role_id_scope_id_unique\" ON \"rbac_role_policy\" (\"role_id\", \"scope_id\") WHERE deleted_at IS NULL"
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_rbac_role_policy_role_id_policy_id_unique\" ON \"rbac_role_policy\" (\"role_id\", \"policy_id\") WHERE deleted_at IS NULL"
},
{
"keyName": "rbac_role_policy_pkey",
"columnNames": [
"id"
],
"columnNames": ["id"],
"composite": false,
"constraint": true,
"primary": true,
@@ -538,25 +520,17 @@
"foreignKeys": {
"rbac_role_policy_role_id_foreign": {
"constraintName": "rbac_role_policy_role_id_foreign",
"columnNames": [
"role_id"
],
"columnNames": ["role_id"],
"localTableName": "public.rbac_role_policy",
"referencedColumnNames": [
"id"
],
"referencedColumnNames": ["id"],
"referencedTableName": "public.rbac_role",
"updateRule": "cascade"
},
"rbac_role_policy_scope_id_foreign": {
"constraintName": "rbac_role_policy_scope_id_foreign",
"columnNames": [
"scope_id"
],
"rbac_role_policy_policy_id_foreign": {
"constraintName": "rbac_role_policy_policy_id_foreign",
"columnNames": ["policy_id"],
"localTableName": "public.rbac_role_policy",
"referencedColumnNames": [
"id"
],
"referencedColumnNames": ["id"],
"referencedTableName": "public.rbac_policy",
"updateRule": "cascade"
}

View File

@@ -1,10 +1,10 @@
import { Migration } from '@mikro-orm/migrations';
import { Migration } from "@medusajs/framework/mikro-orm/migrations";
export class Migration20251215113723 extends Migration {
export class Migration20251219163509 extends Migration {
override async up(): Promise<void> {
this.addSql(`alter table if exists "rbac_role_policy" drop constraint if exists "rbac_role_policy_role_id_scope_id_unique";`);
this.addSql(`alter table if exists "rbac_role_inheritance" drop constraint if exists "rbac_role_inheritance_role_id_inherited_role_id_unique";`);
this.addSql(`alter table if exists "rbac_role_policy" drop constraint if exists "rbac_role_policy_role_id_policy_id_unique";`);
this.addSql(`alter table if exists "rbac_role_parent" drop constraint if exists "rbac_role_parent_role_id_parent_id_unique";`);
this.addSql(`alter table if exists "rbac_role" drop constraint if exists "rbac_role_name_unique";`);
this.addSql(`alter table if exists "rbac_policy" drop constraint if exists "rbac_policy_key_unique";`);
this.addSql(`create table if not exists "rbac_policy" ("id" text not null, "key" text not null, "resource" text not null, "operation" text not null, "name" text null, "description" text null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "rbac_policy_pkey" primary key ("id"));`);
@@ -17,23 +17,23 @@ export class Migration20251215113723 extends Migration {
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_rbac_role_deleted_at" ON "rbac_role" ("deleted_at") WHERE deleted_at IS NULL;`);
this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_rbac_role_name_unique" ON "rbac_role" ("name") WHERE deleted_at IS NULL;`);
this.addSql(`create table if not exists "rbac_role_inheritance" ("id" text not null, "role_id" text not null, "inherited_role_id" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "rbac_role_inheritance_pkey" primary key ("id"));`);
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_rbac_role_inheritance_role_id" ON "rbac_role_inheritance" ("role_id") WHERE deleted_at IS NULL;`);
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_rbac_role_inheritance_inherited_role_id" ON "rbac_role_inheritance" ("inherited_role_id") WHERE deleted_at IS NULL;`);
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_rbac_role_inheritance_deleted_at" ON "rbac_role_inheritance" ("deleted_at") WHERE deleted_at IS NULL;`);
this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_rbac_role_inheritance_role_id_inherited_role_id_unique" ON "rbac_role_inheritance" ("role_id", "inherited_role_id") WHERE deleted_at IS NULL;`);
this.addSql(`create table if not exists "rbac_role_parent" ("id" text not null, "role_id" text not null, "parent_id" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "rbac_role_parent_pkey" primary key ("id"));`);
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_rbac_role_parent_role_id" ON "rbac_role_parent" ("role_id") WHERE deleted_at IS NULL;`);
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_rbac_role_parent_parent_id" ON "rbac_role_parent" ("parent_id") WHERE deleted_at IS NULL;`);
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_rbac_role_parent_deleted_at" ON "rbac_role_parent" ("deleted_at") WHERE deleted_at IS NULL;`);
this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_rbac_role_parent_role_id_parent_id_unique" ON "rbac_role_parent" ("role_id", "parent_id") WHERE deleted_at IS NULL;`);
this.addSql(`create table if not exists "rbac_role_policy" ("id" text not null, "role_id" text not null, "scope_id" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "rbac_role_policy_pkey" primary key ("id"));`);
this.addSql(`create table if not exists "rbac_role_policy" ("id" text not null, "role_id" text not null, "policy_id" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "rbac_role_policy_pkey" primary key ("id"));`);
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_rbac_role_policy_role_id" ON "rbac_role_policy" ("role_id") WHERE deleted_at IS NULL;`);
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_rbac_role_policy_scope_id" ON "rbac_role_policy" ("scope_id") WHERE deleted_at IS NULL;`);
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_rbac_role_policy_policy_id" ON "rbac_role_policy" ("policy_id") WHERE deleted_at IS NULL;`);
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_rbac_role_policy_deleted_at" ON "rbac_role_policy" ("deleted_at") WHERE deleted_at IS NULL;`);
this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_rbac_role_policy_role_id_scope_id_unique" ON "rbac_role_policy" ("role_id", "scope_id") WHERE deleted_at IS NULL;`);
this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_rbac_role_policy_role_id_policy_id_unique" ON "rbac_role_policy" ("role_id", "policy_id") WHERE deleted_at IS NULL;`);
this.addSql(`alter table if exists "rbac_role_inheritance" add constraint "rbac_role_inheritance_role_id_foreign" foreign key ("role_id") references "rbac_role" ("id") on update cascade;`);
this.addSql(`alter table if exists "rbac_role_inheritance" add constraint "rbac_role_inheritance_inherited_role_id_foreign" foreign key ("inherited_role_id") references "rbac_role" ("id") on update cascade;`);
this.addSql(`alter table if exists "rbac_role_parent" add constraint "rbac_role_parent_role_id_foreign" foreign key ("role_id") references "rbac_role" ("id") on update cascade;`);
this.addSql(`alter table if exists "rbac_role_parent" add constraint "rbac_role_parent_parent_id_foreign" foreign key ("parent_id") references "rbac_role" ("id") on update cascade;`);
this.addSql(`alter table if exists "rbac_role_policy" add constraint "rbac_role_policy_role_id_foreign" foreign key ("role_id") references "rbac_role" ("id") on update cascade;`);
this.addSql(`alter table if exists "rbac_role_policy" add constraint "rbac_role_policy_scope_id_foreign" foreign key ("scope_id") references "rbac_policy" ("id") on update cascade;`);
this.addSql(`alter table if exists "rbac_role_policy" add constraint "rbac_role_policy_policy_id_foreign" foreign key ("policy_id") references "rbac_policy" ("id") on update cascade;`);
}
}

View File

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

View File

@@ -0,0 +1,27 @@
import { model } from "@medusajs/framework/utils"
import RbacRole from "./rbac-role"
const RbacRoleParent = model
.define("rbac_role_parent", {
id: model.id({ prefix: "rlin" }).primaryKey(),
role: model.belongsTo(() => RbacRole, { mappedBy: "parents" }),
parent: model.belongsTo(() => RbacRole),
metadata: model.json().nullable(),
})
.indexes([
{
on: ["role_id"],
where: "deleted_at IS NULL",
},
{
on: ["parent_id"],
where: "deleted_at IS NULL",
},
{
on: ["role_id", "parent_id"],
unique: true,
where: "deleted_at IS NULL",
},
])
export default RbacRoleParent

View File

@@ -6,7 +6,7 @@ const RbacRolePolicy = model
.define("rbac_role_policy", {
id: model.id({ prefix: "rlpl" }).primaryKey(),
role: model.belongsTo(() => RbacRole),
scope: model.belongsTo(() => RbacPolicy),
policy: model.belongsTo(() => RbacPolicy),
metadata: model.json().nullable(),
})
.indexes([
@@ -15,11 +15,11 @@ const RbacRolePolicy = model
where: "deleted_at IS NULL",
},
{
on: ["scope_id"],
on: ["policy_id"],
where: "deleted_at IS NULL",
},
{
on: ["role_id", "scope_id"],
on: ["role_id", "policy_id"],
unique: true,
where: "deleted_at IS NULL",
},

View File

@@ -1,4 +1,6 @@
import { model } from "@medusajs/framework/utils"
import RbacRoleParent from "./rbac-role-parent"
import RbacRolePolicy from "./rbac-role-policy"
const RbacRole = model
.define("rbac_role", {
@@ -6,6 +8,12 @@ const RbacRole = model
name: model.text().searchable(),
description: model.text().nullable(),
metadata: model.json().nullable(),
policies: model.hasMany(() => RbacRolePolicy, {
mappedBy: "role",
}),
parents: model.hasMany(() => RbacRoleParent, {
mappedBy: "role",
}),
})
.indexes([
{

View File

@@ -35,17 +35,19 @@ export class RbacRepository extends MikroOrmBase {
const query = `
WITH RECURSIVE role_hierarchy AS (
SELECT id, name, id as original_role_id
SELECT id, name, id as original_role_id, ARRAY[id] as path
FROM rbac_role
WHERE id IN (${placeholders}) AND deleted_at IS NULL
UNION ALL
SELECT r.id, r.name, rh.original_role_id
SELECT r.id, r.name, rh.original_role_id, rh.path || r.id
FROM rbac_role r
INNER JOIN rbac_role_inheritance ri ON ri.inherited_role_id = r.id
INNER JOIN rbac_role_parent ri ON ri.parent_id = r.id
INNER JOIN role_hierarchy rh ON rh.id = ri.role_id
WHERE r.deleted_at IS NULL AND ri.deleted_at IS NULL
WHERE r.deleted_at IS NULL
AND ri.deleted_at IS NULL
AND NOT (r.id = ANY(rh.path))
)
SELECT DISTINCT
rh.original_role_id,
@@ -60,7 +62,7 @@ export class RbacRepository extends MikroOrmBase {
p.updated_at,
CASE WHEN rp.role_id = rh.original_role_id THEN NULL ELSE rp.role_id END as inherited_from_role_id
FROM rbac_policy p
INNER JOIN rbac_role_policy rp ON rp.scope_id = p.id
INNER JOIN rbac_role_policy rp ON rp.policy_id = p.id
INNER JOIN role_hierarchy rh ON rh.id = rp.role_id
WHERE p.deleted_at IS NULL AND rp.deleted_at IS NULL
ORDER BY rh.original_role_id, p.resource, p.operation, p.key
@@ -85,4 +87,40 @@ export class RbacRepository extends MikroOrmBase {
return policiesByRole
}
async checkForCycle(
roleId: string,
parentId: string,
sharedContext: Context = {}
): Promise<boolean> {
const manager = this.getActiveManager<SqlEntityManager>(sharedContext)
const knex = manager.getKnex()
// Check if adding this parent would create a circular dependency
// A cycle exists if role_id is already an ancestor of parent_id
// (i.e., if we traverse up from parent_id, we reach role_id)
const query = `
WITH RECURSIVE role_hierarchy AS (
SELECT id, ARRAY[id] as path
FROM rbac_role
WHERE id = ? AND deleted_at IS NULL
UNION ALL
SELECT r.id, rh.path || r.id
FROM role_hierarchy rh
INNER JOIN rbac_role_parent ri ON ri.role_id = rh.id
INNER JOIN rbac_role r ON r.id = ri.parent_id
WHERE r.deleted_at IS NULL
AND ri.deleted_at IS NULL
AND NOT (r.id = ANY(rh.path))
)
SELECT EXISTS(
SELECT 1 FROM role_hierarchy WHERE id = ?
) as has_cycle
`
const result = await knex.raw(query, [parentId, roleId])
return result.rows[0]?.has_cycle || false
}
}

View File

@@ -1,32 +1,38 @@
import { Context, FindConfig } from "@medusajs/framework/types"
import {
Context,
FilterableRbacRoleProps,
FindConfig,
RbacRoleDTO,
} from "@medusajs/framework/types"
import {
InjectManager,
MedusaContext,
MedusaService,
Policy,
promiseAll,
} from "@medusajs/framework/utils"
import {
RbacPolicy,
RbacRole,
RbacRoleInheritance,
RbacRolePolicy,
} from "@models"
CreateRbacRoleParentDTO,
IRbacModuleService,
RbacRoleParentDTO,
UpdateRbacRoleParentDTO,
} from "@medusajs/types"
import { RbacPolicy, RbacRole, RbacRoleParent, RbacRolePolicy } from "@models"
import { RbacRepository } from "../repositories"
type InjectedDependencies = {
rbacRepository: RbacRepository
}
export default class RbacModuleService extends MedusaService<{
RbacRole: { dto: any }
RbacPolicy: { dto: any }
RbacRoleInheritance: { dto: any }
RbacRolePolicy: { dto: any }
}>({
RbacRole,
RbacPolicy,
RbacRoleInheritance,
RbacRolePolicy,
}) {
export default class RbacModuleService
extends MedusaService({
RbacRole,
RbacPolicy,
RbacRoleParent,
RbacRolePolicy,
})
implements IRbacModuleService
{
protected readonly rbacRepository_: RbacRepository
constructor({ rbacRepository }: InjectedDependencies) {
@@ -35,6 +41,93 @@ export default class RbacModuleService extends MedusaService<{
this.rbacRepository_ = rbacRepository
}
__hooks = {
onApplicationStart: async () => {
this.onApplicationStart()
},
}
async onApplicationStart(): Promise<void> {
await this.syncRegisteredPolicies()
}
@InjectManager()
private async syncRegisteredPolicies(
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
const registeredPolicies = Object.entries(Policy).map(
([name, { resource, operation, description }]) => ({
key: `${resource}:${operation}`,
name,
resource,
operation,
description,
})
)
const registeredKeys = registeredPolicies.map((p) => p.key)
// Fetch all existing policies (including soft-deleted ones)
const existingPolicies = await this.listRbacPolicies(
{},
{ withDeleted: true },
sharedContext
)
const existingPoliciesMap = new Map(existingPolicies.map((p) => [p.key, p]))
const policiesToCreate: any[] = []
const policiesToUpdate: any[] = []
const policiesToRestore: string[] = []
// Process registered policies
for (const registeredPolicy of registeredPolicies) {
const existing = existingPoliciesMap.get(registeredPolicy.key)
const hasChanges =
existing &&
(existing.name !== registeredPolicy.name ||
existing.description !== registeredPolicy.description)
if (!existing) {
policiesToCreate.push(registeredPolicy)
} else if (existing.deleted_at) {
policiesToRestore.push(existing.id)
if (hasChanges) {
policiesToUpdate.push({
id: existing.id,
name: registeredPolicy.name,
description: registeredPolicy.description,
})
}
} else if (hasChanges) {
policiesToUpdate.push({
id: existing.id,
name: registeredPolicy.name,
description: registeredPolicy.description,
})
}
}
const policiesToSoftDelete = existingPolicies
.filter((p) => !p.deleted_at && !registeredKeys.includes(p.key))
.map((p) => p.id)
// First restore any soft-deleted policies
if (policiesToRestore.length > 0) {
await this.restoreRbacPolicies(policiesToRestore, {}, sharedContext)
}
await promiseAll([
policiesToCreate.length > 0 &&
this.createRbacPolicies(policiesToCreate, sharedContext),
policiesToUpdate.length > 0 &&
this.updateRbacPolicies(policiesToUpdate, sharedContext),
policiesToSoftDelete.length > 0 &&
this.softDeleteRbacPolicies(policiesToSoftDelete, {}, sharedContext),
])
}
@InjectManager()
async listPoliciesForRole(
roleId: string,
@@ -46,41 +139,13 @@ export default class RbacModuleService extends MedusaService<{
@InjectManager()
// @ts-expect-error
async listRbacRoles(
filters: any = {},
config: FindConfig<any> = {},
filters: FilterableRbacRoleProps = {},
config: FindConfig<RbacRoleDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<any[]> {
const roles = await super.listRbacRoles(filters, config, sharedContext)
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(
): Promise<RbacRoleDTO[]> {
const roles = await super.listRbacRoles(
filters,
config,
config as any,
sharedContext
)
@@ -100,6 +165,102 @@ export default class RbacModuleService extends MedusaService<{
}
}
return [roles, count]
return roles as unknown as RbacRoleDTO[]
}
@InjectManager()
// @ts-expect-error
async listAndCountRbacRoles(
filters: FilterableRbacRoleProps = {},
config: FindConfig<RbacRoleDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[RbacRoleDTO[], number]> {
const [roles, count] = await super.listAndCountRbacRoles(
filters,
config as any,
sharedContext
)
const shouldIncludePolicies =
config.relations?.includes("policies") ||
config.select?.includes("policies")
if (shouldIncludePolicies && roles.length > 0) {
const roleIds = roles.map((role) => role.id)
const policiesByRole = await this.rbacRepository_.listPoliciesForRoles(
roleIds,
sharedContext
)
for (const role of roles) {
role.policies = policiesByRole.get(role.id) || []
}
}
return [roles as unknown as RbacRoleDTO[], count]
}
@InjectManager()
// @ts-expect-error
async createRbacRoleParents(
data: CreateRbacRoleParentDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<RbacRoleParentDTO[]> {
for (const parent of data) {
const { role_id, parent_id } = parent
if (role_id === parent_id) {
throw new Error(
`Cannot create role parent relationship: a role cannot be its own parent (role_id: ${role_id})`
)
}
const wouldCreateCycle = await this.rbacRepository_.checkForCycle(
role_id,
parent_id,
sharedContext
)
if (wouldCreateCycle) {
throw new Error(
`Cannot create role parent relationship: this would create a circular dependency (role_id: ${role_id}, parent_id: ${parent_id})`
)
}
}
return await super.createRbacRoleParents(data, sharedContext)
}
@InjectManager()
// @ts-expect-error
async updateRbacRoleParents(
data: UpdateRbacRoleParentDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<RbacRoleParentDTO[]> {
for (const parent of data) {
const { role_id, parent_id } = parent
if (parent_id) {
if (role_id === parent_id) {
throw new Error(
`Cannot update role parent relationship: a role cannot be its own parent (role_id: ${role_id})`
)
}
const wouldCreateCycle = await this.rbacRepository_.checkForCycle(
role_id!,
parent_id,
sharedContext
)
if (wouldCreateCycle) {
throw new Error(
`Cannot update role parent relationship: this would create a circular dependency (role_id: ${role_id}, parent_id: ${parent_id})`
)
}
}
}
return await super.updateRbacRoleParents(data, sharedContext)
}
}