diff --git a/packages/api-security/src/createSecurity/createGroupsMethods.ts b/packages/api-security/src/createSecurity/createGroupsMethods.ts index d86b8017600..dbb0a72b1cf 100644 --- a/packages/api-security/src/createSecurity/createGroupsMethods.ts +++ b/packages/api-security/src/createSecurity/createGroupsMethods.ts @@ -28,14 +28,6 @@ import { } from "~/types"; import NotAuthorizedError from "../NotAuthorizedError"; import { SecurityConfig } from "~/types"; -import { - listGroupsFromProvider as baseListGroupsFromPlugins, - type ListGroupsFromPluginsParams -} from "./groupsTeamsPlugins/listGroupsFromProvider"; -import { - getGroupFromProvider as baseGetGroupFromPlugins, - type GetGroupFromPluginsParams -} from "./groupsTeamsPlugins/getGroupFromProvider"; const CreateDataModel = withFields({ tenant: string({ validation: validation.create("required") }), @@ -159,7 +151,8 @@ async function updateTenantLinks( export const createGroupsMethods = ({ getTenant: initialGetTenant, storageOperations, - groupsProvider + getGroupRepository, + listGroupsRepository }: SecurityConfig) => { const getTenant = () => { const tenant = initialGetTenant(); @@ -169,24 +162,6 @@ export const createGroupsMethods = ({ return tenant; }; - const listGroupsFromPlugins = ( - params: Pick - ): Promise => { - return baseListGroupsFromPlugins({ - ...params, - groupsProvider - }); - }; - - const getGroupFromPlugins = ( - params: Pick - ): Promise => { - return baseGetGroupFromPlugins({ - ...params, - groupsProvider - }); - }; - return { onGroupBeforeCreate: createTopic("security.onGroupBeforeCreate"), onGroupAfterCreate: createTopic("security.onGroupAfterCreate"), @@ -203,13 +178,7 @@ export const createGroupsMethods = ({ let group: Group | null = null; try { const whereWithTenant = { ...where, tenant: where.tenant || getTenant() }; - const groupFromPlugins = await getGroupFromPlugins({ where: whereWithTenant }); - - if (groupFromPlugins) { - group = groupFromPlugins; - } else { - group = await storageOperations.getGroup({ where: whereWithTenant }); - } + group = await getGroupRepository.execute({ where: whereWithTenant }); } catch (ex) { throw new WebinyError( ex.message || "Could not get group.", @@ -228,17 +197,10 @@ export const createGroupsMethods = ({ try { const whereWithTenant = { ...where, tenant: getTenant() }; - const groupsFromDatabase = await storageOperations.listGroups({ + return await listGroupsRepository.execute({ where: whereWithTenant, sort: ["createdOn_ASC"] }); - - const groupsFromPlugins = await listGroupsFromPlugins({ where: whereWithTenant }); - - // We don't have to do any extra sorting because, as we can see above, `createdOn_ASC` is - // hardcoded, and groups coming from plugins don't have `createdOn`, meaning they should - // always be at the top of the list. - return [...groupsFromPlugins, ...groupsFromDatabase]; } catch (ex) { throw new WebinyError( ex.message || "Could not list security groups.", diff --git a/packages/api-security/src/createSecurity/groupsTeamsPlugins/getGroupFromProvider.ts b/packages/api-security/src/createSecurity/groupsTeamsPlugins/getGroupFromProvider.ts deleted file mode 100644 index c3d4b56161f..00000000000 --- a/packages/api-security/src/createSecurity/groupsTeamsPlugins/getGroupFromProvider.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { SecurityConfig } from "~/types"; -import { listGroupsFromProvider } from "./listGroupsFromProvider"; - -export interface GetGroupFromPluginsParams { - groupsProvider?: SecurityConfig["groupsProvider"]; - where: { - tenant: string; - id?: string; - slug?: string; - }; -} - -export const getGroupFromProvider = async (params: GetGroupFromPluginsParams) => { - const { groupsProvider, where } = params; - const [group] = await listGroupsFromProvider({ - groupsProvider, - where: { - tenant: where.tenant, - id_in: where.id ? [where.id] : undefined, - slug_in: where.slug ? [where.slug] : undefined - } - }); - - return group; -}; diff --git a/packages/api-security/src/createSecurity/groupsTeamsPlugins/listGroupsFromProvider.ts b/packages/api-security/src/createSecurity/groupsTeamsPlugins/listGroupsFromProvider.ts deleted file mode 100644 index ca0f9aa81a9..00000000000 --- a/packages/api-security/src/createSecurity/groupsTeamsPlugins/listGroupsFromProvider.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { SecurityConfig } from "~/types"; - -export interface ListGroupsFromPluginsParams { - groupsProvider?: SecurityConfig["groupsProvider"]; - where: { - tenant: string; - id_in?: string[]; - slug_in?: string[]; - }; -} - -export const listGroupsFromProvider = async (params: ListGroupsFromPluginsParams) => { - const { groupsProvider, where } = params; - if (!groupsProvider) { - return []; - } - - const groups = await groupsProvider(); - - return groups.filter(group => { - // First we ensure the group belongs to the correct tenant. - if (group.tenant) { - if (group.tenant !== where.tenant) { - return false; - } - } - - const { id_in, slug_in } = where; - if (id_in) { - if (!id_in.includes(group.id)) { - return false; - } - } - - if (slug_in) { - if (!slug_in.includes(group.id)) { - return false; - } - } - - return group; - }); -}; diff --git a/packages/api-security/src/groups/repository/GetGroupRepository.ts b/packages/api-security/src/groups/repository/GetGroupRepository.ts new file mode 100644 index 00000000000..361fb6f064c --- /dev/null +++ b/packages/api-security/src/groups/repository/GetGroupRepository.ts @@ -0,0 +1,17 @@ +import type { Group, SecurityStorageOperations } from "~/types"; +import type { + IGetGroupRepository, + GetGroupRepositoryParams +} from "./abstractions/IGetGroupRepository"; + +export class GetGroupRepository implements IGetGroupRepository { + private storageOperations: SecurityStorageOperations; + + constructor(storageOperations: SecurityStorageOperations) { + this.storageOperations = storageOperations; + } + + async execute(params: GetGroupRepositoryParams): Promise { + return await this.storageOperations.getGroup(params); + } +} diff --git a/packages/api-security/src/groups/repository/ListGroupsRepository.ts b/packages/api-security/src/groups/repository/ListGroupsRepository.ts new file mode 100644 index 00000000000..ee7027daf4d --- /dev/null +++ b/packages/api-security/src/groups/repository/ListGroupsRepository.ts @@ -0,0 +1,17 @@ +import type { + IListGroupsRepository, + ListGroupsRepositoryParams +} from "./abstractions/IListGroupsRepository"; +import type { Group, SecurityStorageOperations } from "~/types"; + +export class ListGroupsRepository implements IListGroupsRepository { + private storageOperations: SecurityStorageOperations; + + constructor(storageOperations: SecurityStorageOperations) { + this.storageOperations = storageOperations; + } + + async execute(params: ListGroupsRepositoryParams): Promise { + return await this.storageOperations.listGroups(params); + } +} diff --git a/packages/api-security/src/groups/repository/WithGroupFromPlugins.ts b/packages/api-security/src/groups/repository/WithGroupFromPlugins.ts new file mode 100644 index 00000000000..da624d9cd63 --- /dev/null +++ b/packages/api-security/src/groups/repository/WithGroupFromPlugins.ts @@ -0,0 +1,37 @@ +import type { PluginsContainer } from "@webiny/plugins"; +import type { + GetGroupRepositoryParams, + IGetGroupRepository +} from "./abstractions/IGetGroupRepository"; +import type { Group } from "~/types"; +import { SecurityRolePlugin } from "~/plugins/SecurityRolePlugin"; +import { createFilter } from "./filterGroups"; + +export class WithGroupFromPlugins implements IGetGroupRepository { + private plugins: PluginsContainer; + private repository: IGetGroupRepository; + + constructor(plugins: PluginsContainer, repository: IGetGroupRepository) { + this.plugins = plugins; + this.repository = repository; + } + + async execute(params: GetGroupRepositoryParams): Promise { + const filterGroups = createFilter({ + tenant: params.where.tenant, + id_in: params.where.id ? [params.where.id] : undefined, + slug_in: params.where.slug ? [params.where.slug] : undefined + }); + + const [groupFromPlugins] = this.plugins + .byType(SecurityRolePlugin.type) + .map(plugin => plugin.securityRole) + .filter(filterGroups); + + if (groupFromPlugins) { + return groupFromPlugins; + } + + return this.repository.execute(params); + } +} diff --git a/packages/api-security/src/groups/repository/WithGroupsFromPlugins.ts b/packages/api-security/src/groups/repository/WithGroupsFromPlugins.ts new file mode 100644 index 00000000000..0c24ec6c174 --- /dev/null +++ b/packages/api-security/src/groups/repository/WithGroupsFromPlugins.ts @@ -0,0 +1,33 @@ +import type { PluginsContainer } from "@webiny/plugins"; +import type { + IListGroupsRepository, + ListGroupsRepositoryParams +} from "./abstractions/IListGroupsRepository"; +import type { Group } from "~/types"; +import { SecurityRolePlugin } from "~/plugins/SecurityRolePlugin"; +import { createFilter } from "./filterGroups"; + +export class WithGroupsFromPlugins implements IListGroupsRepository { + private plugins: PluginsContainer; + private repository: IListGroupsRepository; + + constructor(plugins: PluginsContainer, repository: IListGroupsRepository) { + this.plugins = plugins; + this.repository = repository; + } + + async execute(params: ListGroupsRepositoryParams): Promise { + const baseGroups = await this.repository.execute(params); + + const filterGroups = createFilter(params.where); + + const groupsFromPlugins = this.plugins + .byType(SecurityRolePlugin.type) + .map(plugin => plugin.securityRole) + .filter(filterGroups); + + // We don't have to do any extra sorting because groups coming from plugins don't have `createdOn`, + // meaning they should always be at the top of the list. + return [...groupsFromPlugins, ...baseGroups]; + } +} diff --git a/packages/api-security/src/groups/repository/abstractions/IGetGroupRepository.ts b/packages/api-security/src/groups/repository/abstractions/IGetGroupRepository.ts new file mode 100644 index 00000000000..915a81411f9 --- /dev/null +++ b/packages/api-security/src/groups/repository/abstractions/IGetGroupRepository.ts @@ -0,0 +1,13 @@ +import type { Group } from "~/types"; + +export interface GetGroupRepositoryParams { + where: { + id?: string; + slug?: string; + tenant: string; + }; +} + +export interface IGetGroupRepository { + execute(params: GetGroupRepositoryParams): Promise; +} diff --git a/packages/api-security/src/groups/repository/abstractions/IListGroupsRepository.ts b/packages/api-security/src/groups/repository/abstractions/IListGroupsRepository.ts new file mode 100644 index 00000000000..cd49eb82483 --- /dev/null +++ b/packages/api-security/src/groups/repository/abstractions/IListGroupsRepository.ts @@ -0,0 +1,14 @@ +import type { Group } from "~/types"; + +export interface ListGroupsRepositoryParams { + where: { + id_in?: string[]; + slug_in?: string[]; + tenant: string; + }; + sort?: string[]; +} + +export interface IListGroupsRepository { + execute(params: ListGroupsRepositoryParams): Promise; +} diff --git a/packages/api-security/src/groups/repository/filterGroups.ts b/packages/api-security/src/groups/repository/filterGroups.ts new file mode 100644 index 00000000000..f2aa869c424 --- /dev/null +++ b/packages/api-security/src/groups/repository/filterGroups.ts @@ -0,0 +1,33 @@ +import type { Group } from "~/types"; + +export interface FilterGroupsParams { + id_in?: string[]; + slug_in?: string[]; + tenant: string; +} + +export const createFilter = (where: FilterGroupsParams) => { + return (group: Group) => { + // First we ensure the group belongs to the correct tenant. + if (group.tenant) { + if (group.tenant !== where.tenant) { + return false; + } + } + + const { id_in, slug_in } = where; + if (id_in) { + if (!id_in.includes(group.id)) { + return false; + } + } + + if (slug_in) { + if (!slug_in.includes(group.id)) { + return false; + } + } + + return true; + }; +}; diff --git a/packages/api-security/src/index.ts b/packages/api-security/src/index.ts index 1365fe0c882..a805a932b4c 100644 --- a/packages/api-security/src/index.ts +++ b/packages/api-security/src/index.ts @@ -16,8 +16,11 @@ import { MultiTenancyAppConfig, MultiTenancyGraphQLConfig } from "~/enterprise/multiTenancy"; -import { SecurityRolePlugin } from "~/plugins/SecurityRolePlugin"; import { SecurityTeamPlugin } from "~/plugins/SecurityTeamPlugin"; +import { WithGroupsFromPlugins } from "~/groups/repository/WithGroupsFromPlugins"; +import { ListGroupsRepository } from "~/groups/repository/ListGroupsRepository"; +import { GetGroupRepository } from "~/groups/repository/GetGroupRepository"; +import { WithGroupFromPlugins } from "~/groups/repository/WithGroupFromPlugins"; export { default as NotAuthorizedResponse } from "./NotAuthorizedResponse"; export { default as NotAuthorizedError } from "./NotAuthorizedError"; @@ -38,6 +41,16 @@ export const createSecurityContext = ({ storageOperations }: SecurityConfig) => const license = context.wcp.getProjectLicense(); + const listGroupsRepository = new WithGroupsFromPlugins( + context.plugins, + new ListGroupsRepository(storageOperations) + ); + + const getGroupRepository = new WithGroupFromPlugins( + context.plugins, + new GetGroupRepository(storageOperations) + ); + context.security = await createSecurity({ advancedAccessControlLayer: license?.package?.features?.advancedAccessControlLayer, getTenant: () => { @@ -45,10 +58,8 @@ export const createSecurityContext = ({ storageOperations }: SecurityConfig) => return tenant ? tenant.id : undefined; }, storageOperations, - groupsProvider: async () => - context.plugins - .byType(SecurityRolePlugin.type) - .map(plugin => plugin.securityRole), + getGroupRepository, + listGroupsRepository, teamsProvider: async () => context.plugins .byType(SecurityTeamPlugin.type) diff --git a/packages/api-security/src/types.ts b/packages/api-security/src/types.ts index 83a9d1bfe91..e847261df77 100644 --- a/packages/api-security/src/types.ts +++ b/packages/api-security/src/types.ts @@ -5,6 +5,8 @@ import { Topic } from "@webiny/pubsub/types"; import { GetTenant } from "~/createSecurity"; import { ProjectPackageFeatures } from "@webiny/wcp/types"; import { TenancyContext } from "@webiny/api-tenancy/types"; +import { IGetGroupRepository } from "~/groups/repository/abstractions/IGetGroupRepository"; +import { IListGroupsRepository } from "~/groups/repository/abstractions/IListGroupsRepository"; // Backwards compatibility - START export type SecurityIdentity = Identity; @@ -34,7 +36,8 @@ export interface SecurityConfig { advancedAccessControlLayer?: ProjectPackageFeatures["advancedAccessControlLayer"]; getTenant: GetTenant; storageOperations: SecurityStorageOperations; - groupsProvider?: () => Promise; + getGroupRepository: IGetGroupRepository; + listGroupsRepository: IListGroupsRepository; teamsProvider?: () => Promise; }