chore(rbac): user link and utils (#14320)
This commit is contained in:
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 ?? {},
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user