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

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

No files matched your search

@@ -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,
]
@@ -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,
})
}
@@ -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: [],
},
]
@@ -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,
}
@@ -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 })
}
@@ -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()
@@ -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,
})
}
@@ -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: [],
},
]
@@ -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,
}
@@ -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 })
}
@@ -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()
@@ -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,
})
}
@@ -0,0 +1 @@
export {}
@@ -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: [],
},
]
@@ -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,
}
@@ -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 })
}
@@ -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()
+2
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,