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

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

No files matched your search

@@ -1,11 +1,9 @@
import { MiddlewareRoute } from "@medusajs/framework/http"
import { adminRbacPolicyRoutesMiddlewares } from "./policies/middlewares"
import { adminRbacRolePolicyRoutesMiddlewares } from "./role-policies/middlewares"
import { adminRbacRoleRoutesMiddlewares } from "./roles/middlewares"
export const adminRbacRoutesMiddlewares: MiddlewareRoute[] = [
...adminRbacRoleRoutesMiddlewares,
...adminRbacPolicyRoutesMiddlewares,
...adminRbacRolePolicyRoutesMiddlewares,
]
@@ -3,7 +3,12 @@ import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
import {
ContainerRegistrationKeys,
defineFileConfig,
FeatureFlag,
} from "@medusajs/framework/utils"
import RbacFeatureFlag from "../../../../feature-flags/rbac"
import { AdminCreateRbacPolicyType } from "./validators"
export const GET = async (
@@ -48,3 +53,7 @@ export const POST = async (
res.status(200).json({ policy })
}
defineFileConfig({
isDisabled: () => !FeatureFlag.isFeatureEnabled(RbacFeatureFlag.key),
})
@@ -1,91 +0,0 @@
import {
deleteRbacRolePoliciesWorkflow,
updateRbacRolePoliciesWorkflow,
} from "@medusajs/core-flows"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import {
ContainerRegistrationKeys,
MedusaError,
} from "@medusajs/framework/utils"
import { AdminUpdateRbacRolePolicyType } from "../validators"
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
const { data: rolePolicies } = await query.graph({
entity: "rbac_role_policy",
filters: { id: req.params.id },
fields: req.queryConfig.fields,
})
const role_policy = rolePolicies[0]
if (!role_policy) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Role policy with id: ${req.params.id} not found`
)
}
res.status(200).json({ role_policy })
}
export const POST = async (
req: AuthenticatedMedusaRequest<AdminUpdateRbacRolePolicyType>,
res: MedusaResponse
) => {
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
const { data: existing } = await query.graph({
entity: "rbac_role_policy",
filters: { id: req.params.id },
fields: ["id"],
})
const existingRolePolicy = existing[0]
if (!existingRolePolicy) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Role policy with id "${req.params.id}" not found`
)
}
const { result } = await updateRbacRolePoliciesWorkflow(req.scope).run({
input: {
selector: { id: req.params.id },
update: req.validatedBody,
},
})
const { data: rolePolicies } = await query.graph({
entity: "rbac_role_policy",
filters: { id: result[0].id },
fields: req.queryConfig.fields,
})
const role_policy = rolePolicies[0]
res.status(200).json({ role_policy })
}
export const DELETE = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const id = req.params.id
await deleteRbacRolePoliciesWorkflow(req.scope).run({
input: { ids: [id] },
})
res.status(200).json({
id,
object: "rbac_role_policy",
deleted: true,
})
}
@@ -1,64 +0,0 @@
import * as QueryConfig from "./query-config"
import {
validateAndTransformBody,
validateAndTransformQuery,
} from "@medusajs/framework"
import { MiddlewareRoute } from "@medusajs/framework/http"
import {
AdminCreateRbacRolePolicy,
AdminGetRbacRolePoliciesParams,
AdminGetRbacRolePolicyParams,
AdminUpdateRbacRolePolicy,
} from "./validators"
export const adminRbacRolePolicyRoutesMiddlewares: MiddlewareRoute[] = [
{
method: ["GET"],
matcher: "/admin/rbac/role-policies",
middlewares: [
validateAndTransformQuery(
AdminGetRbacRolePoliciesParams,
QueryConfig.listTransformQueryConfig
),
],
},
{
method: ["GET"],
matcher: "/admin/rbac/role-policies/:id",
middlewares: [
validateAndTransformQuery(
AdminGetRbacRolePolicyParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
{
method: ["POST"],
matcher: "/admin/rbac/role-policies",
middlewares: [
validateAndTransformBody(AdminCreateRbacRolePolicy),
validateAndTransformQuery(
AdminGetRbacRolePolicyParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
{
method: ["POST"],
matcher: "/admin/rbac/role-policies/:id",
middlewares: [
validateAndTransformBody(AdminUpdateRbacRolePolicy),
validateAndTransformQuery(
AdminGetRbacRolePolicyParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
{
method: ["DELETE"],
matcher: "/admin/rbac/role-policies/:id",
middlewares: [],
},
]
@@ -1,20 +0,0 @@
export const defaultAdminRbacRolePolicyFields = [
"id",
"role_id",
"scope_id",
"metadata",
"created_at",
"updated_at",
"deleted_at",
]
export const retrieveTransformQueryConfig = {
defaults: defaultAdminRbacRolePolicyFields,
isList: false,
}
export const listTransformQueryConfig = {
...retrieveTransformQueryConfig,
defaultLimit: 20,
isList: true,
}
@@ -1,50 +0,0 @@
import { createRbacRolePoliciesWorkflow } from "@medusajs/core-flows"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
import { AdminCreateRbacRolePolicyType } from "./validators"
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
const { data: role_policies, metadata } = await query.graph({
entity: "rbac_role_policy",
fields: req.queryConfig.fields,
filters: req.filterableFields,
pagination: req.queryConfig.pagination,
})
res.status(200).json({
role_policies,
count: metadata?.count ?? 0,
offset: metadata?.skip ?? 0,
limit: metadata?.take ?? 0,
})
}
export const POST = async (
req: AuthenticatedMedusaRequest<AdminCreateRbacRolePolicyType>,
res: MedusaResponse
) => {
const input = [req.validatedBody]
const { result } = await createRbacRolePoliciesWorkflow(req.scope).run({
input: { role_policies: input },
})
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
const { data: rolePolicies } = await query.graph({
entity: "rbac_role_policy",
fields: req.queryConfig.fields,
filters: { id: result[0].id },
})
const role_policy = rolePolicies[0]
res.status(200).json({ role_policy })
}
@@ -1,52 +0,0 @@
import { z } from "zod"
import { applyAndAndOrOperators } from "../../../utils/common-validators"
import {
createFindParams,
createOperatorMap,
createSelectParams,
} from "../../../utils/validators"
export type AdminGetRbacRolePolicyParamsType = z.infer<
typeof AdminGetRbacRolePolicyParams
>
export const AdminGetRbacRolePolicyParams = createSelectParams()
export const AdminGetRbacRolePoliciesParamsFields = z.object({
q: z.string().optional(),
id: z.union([z.string(), z.array(z.string())]).optional(),
role_id: z.union([z.string(), z.array(z.string())]).optional(),
scope_id: z.union([z.string(), z.array(z.string())]).optional(),
created_at: createOperatorMap().optional(),
updated_at: createOperatorMap().optional(),
deleted_at: createOperatorMap().optional(),
})
export type AdminGetRbacRolePoliciesParamsType = z.infer<
typeof AdminGetRbacRolePoliciesParams
>
export const AdminGetRbacRolePoliciesParams = createFindParams({
limit: 50,
offset: 0,
})
.merge(AdminGetRbacRolePoliciesParamsFields)
.merge(applyAndAndOrOperators(AdminGetRbacRolePoliciesParamsFields))
export type AdminCreateRbacRolePolicyType = z.infer<
typeof AdminCreateRbacRolePolicy
>
export const AdminCreateRbacRolePolicy = z
.object({
role_id: z.string(),
scope_id: z.string(),
metadata: z.record(z.unknown()).nullish(),
})
.strict()
export type AdminUpdateRbacRolePolicyType = z.infer<
typeof AdminUpdateRbacRolePolicy
>
export const AdminUpdateRbacRolePolicy = z
.object({
metadata: z.record(z.unknown()).nullish(),
})
.strict()
@@ -0,0 +1,40 @@
import { deleteRbacRolePoliciesWorkflow } from "@medusajs/core-flows"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import { defineFileConfig, FeatureFlag } from "@medusajs/framework/utils"
import RbacFeatureFlag from "../../../../../../../feature-flags/rbac"
export const DELETE = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const { policy_id, id: role_id } = req.params
// First, we need to find the role_policy_id that connects this role and policy
const query = req.scope.resolve("query")
const { data: rolePolicies } = await query.graph({
entity: "rbac_role_policy",
fields: ["id"],
filters: { role_id, policy_id },
})
const rolePolicyId = rolePolicies[0]?.id
await deleteRbacRolePoliciesWorkflow(req.scope).run({
input: {
role_policy_ids: rolePolicyId ? [rolePolicyId] : [],
},
})
res.status(200).json({
id: rolePolicyId,
object: "rbac_role_policy",
deleted: true,
})
}
defineFileConfig({
isDisabled: () => !FeatureFlag.isFeatureEnabled(RbacFeatureFlag.key),
})
@@ -0,0 +1,69 @@
import { createRbacRolePoliciesWorkflow } from "@medusajs/core-flows"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import {
ContainerRegistrationKeys,
defineFileConfig,
FeatureFlag,
} from "@medusajs/framework/utils"
import RbacFeatureFlag from "../../../../../../feature-flags/rbac"
import { AdminAddRolePoliciesType } from "../../validators"
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const roleId = req.params.id
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
const { data: policies, metadata } = await query.graph({
entity: "rbac_role_policy",
fields: req.queryConfig?.fields,
filters: { ...req.filterableFields, role_id: roleId },
pagination: req.queryConfig?.pagination || {},
})
res.status(200).json({
policies,
count: metadata?.count ?? 0,
offset: metadata?.skip ?? 0,
limit: metadata?.take ?? 0,
})
}
export const POST = async (
req: AuthenticatedMedusaRequest<AdminAddRolePoliciesType>,
res: MedusaResponse
) => {
const roleId = req.params.id
const { policies } = req.validatedBody
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
const rolePolicies = policies.map((policyId) => ({
role_id: roleId,
policy_id: policyId,
}))
const { result } = await createRbacRolePoliciesWorkflow(req.scope).run({
input: {
actor_id: req.auth_context.actor_id,
actor: req.auth_context.actor_type,
policies: rolePolicies,
},
})
// Get the created role-policy association
const { data } = await query.graph({
entity: "rbac_role_policy",
fields: req.queryConfig?.fields,
filters: { id: result[0].id },
})
res.status(200).json({ policies: data })
}
defineFileConfig({
isDisabled: () => !FeatureFlag.isFeatureEnabled(RbacFeatureFlag.key),
})
@@ -57,6 +57,8 @@ export const POST = async (
const { result } = await updateRbacRolesWorkflow(req.scope).run({
input: {
actor_id: req.auth_context.actor_id,
actor: req.auth_context.actor_type,
selector: { id: req.params.id },
update: req.validatedBody,
},
@@ -1 +0,0 @@
export {}
@@ -7,6 +7,7 @@ import {
import { MiddlewareRoute } from "@medusajs/framework/http"
import {
AdminAddRolePoliciesType,
AdminCreateRbacRole,
AdminGetRbacRoleParams,
AdminGetRbacRolesParams,
@@ -56,6 +57,32 @@ export const adminRbacRoleRoutesMiddlewares: MiddlewareRoute[] = [
),
],
},
{
method: ["GET"],
matcher: "/admin/rbac/roles/:id/policies",
middlewares: [
validateAndTransformQuery(
AdminGetRbacRoleParams,
QueryConfig.retrieveRolePoliciesTransformQueryConfig
),
],
},
{
method: ["POST"],
matcher: "/admin/rbac/roles/:id/policies",
middlewares: [
validateAndTransformBody(AdminAddRolePoliciesType),
validateAndTransformQuery(
AdminGetRbacRoleParams,
QueryConfig.retrieveRolePoliciesTransformQueryConfig
),
],
},
{
method: ["DELETE"],
matcher: "/admin/rbac/roles/:id/policies/:policy_id",
middlewares: [],
},
{
method: ["DELETE"],
matcher: "/admin/rbac/roles/:id",
@@ -19,3 +19,24 @@ export const listTransformQueryConfig = {
defaultLimit: 20,
isList: true,
}
export const defaultAdminRolePoliciesFields = [
"id",
"role_id",
"policy_id",
"policy",
"metadata",
"created_at",
"updated_at",
"deleted_at",
]
export const retrieveRolePoliciesTransformQueryConfig = {
defaults: defaultAdminRolePoliciesFields,
isList: false,
}
export const listRolePoliciesTransformQueryConfig = {
...retrieveRolePoliciesTransformQueryConfig,
isList: true,
}
@@ -3,7 +3,12 @@ import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
import {
ContainerRegistrationKeys,
defineFileConfig,
FeatureFlag,
} from "@medusajs/framework/utils"
import RbacFeatureFlag from "../../../../feature-flags/rbac"
import { AdminCreateRbacRoleType } from "./validators"
export const GET = async (
@@ -34,7 +39,11 @@ export const POST = async (
const input = [req.validatedBody]
const { result } = await createRbacRolesWorkflow(req.scope).run({
input: { roles: input },
input: {
actor_id: req.auth_context.actor_id,
actor: req.auth_context.actor_type,
roles: input,
},
})
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
@@ -48,3 +57,7 @@ export const POST = async (
res.status(200).json({ role })
}
defineFileConfig({
isDisabled: () => !FeatureFlag.isFeatureEnabled(RbacFeatureFlag.key),
})
@@ -7,7 +7,11 @@ import {
} from "../../../utils/validators"
export type AdminGetRbacRoleParamsType = z.infer<typeof AdminGetRbacRoleParams>
export const AdminGetRbacRoleParams = createSelectParams()
export const AdminGetRbacRoleParams = createSelectParams().merge(
z.object({
policies: z.union([z.string(), z.array(z.string())]).optional(),
})
)
export const AdminGetRbacRolesParamsFields = z.object({
q: z.string().optional(),
@@ -48,3 +52,9 @@ export const AdminUpdateRbacRole = z
metadata: z.record(z.unknown()).nullish(),
})
.strict()
export const AdminAddRolePoliciesType = z.object({
policies: z.array(z.string().min(1)).min(1),
})
export type AdminAddRolePoliciesType = z.infer<typeof AdminAddRolePoliciesType>
@@ -1,3 +1,4 @@
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import {
AuthenticationInput,
ConfigModule,
@@ -8,7 +9,6 @@ import {
MedusaError,
Modules,
} from "@medusajs/framework/utils"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { generateJwtTokenForAuthIdentity } from "../../../utils/generate-jwt-token"
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
@@ -35,8 +35,13 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
if (success && authIdentity) {
const { http } = config.projectConfig
const token = generateJwtTokenForAuthIdentity(
{ authIdentity, actorType: actor_type, authProvider: auth_provider },
const token = await generateJwtTokenForAuthIdentity(
{
authIdentity,
actorType: actor_type,
authProvider: auth_provider,
container: req.scope,
},
{
secret: http.jwtSecret!,
expiresIn: http.jwtExpiresIn,
@@ -1,3 +1,4 @@
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import {
AuthenticationInput,
ConfigModule,
@@ -8,7 +9,6 @@ import {
MedusaError,
Modules,
} from "@medusajs/framework/utils"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { generateJwtTokenForAuthIdentity } from "../../../utils/generate-jwt-token"
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
@@ -35,11 +35,12 @@ export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
if (success && authIdentity) {
const { http } = config.projectConfig
const token = generateJwtTokenForAuthIdentity(
const token = await generateJwtTokenForAuthIdentity(
{
authIdentity,
actorType: actor_type,
authProvider: auth_provider,
container: req.scope,
},
{
secret: http.jwtSecret!,
@@ -1,3 +1,4 @@
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import {
AuthenticationInput,
ConfigModule,
@@ -8,7 +9,6 @@ import {
MedusaError,
Modules,
} from "@medusajs/framework/utils"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { generateJwtTokenForAuthIdentity } from "../../utils/generate-jwt-token"
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
@@ -39,11 +39,12 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
if (success && authIdentity) {
const { http } = config.projectConfig
const token = generateJwtTokenForAuthIdentity(
const token = await generateJwtTokenForAuthIdentity(
{
authIdentity,
actorType: actor_type,
authProvider: auth_provider,
container: req.scope,
},
{
secret: http.jwtSecret!,
@@ -1,9 +1,9 @@
import { IAuthModuleService } from "@medusajs/framework/types"
import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import { IAuthModuleService } from "@medusajs/framework/types"
import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils"
import { generateJwtTokenForAuthIdentity } from "../../utils/generate-jwt-token"
// Retrieve a newly generated JWT token. All checks that the existing token is valid already happen in the auth middleware.
@@ -23,8 +23,12 @@ export const POST = async (
ContainerRegistrationKeys.CONFIG_MODULE
).projectConfig
const token = generateJwtTokenForAuthIdentity(
{ authIdentity, actorType: req.auth_context.actor_type },
const token = await generateJwtTokenForAuthIdentity(
{
authIdentity,
actorType: req.auth_context.actor_type,
container: req.scope,
},
{
secret: http.jwtSecret!,
expiresIn: http.jwtExpiresIn,
@@ -1,19 +1,27 @@
import {
AuthIdentityDTO,
MedusaContainer,
ProjectConfigOptions,
} from "@medusajs/framework/types"
import { generateJwtToken } from "@medusajs/framework/utils"
import {
ContainerRegistrationKeys,
FeatureFlag,
generateJwtToken,
} from "@medusajs/framework/utils"
import { type Secret } from "jsonwebtoken"
import RbacFeatureFlag from "../../../feature-flags/rbac"
export function generateJwtTokenForAuthIdentity(
export async function generateJwtTokenForAuthIdentity(
{
authIdentity,
actorType,
authProvider,
container,
}: {
authIdentity: AuthIdentityDTO
actorType: string
authProvider?: string
container?: MedusaContainer
},
{
secret,
@@ -37,6 +45,29 @@ export function generateJwtTokenForAuthIdentity(
(identity) => identity.provider === authProvider
)[0]
let roles: string[] = []
if (FeatureFlag.isFeatureEnabled(RbacFeatureFlag.key)) {
if (container && entityId) {
try {
const query = container.resolve(ContainerRegistrationKeys.QUERY)
const { data: userRoles } = await query.graph({
entity: actorType,
fields: ["rbac_roles.id"],
filters: {
id: entityId,
},
})
if (userRoles?.[0]?.rbac_roles) {
roles = userRoles[0].rbac_roles.map((role) => role.id)
}
} catch {
// ignore
}
}
}
return generateJwtToken(
{
actor_id: entityId ?? "",
@@ -44,6 +75,7 @@ export function generateJwtTokenForAuthIdentity(
auth_identity_id: authIdentity?.id ?? "",
app_metadata: {
[entityIdKey]: entityId,
roles,
},
user_metadata: providerIdentity?.user_metadata ?? {},
},
+27 -17
View File
@@ -11,10 +11,12 @@ import {
dynamicImport,
FileSystem,
generateContainerTypes,
generatePolicyTypes,
gqlSchemaToTypes,
GracefulShutdownServer,
isFileSkipped,
isPresent,
promiseAll,
} from "@medusajs/framework/utils"
import { MedusaModule } from "@medusajs/framework/modules-sdk"
@@ -274,27 +276,35 @@ async function start(args: {
if (generateTypes) {
const typesDirectory = path.join(directory, ".medusa/types")
/**
* Cleanup existing types directory before creating new artifacts
*/
await new FileSystem(typesDirectory).cleanup({ recursive: true })
const fileGenPromises: Promise<void>[] = []
await generateContainerTypes(modules, {
outputDir: typesDirectory,
interfaceName: "ModuleImplementations",
})
logger.debug("Generated container types")
fileGenPromises.push(
generateContainerTypes(modules, {
outputDir: typesDirectory,
interfaceName: "ModuleImplementations",
})
)
if (gqlSchema) {
await gqlSchemaToTypes({
outputDir: typesDirectory,
filename: "query-entry-points",
interfaceName: "RemoteQueryEntryPoints",
schema: gqlSchema,
joinerConfigs: MedusaModule.getAllJoinerConfigs(),
})
logger.debug("Generated modules types")
fileGenPromises.push(
gqlSchemaToTypes({
outputDir: typesDirectory,
filename: "query-entry-points",
interfaceName: "RemoteQueryEntryPoints",
schema: gqlSchema,
joinerConfigs: MedusaModule.getAllJoinerConfigs(),
})
)
}
fileGenPromises.push(
generatePolicyTypes({
outputDir: typesDirectory,
})
)
await promiseAll(fileGenPromises)
logger.debug("Generated policy types")
}
// Register a health check endpoint. Ideally this also checks the readiness of the service, rather than just returning a static response.
+10
View File
@@ -0,0 +1,10 @@
import { FlagSettings } from "@medusajs/framework/feature-flags"
const RbacFeatureFlag: FlagSettings = {
key: "rbac",
default_val: false,
env_key: "MEDUSA_FF_RBAC",
description: "Enable role based access control",
}
export default RbacFeatureFlag
+8 -2
View File
@@ -1,4 +1,5 @@
import { container, MedusaAppLoader } from "@medusajs/framework"
import { container, MedusaAppLoader, policiesLoader } from "@medusajs/framework"
import { asValue } from "@medusajs/framework/awilix"
import { configLoader } from "@medusajs/framework/config"
import { pgConnectionLoader } from "@medusajs/framework/database"
import { featureFlagsLoader } from "@medusajs/framework/feature-flags"
@@ -22,7 +23,6 @@ import {
validateModuleName,
} from "@medusajs/framework/utils"
import { WorkflowLoader } from "@medusajs/framework/workflows"
import { asValue } from "@medusajs/framework/awilix"
import { Express, NextFunction, Request, Response } from "express"
import { join } from "path"
import requestIp from "request-ip"
@@ -182,6 +182,12 @@ export default async ({
)
await new LinkLoader(linksSourcePaths, logger).load()
// Load policies from project root and all plugins
await policiesLoader(rootDirectory)
for (const plugin of plugins) {
await policiesLoader(plugin.resolve)
}
const {
onApplicationStart,
onApplicationShutdown,
+6
View File
@@ -0,0 +1,6 @@
import RbacModule from "@medusajs/rbac"
export * from "@medusajs/rbac"
export default RbacModule
export const discoveryPath = require.resolve("@medusajs/rbac")
@@ -0,0 +1,152 @@
import { MedusaContainer } from "@medusajs/framework/types"
import {
ContainerRegistrationKeys,
FeatureFlag,
useCache,
} from "@medusajs/framework/utils"
import RbacFeatureFlag from "../../feature-flags/rbac"
export type PermissionAction = {
resource: string
operation: string
}
/*
/**
*
* @property roles the role(s) to check. Can be a single string or an array of strings.
* @property actions the action(s) to check. Can be a single `PermissionAction` or an array of `PermissionAction`s.
* @property container the Medusa container
*/
export type HasPermissionInput = {
roles: string | string[]
actions: PermissionAction | PermissionAction[]
container: MedusaContainer
}
type RolePoliciesCache = Map<string, Map<string, Set<string>>>
/**
* Checks if the given role(s) have permission to perform the specified action(s).
*
* @param input - The input containing roles, actions, and container
* @returns true if all actions are permitted, false otherwise
*
* @example
* ```ts
* const canWrite = await hasPermission({
* roles: ['role_123'],
* actions: { resource: 'product', operation: 'write' },
* container
* })
* ```
*/
export async function hasPermission(
input: HasPermissionInput
): Promise<boolean> {
const { roles, actions, container } = input
const roleIds = Array.isArray(roles) ? roles : [roles]
const actionList = Array.isArray(actions) ? actions : [actions]
const isDisabled = !FeatureFlag.isFeatureEnabled(RbacFeatureFlag.key)
if (isDisabled || !roleIds?.length || !actionList?.length) {
return true
}
const rolePoliciesMap = await fetchRolePolicies(roleIds, container)
for (const action of actionList) {
let hasAccess = false
for (const roleId of roleIds) {
const resourceMap = rolePoliciesMap.get(roleId)
if (!resourceMap) {
continue
}
const operations = resourceMap.get(action.resource)
if (
operations &&
(operations.has(action.operation) || operations.has("*"))
) {
hasAccess = true
break
}
}
if (!hasAccess) {
return false
}
}
return true
}
/**
* Fetches a single role's policies from cache or database.
*/
async function fetchSingleRolePolicies(
roleId: string,
container: MedusaContainer
): Promise<Map<string, Set<string>>> {
const query = container.resolve(ContainerRegistrationKeys.QUERY)
const tags: string[] = []
return await useCache<Map<string, Set<string>>>(
async () => {
const { data: roles } = await query.graph({
entity: "rbac_role",
fields: ["id", "policies.*"],
filters: { id: roleId },
})
const role = roles[0]
const resourceMap = new Map<string, Set<string>>()
tags.push(`rbac_role:${roleId}`)
if (role?.policies && Array.isArray(role.policies)) {
const policyIds: string[] = []
for (const policy of role.policies) {
policyIds.push(policy.id)
if (!resourceMap.has(policy.resource)) {
resourceMap.set(policy.resource, new Set())
}
resourceMap.get(policy.resource)!.add(policy.operation)
tags.push(`rbac_policy:${policy.id}`)
}
}
return resourceMap
},
{
container,
key: roleId,
tags,
ttl: 60 * 60 * 24 * 7,
providers: ["cache-memory"],
}
)
}
/**
* Fetches policies for multiple roles by composing individually cached role queries.
*/
async function fetchRolePolicies(
roleIds: string[],
container: MedusaContainer
): Promise<RolePoliciesCache> {
const rolePoliciesMap: RolePoliciesCache = new Map()
await Promise.all(
roleIds.map(async (roleId) => {
const resourceMap = await fetchSingleRolePolicies(roleId, container)
rolePoliciesMap.set(roleId, resourceMap)
})
)
return rolePoliciesMap
}