Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(api-security): introduce groups get/list repositories #4318

Open
wants to merge 2 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 4 additions & 42 deletions packages/api-security/src/createSecurity/createGroupsMethods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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") }),
Expand Down Expand Up @@ -159,7 +151,8 @@ async function updateTenantLinks(
export const createGroupsMethods = ({
getTenant: initialGetTenant,
storageOperations,
groupsProvider
getGroupRepository,
listGroupsRepository
}: SecurityConfig) => {
const getTenant = () => {
const tenant = initialGetTenant();
Expand All @@ -169,24 +162,6 @@ export const createGroupsMethods = ({
return tenant;
};

const listGroupsFromPlugins = (
params: Pick<ListGroupsFromPluginsParams, "where">
): Promise<Group[]> => {
return baseListGroupsFromPlugins({
...params,
groupsProvider
});
};

const getGroupFromPlugins = (
params: Pick<GetGroupFromPluginsParams, "where">
): Promise<Group> => {
return baseGetGroupFromPlugins({
...params,
groupsProvider
});
};

return {
onGroupBeforeCreate: createTopic("security.onGroupBeforeCreate"),
onGroupAfterCreate: createTopic("security.onGroupAfterCreate"),
Expand All @@ -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.",
Expand All @@ -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.",
Expand Down

This file was deleted.

This file was deleted.

17 changes: 17 additions & 0 deletions packages/api-security/src/groups/repository/GetGroupRepository.ts
Original file line number Diff line number Diff line change
@@ -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<Group | null> {
return await this.storageOperations.getGroup(params);
}
}
Original file line number Diff line number Diff line change
@@ -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<Group[]> {
return await this.storageOperations.listGroups(params);
}
}
Original file line number Diff line number Diff line change
@@ -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<Group | null> {
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>(SecurityRolePlugin.type)
.map(plugin => plugin.securityRole)
.filter(filterGroups);

if (groupFromPlugins) {
return groupFromPlugins;
}

return this.repository.execute(params);
}
}
Original file line number Diff line number Diff line change
@@ -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<Group[]> {
const baseGroups = await this.repository.execute(params);

const filterGroups = createFilter(params.where);

const groupsFromPlugins = this.plugins
.byType<SecurityRolePlugin>(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];
}
}
Original file line number Diff line number Diff line change
@@ -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<Group | null>;
}
Original file line number Diff line number Diff line change
@@ -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<Group[]>;
}
33 changes: 33 additions & 0 deletions packages/api-security/src/groups/repository/filterGroups.ts
Original file line number Diff line number Diff line change
@@ -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;
};
};
21 changes: 16 additions & 5 deletions packages/api-security/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -38,17 +41,25 @@ 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: () => {
const tenant = context.tenancy.getCurrentTenant();
return tenant ? tenant.id : undefined;
},
storageOperations,
groupsProvider: async () =>
context.plugins
.byType<SecurityRolePlugin>(SecurityRolePlugin.type)
.map(plugin => plugin.securityRole),
getGroupRepository,
listGroupsRepository,
teamsProvider: async () =>
context.plugins
.byType<SecurityTeamPlugin>(SecurityTeamPlugin.type)
Expand Down
5 changes: 4 additions & 1 deletion packages/api-security/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -34,7 +36,8 @@ export interface SecurityConfig {
advancedAccessControlLayer?: ProjectPackageFeatures["advancedAccessControlLayer"];
getTenant: GetTenant;
storageOperations: SecurityStorageOperations;
groupsProvider?: () => Promise<SecurityRole[]>;
getGroupRepository: IGetGroupRepository;
listGroupsRepository: IListGroupsRepository;
teamsProvider?: () => Promise<SecurityTeam[]>;
}

Expand Down
Loading