From 9125c5776aeb50019f2b7e549bcad380570b2c2f Mon Sep 17 00:00:00 2001 From: Adrian Smijulj Date: Tue, 27 Feb 2024 13:20:14 +0100 Subject: [PATCH] fix(cms): bring back old permissions checking-related utils (#3904) --- packages/api-headless-cms/src/context.ts | 30 +++++ packages/api-headless-cms/src/types.ts | 20 +++- .../utils/permissions/EntriesPermissions.ts | 4 + .../permissions/ModelGroupsPermissions.ts | 52 ++++++++ .../utils/permissions/ModelsPermissions.ts | 113 ++++++++++++++++++ 5 files changed, 217 insertions(+), 2 deletions(-) create mode 100644 packages/api-headless-cms/src/utils/permissions/EntriesPermissions.ts create mode 100644 packages/api-headless-cms/src/utils/permissions/ModelGroupsPermissions.ts create mode 100644 packages/api-headless-cms/src/utils/permissions/ModelsPermissions.ts diff --git a/packages/api-headless-cms/src/context.ts b/packages/api-headless-cms/src/context.ts index c448b4b40f9..a7cee1bce16 100644 --- a/packages/api-headless-cms/src/context.ts +++ b/packages/api-headless-cms/src/context.ts @@ -9,6 +9,9 @@ import { createModelsCrud } from "~/crud/contentModel.crud"; import { createContentEntryCrud } from "~/crud/contentEntry.crud"; import { StorageOperationsCmsModelPlugin } from "~/plugins"; import { createCmsModelFieldConvertersAttachFactory } from "~/utils/converters/valueKeyStorageConverter"; +import { ModelsPermissions } from "~/utils/permissions/ModelsPermissions"; +import { ModelGroupsPermissions } from "./utils/permissions/ModelGroupsPermissions"; +import { EntriesPermissions } from "./utils/permissions/EntriesPermissions"; import { createExportCrud } from "~/export"; import { createImportCrud } from "~/export/crud/importing"; @@ -60,6 +63,25 @@ export const createContextPlugin = ({ storageOperations }: CrudParams) => { await context.benchmark.measure("headlessCms.createContext", async () => { await storageOperations.beforeInit(context); + const modelGroupsPermissions = new ModelGroupsPermissions({ + getIdentity: context.security.getIdentity, + getPermissions: () => context.security.getPermissions("cms.contentModelGroup"), + fullAccessPermissionName: "cms.*" + }); + + const modelsPermissions = new ModelsPermissions({ + getIdentity: context.security.getIdentity, + getPermissions: () => context.security.getPermissions("cms.contentModel"), + fullAccessPermissionName: "cms.*", + modelGroupsPermissions + }); + + const entriesPermissions = new EntriesPermissions({ + getIdentity: context.security.getIdentity, + getPermissions: () => context.security.getPermissions("cms.contentEntry"), + fullAccessPermissionName: "cms.*" + }); + const accessControl = new AccessControl({ getIdentity: context.security.getIdentity, getGroupsPermissions: () => @@ -81,6 +103,14 @@ export const createContextPlugin = ({ storageOperations }: CrudParams) => { PREVIEW: type === "preview", MANAGE: type === "manage", storageOperations, + + // TODO: remove with 5.40 release. + permissions: { + groups: modelGroupsPermissions, + models: modelsPermissions, + entries: entriesPermissions + }, + accessControl, ...createSystemCrud({ context, diff --git a/packages/api-headless-cms/src/types.ts b/packages/api-headless-cms/src/types.ts index 0069a305823..d02eed2c1ea 100644 --- a/packages/api-headless-cms/src/types.ts +++ b/packages/api-headless-cms/src/types.ts @@ -12,9 +12,18 @@ import { Topic } from "@webiny/pubsub/types"; import { CmsModelConverterCallable } from "~/utils/converters/ConverterCollection"; import { HeadlessCmsExport, HeadlessCmsImport } from "~/export/types"; import { AccessControl } from "~/crud/AccessControl/AccessControl"; +import { ModelGroupsPermissions } from "~/utils/permissions/ModelGroupsPermissions"; +import { ModelsPermissions } from "~/utils/permissions/ModelsPermissions"; +import { EntriesPermissions } from "~/utils/permissions/EntriesPermissions"; export type ApiEndpoint = "manage" | "preview" | "read"; +interface HeadlessCmsPermissions { + groups: ModelGroupsPermissions; + models: ModelsPermissions; + entries: EntriesPermissions; +} + export interface HeadlessCms extends CmsSystemContext, CmsGroupContext, @@ -48,12 +57,19 @@ export interface HeadlessCms * The storage operations loaded for current context. */ storageOperations: HeadlessCmsStorageOperations; + + /** + * Use to ensure perform authorization and ensure identities have access to the groups, models and entries. + */ + accessControl: AccessControl; + /** * Permissions for groups, models and entries. - * * @internal + * @deprecated Will be removed with the 5.40.0 release. Use `accessControl` instead. */ - accessControl: AccessControl; + permissions: HeadlessCmsPermissions; + /** * Export operations. */ diff --git a/packages/api-headless-cms/src/utils/permissions/EntriesPermissions.ts b/packages/api-headless-cms/src/utils/permissions/EntriesPermissions.ts new file mode 100644 index 00000000000..9adef9f6253 --- /dev/null +++ b/packages/api-headless-cms/src/utils/permissions/EntriesPermissions.ts @@ -0,0 +1,4 @@ +import { CmsEntryPermission } from "~/types"; +import { AppPermissions } from "@webiny/api-security"; + +export class EntriesPermissions extends AppPermissions {} diff --git a/packages/api-headless-cms/src/utils/permissions/ModelGroupsPermissions.ts b/packages/api-headless-cms/src/utils/permissions/ModelGroupsPermissions.ts new file mode 100644 index 00000000000..8eb5c665b26 --- /dev/null +++ b/packages/api-headless-cms/src/utils/permissions/ModelGroupsPermissions.ts @@ -0,0 +1,52 @@ +import { AppPermissions, NotAuthorizedError } from "@webiny/api-security"; +import { CmsGroup, CmsGroupPermission } from "~/types"; + +export interface CanAccessGroupParams { + group: Pick; +} + +export class ModelGroupsPermissions extends AppPermissions { + async canAccessGroup({ group }: CanAccessGroupParams) { + if (await this.hasFullAccess()) { + return true; + } + + const permissions = await this.getPermissions(); + + const locale = group.locale; + + for (const permission of permissions) { + const { groups } = permission; + + // When no groups defined on permission it means user has access to everything. + if (!groups) { + return true; + } + + // when there is no locale in groups, it means that no access was given + // this happens when access control was set but no models or groups were added + if ( + Array.isArray(groups[locale]) === false || + groups[locale].includes(group.id) === false + ) { + continue; + } + return true; + } + + return false; + } + + async ensureCanAccessGroup(params: CanAccessGroupParams) { + const canAccessModel = await this.canAccessGroup(params); + if (canAccessModel) { + return; + } + + throw new NotAuthorizedError({ + data: { + reason: `Not allowed to access group "${params.group.id}".` + } + }); + } +} diff --git a/packages/api-headless-cms/src/utils/permissions/ModelsPermissions.ts b/packages/api-headless-cms/src/utils/permissions/ModelsPermissions.ts new file mode 100644 index 00000000000..53a06368784 --- /dev/null +++ b/packages/api-headless-cms/src/utils/permissions/ModelsPermissions.ts @@ -0,0 +1,113 @@ +import { AppPermissions, AppPermissionsParams, NotAuthorizedError } from "@webiny/api-security"; +import { + CmsGroupPermission, + CmsModel as BaseCmsModel, + CmsModelGroup as BaseCmsModelGroup, + CmsModelPermission +} from "~/types"; +import { ModelGroupsPermissions } from "~/utils/permissions/ModelGroupsPermissions"; + +export interface ModelsPermissionsParams extends AppPermissionsParams { + modelGroupsPermissions: ModelGroupsPermissions; +} + +interface PickedCmsModel extends Pick { + group: Pick; +} + +export interface CanAccessModelParams { + model: PickedCmsModel; +} + +export interface EnsureModelAccessParams { + model: PickedCmsModel; +} + +export class ModelsPermissions extends AppPermissions { + private readonly modelGroupsPermissions: ModelGroupsPermissions; + + public constructor(params: ModelsPermissionsParams) { + super(params); + this.modelGroupsPermissions = params.modelGroupsPermissions; + } + + public async canAccessModel({ model }: CanAccessModelParams) { + if (await this.hasFullAccess()) { + return true; + } + + const modelGroupsPermissions = this.modelGroupsPermissions; + + // eslint-disable-next-line + const modelsPermissions = this; + + const canReadGroups = await modelGroupsPermissions.ensure({ rwd: "r" }, { throw: false }); + if (!canReadGroups) { + return false; + } + + const canReadModels = await modelsPermissions.ensure({ rwd: "r" }, { throw: false }); + if (!canReadModels) { + return false; + } + + const modelGroupsPermissionsList = await modelGroupsPermissions.getPermissions(); + const modelsPermissionsList = await this.getPermissions(); + + const locale = model.locale; + + for (let i = 0; i < modelGroupsPermissionsList.length; i++) { + const modelGroupPermission = modelGroupsPermissionsList[i]; + + const { groups } = modelGroupPermission; + + for (let j = 0; j < modelsPermissionsList.length; j++) { + const modelPermission = modelsPermissionsList[j]; + + const { models } = modelPermission; + // when no models or groups defined on permission + // it means user has access to everything + if (!models && !groups) { + return true; + } + + // Does the model belong to a group for which user has permission? + if (groups) { + if ( + Array.isArray(groups[locale]) === false || + groups[locale].includes(model.group.id) === false + ) { + continue; + } + } + + // Does the user have access to the specific model? + if (models) { + if ( + Array.isArray(models[locale]) === false || + models[locale].includes(model.modelId) === false + ) { + continue; + } + } + + return true; + } + } + + return false; + } + + public async ensureCanAccessModel(params: EnsureModelAccessParams) { + const canAccessModel = await this.canAccessModel(params); + if (canAccessModel) { + return; + } + + throw new NotAuthorizedError({ + data: { + reason: `Not allowed to access model "${params.model.modelId}".` + } + }); + } +}