chore(medusa): middleware policies (#14521)

This commit is contained in:
Carlos R. L. Rodrigues
2026-01-13 16:46:53 -03:00
committed by GitHub
parent cec8b8e428
commit 8426fca710
13 changed files with 146 additions and 99 deletions

View File

@@ -1,5 +1,5 @@
export * from "./admin-consts"
export * from "./clean-response-data"
export * from "./define-middlewares"
export * from "./exception-formatter"
export * from "./middlewares"
export * from "./define-middlewares"
export * from "./admin-consts"

View File

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