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

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"]
}
}
}