diff --git a/packages/api-security-auth0/src/createAuth0.ts b/packages/api-security-auth0/src/createAuth0.ts index 7a1cff7e0e4..5aec07c4b8f 100644 --- a/packages/api-security-auth0/src/createAuth0.ts +++ b/packages/api-security-auth0/src/createAuth0.ts @@ -1,5 +1,5 @@ import { createAuthenticator, AuthenticatorConfig } from "~/createAuthenticator"; -import { createGroupAuthorizer, GroupAuthorizerConfig } from "~/createGroupAuthorizer"; +import { createGroupsTeamsAuthorizer, GroupsTeamsAuthorizerConfig } from "@webiny/api-security"; import { createIdentityType } from "~/createIdentityType"; import { createAdminUsersHooks } from "./createAdminUsersHooks"; import { extendTenancy } from "./extendTenancy"; @@ -7,7 +7,7 @@ import { Context } from "~/types"; export interface CreateAuth0Config extends AuthenticatorConfig, - GroupAuthorizerConfig { + GroupsTeamsAuthorizerConfig { graphQLIdentityType?: string; } @@ -22,7 +22,7 @@ export const createAuth0 = ( domain: config.domain, getIdentity: config.getIdentity }), - createGroupAuthorizer({ + createGroupsTeamsAuthorizer({ identityType, getGroupSlug: config.getGroupSlug, inheritGroupsFromParentTenant: config.inheritGroupsFromParentTenant, diff --git a/packages/api-security-auth0/src/createGroupAuthorizer.ts b/packages/api-security-auth0/src/createGroupAuthorizer.ts deleted file mode 100644 index 1704b7ab701..00000000000 --- a/packages/api-security-auth0/src/createGroupAuthorizer.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { ContextPlugin } from "@webiny/handler"; -import { getPermissionsFromSecurityGroupsForLocale } from "@webiny/api-security"; -import { Context } from "~/types"; - -export type GroupSlug = string | undefined; - -export interface GroupAuthorizerConfig { - /** - * Specify an `identityType` if you want to only run this authorizer for specific identities. - */ - identityType?: string; - - /** - * Get a group slug to load permissions from. - */ - getGroupSlug?: (context: TContext) => Promise | GroupSlug; - - /** - * If a security group is not found, try loading it from a parent tenant (default: true). - */ - inheritGroupsFromParentTenant?: boolean; - - /** - * Check whether the current identity is authorized to access the current tenant. - */ - canAccessTenant?: (context: TContext) => boolean | Promise; -} - -export const createGroupAuthorizer = ( - config: GroupAuthorizerConfig -) => { - return new ContextPlugin(context => { - const { security } = context; - security.addAuthorizer(async () => { - const identity = security.getIdentity(); - const tenant = context.tenancy.getCurrentTenant(); - - if (!identity) { - return null; - } - - // If `identityType` is specified, we'll only execute this authorizer for a matching identity. - if (config.identityType && identity.type !== config.identityType) { - return null; - } - - const locale = context.i18n?.getContentLocale(); - if (!locale) { - return null; - } - - if (config.canAccessTenant) { - const canAccessTenant = await config.canAccessTenant(context); - if (!canAccessTenant) { - return []; - } - } - - const groupSlug = config.getGroupSlug - ? await config.getGroupSlug(context) - : identity.group; - - if (!groupSlug) { - return null; - } - - const group = await security - .getStorageOperations() - .getGroup({ where: { slug: groupSlug, tenant: tenant.id } }); - - if (group) { - return getPermissionsFromSecurityGroupsForLocale([group], locale.code); - } - - // If no security group was found, it could be due to an identity accessing a sub-tenant. - // In this case, let's try loading a security group from the parent tenant. - - // NOTE: this will work well for flat tenant hierarchy where there's a `root` tenant and 1 level of sibling sub-tenants. - // For multi-level hierarchy, the best approach is to code a plugin with the desired permission fetching logic. - - if (tenant.parent && config.inheritGroupsFromParentTenant !== false) { - const group = await security.getStorageOperations().getGroup({ - where: { slug: groupSlug, tenant: tenant.parent } - }); - - if (group) { - return getPermissionsFromSecurityGroupsForLocale([group], locale.code); - } - } - - return null; - }); - }); -}; diff --git a/packages/api-security-auth0/src/index.ts b/packages/api-security-auth0/src/index.ts index d516bf6bca9..9b0cc1eb134 100644 --- a/packages/api-security-auth0/src/index.ts +++ b/packages/api-security-auth0/src/index.ts @@ -1,6 +1,20 @@ +import { + createGroupsTeamsAuthorizer, + type GroupsTeamsAuthorizerConfig +} from "@webiny/api-security"; + export { createIdentityType } from "./createIdentityType"; export { createAuthenticator } from "./createAuthenticator"; export type { AuthenticatorConfig } from "./createAuthenticator"; -export { createGroupAuthorizer } from "./createGroupAuthorizer"; -export type { GroupAuthorizerConfig } from "./createGroupAuthorizer"; export { createAuth0 } from "./createAuth0"; + +export { createGroupsTeamsAuthorizer, type GroupsTeamsAuthorizerConfig }; + +// Backwards compatibility. +// @deprecated Use `createGroupsTeamsAuthorizer` instead. +const createGroupAuthorizer = createGroupsTeamsAuthorizer; + +// @deprecated Use `GroupsTeamsAuthorizerConfig` instead. +type GroupAuthorizerConfig = GroupsTeamsAuthorizerConfig; + +export { createGroupAuthorizer, type GroupAuthorizerConfig }; diff --git a/packages/api-security-okta/src/createGroupAuthorizer.ts b/packages/api-security-okta/src/createGroupAuthorizer.ts deleted file mode 100644 index 38e342c4bb9..00000000000 --- a/packages/api-security-okta/src/createGroupAuthorizer.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { SecurityContext } from "@webiny/api-security/types"; -import { ContextPlugin } from "@webiny/api"; -import { TenancyContext } from "@webiny/api-tenancy/types"; -import { I18NContext } from "@webiny/api-i18n/types"; -import { getPermissionsFromSecurityGroupsForLocale } from "@webiny/api-security"; - -type Context = TenancyContext & SecurityContext & I18NContext; - -export type GroupSlug = string | undefined; - -export interface GroupAuthorizerConfig { - // Specify an `identityType` if you want to only run this authorizer for specific identities. - identityType?: string; - - // Get a group slug to load permissions from. - getGroupSlug?: (context: TContext) => Promise | GroupSlug; -} - -export const createGroupAuthorizer = ( - config: GroupAuthorizerConfig -) => { - return new ContextPlugin(context => { - const { security } = context; - security.addAuthorizer(async () => { - const identity = security.getIdentity(); - const tenant = context.tenancy.getCurrentTenant(); - - if (!identity) { - return null; - } - - // If `identityType` is specified, we'll only execute this authorizer for a matching identity. - if (config.identityType && identity.type !== config.identityType) { - return null; - } - - const locale = context.i18n?.getContentLocale(); - if (!locale) { - return null; - } - - const groupSlug = config.getGroupSlug - ? await config.getGroupSlug(context) - : identity.group; - - if (!groupSlug) { - return null; - } - - let group = await security - .getStorageOperations() - .getGroup({ where: { slug: groupSlug, tenant: tenant.id } }); - - if (group) { - return getPermissionsFromSecurityGroupsForLocale([group], locale.code); - } - - // If no security group was found, it could be due to an identity accessing a sub-tenant. - // In this case, let's try loading a security group from the parent tenant. - - // NOTE: this will work well for flat tenant hierarchy where there's a `root` tenant and 1 level of sibling sub-tenants. - // For multi-level hierarchy, the best approach is to code a plugin with the desired permission fetching logic. - - if (tenant.parent) { - group = await security.getGroup({ - where: { slug: groupSlug, tenant: tenant.parent } - }); - - if (group) { - return getPermissionsFromSecurityGroupsForLocale([group], locale.code); - } - } - - return null; - }); - }); -}; diff --git a/packages/api-security-okta/src/createOkta.ts b/packages/api-security-okta/src/createOkta.ts index 21d0772c630..8e2fea1e89f 100644 --- a/packages/api-security-okta/src/createOkta.ts +++ b/packages/api-security-okta/src/createOkta.ts @@ -1,5 +1,5 @@ import { createAuthenticator, AuthenticatorConfig } from "~/createAuthenticator"; -import { createGroupAuthorizer, GroupAuthorizerConfig } from "~/createGroupAuthorizer"; +import { createGroupsTeamsAuthorizer, GroupsTeamsAuthorizerConfig } from "@webiny/api-security"; import { createIdentityType } from "~/createIdentityType"; import { extendTenancy } from "./extendTenancy"; import { createAdminUsersHooks } from "./createAdminUsersHooks"; @@ -7,7 +7,7 @@ import { Context } from "~/types"; export interface CreateOktaConfig extends AuthenticatorConfig, - GroupAuthorizerConfig { + GroupsTeamsAuthorizerConfig { graphQLIdentityType?: string; } @@ -22,7 +22,7 @@ export const createOkta = ( issuer: config.issuer, getIdentity: config.getIdentity }), - createGroupAuthorizer({ + createGroupsTeamsAuthorizer({ identityType, getGroupSlug: config.getGroupSlug }), diff --git a/packages/api-security-okta/src/index.ts b/packages/api-security-okta/src/index.ts index 405cdb6364a..9414095b7b2 100644 --- a/packages/api-security-okta/src/index.ts +++ b/packages/api-security-okta/src/index.ts @@ -1,6 +1,20 @@ +import { + createGroupsTeamsAuthorizer, + type GroupsTeamsAuthorizerConfig +} from "@webiny/api-security"; + export { createIdentityType } from "./createIdentityType"; export { createAuthenticator } from "./createAuthenticator"; export type { AuthenticatorConfig } from "./createAuthenticator"; -export { createGroupAuthorizer } from "./createGroupAuthorizer"; -export type { GroupAuthorizerConfig } from "./createGroupAuthorizer"; export { createOkta } from "./createOkta"; + +export { createGroupsTeamsAuthorizer, type GroupsTeamsAuthorizerConfig }; + +// Backwards compatibility. +// @deprecated Use `createGroupsTeamsAuthorizer` instead. +const createGroupAuthorizer = createGroupsTeamsAuthorizer; + +// @deprecated Use `GroupsTeamsAuthorizerConfig` instead. +type GroupAuthorizerConfig = GroupsTeamsAuthorizerConfig; + +export { createGroupAuthorizer, type GroupAuthorizerConfig }; diff --git a/packages/api-security/src/index.ts b/packages/api-security/src/index.ts index 1365fe0c882..68653d48557 100644 --- a/packages/api-security/src/index.ts +++ b/packages/api-security/src/index.ts @@ -29,6 +29,7 @@ export interface SecurityConfig extends MultiTenancyAppConfig { export * from "./utils/AppPermissions"; export * from "./utils/getPermissionsFromSecurityGroupsForLocale"; export * from "./utils/IdentityValue"; +export * from "./utils/createGroupsTeamsAuthorizer"; type Context = SecurityContext & TenancyContext & WcpContext; diff --git a/packages/api-security/src/utils/createGroupsTeamsAuthorizer.ts b/packages/api-security/src/utils/createGroupsTeamsAuthorizer.ts new file mode 100644 index 00000000000..bddfbdba150 --- /dev/null +++ b/packages/api-security/src/utils/createGroupsTeamsAuthorizer.ts @@ -0,0 +1,84 @@ +import { ContextPlugin } from "@webiny/handler"; +import { SecurityContext } from "~/types"; +import { + GroupsTeamsAuthorizerConfig, + listPermissionsFromGroupsAndTeams +} from "./createGroupsTeamsAuthorizer/listPermissionsFromGroupsAndTeams"; + +export type { GroupsTeamsAuthorizerConfig }; + +export const createGroupsTeamsAuthorizer = ( + config: GroupsTeamsAuthorizerConfig +) => { + return new ContextPlugin(context => { + const { security, tenancy } = context; + security.addAuthorizer(async () => { + const identity = security.getIdentity(); + if (!identity) { + return null; + } + + // If `identityType` is specified, we'll only execute this authorizer for a matching identity. + if (config.identityType && identity.type !== config.identityType) { + return null; + } + + // @ts-expect-error Check `packages/api-security/src/plugins/tenantLinkAuthorization.ts:23`. + const locale = context.i18n?.getContentLocale(); + if (!locale) { + return null; + } + + if (config.canAccessTenant) { + const canAccessTenant = await config.canAccessTenant(context); + if (!canAccessTenant) { + return []; + } + } + + const currentTenantPermissions = await listPermissionsFromGroupsAndTeams({ + config, + context, + identity, + localeCode: locale.code + }); + + if (Array.isArray(currentTenantPermissions)) { + return currentTenantPermissions; + } + + // If no security groups were found, it could be due to an identity accessing a sub-tenant. In this case, + // let's try loading permissions from the parent tenant. Note that this will work well for flat tenant + // hierarchy where there's a `root` tenant and 1 level of sibling sub-tenants. For multi-level hierarchy, + // the best approach is to code a plugin with the desired permissions-fetching logic. + if (config.inheritGroupsFromParentTenant === false) { + return null; + } + + const parentTenantId = context.tenancy.getCurrentTenant().parent; + if (!parentTenantId) { + return null; + } + + const parentTenant = await tenancy.getTenantById(parentTenantId); + if (!parentTenant) { + return null; + } + + const parentTenantPermissions = await tenancy.withTenant(parentTenant, async () => { + return listPermissionsFromGroupsAndTeams({ + config, + context, + identity, + localeCode: locale.code + }); + }); + + if (Array.isArray(parentTenantPermissions)) { + return parentTenantPermissions; + } + + return null; + }); + }); +}; diff --git a/packages/api-security/src/utils/createGroupsTeamsAuthorizer/listPermissionsFromGroupsAndTeams.ts b/packages/api-security/src/utils/createGroupsTeamsAuthorizer/listPermissionsFromGroupsAndTeams.ts new file mode 100644 index 00000000000..4cc5ba32003 --- /dev/null +++ b/packages/api-security/src/utils/createGroupsTeamsAuthorizer/listPermissionsFromGroupsAndTeams.ts @@ -0,0 +1,128 @@ +import { getPermissionsFromSecurityGroupsForLocale } from "../getPermissionsFromSecurityGroupsForLocale"; +import { SecurityContext } from "~/types"; +import { Identity } from "@webiny/api-authentication/types"; + +export type GroupSlug = string | undefined; +export type TeamSlug = string | undefined; + +export interface GroupsTeamsAuthorizerConfig { + /** + * Specify an `identityType` if you want to only run this authorizer for specific identities. + */ + identityType?: string; + + /** + * @deprecated Use `listGroupSlugs` instead. + * Get a group slug to load permissions from. + */ + getGroupSlug?: (context: TContext) => Promise | GroupSlug; + + /** + * List group slugs to load permissions from. + */ + listGroupSlugs?: (context: TContext) => Promise | GroupSlug[]; + + /** + * List team slugs to load groups and ultimately permissions from. + */ + listTeamSlugs?: (context: TContext) => Promise | TeamSlug[]; + + /** + * If a security group is not found, try loading it from a parent tenant (default: true). + */ + inheritGroupsFromParentTenant?: boolean; + + /** + * Check whether the current identity is authorized to access the current tenant. + */ + canAccessTenant?: (context: TContext) => boolean | Promise; +} + +export interface ListPermissionsFromGroupsAndTeamsParams< + TContext extends SecurityContext = SecurityContext +> { + config: GroupsTeamsAuthorizerConfig; + identity: Identity; + localeCode: string; + context: TContext; +} + +export const listPermissionsFromGroupsAndTeams = async < + TContext extends SecurityContext = SecurityContext +>( + params: ListPermissionsFromGroupsAndTeamsParams +) => { + const { config, context, identity, localeCode } = params; + const { security, wcp } = context; + + // Load groups that are associated with the current identity. Also load groups + // that are assigned via one or more teams (if the Teams feature is enabled). + const groupSlugs: GroupSlug[] = []; + const teamSlugs: TeamSlug[] = []; + + if (config.getGroupSlug) { + const loadedGroupSlug = await config.getGroupSlug(context); + groupSlugs.push(loadedGroupSlug); + } + + if (config.listGroupSlugs) { + const loadedGroupSlugs = await config.listGroupSlugs(context); + groupSlugs.push(...loadedGroupSlugs); + } + + if (identity.group) { + groupSlugs.push(identity.group); + } + + if (identity.groups) { + groupSlugs.push(...identity.groups); + } + + if (wcp.canUseTeams()) { + // Load groups coming from teams. + if (config.listTeamSlugs) { + const loadedTeamSlugs = await config.listTeamSlugs(context); + teamSlugs.push(...loadedTeamSlugs); + } + + if (identity.team) { + teamSlugs.push(identity.team); + } + + if (identity.teams) { + teamSlugs.push(...identity.teams); + } + + const filteredTeamSlugs = teamSlugs.filter(Boolean) as string[]; + const dedupedTeamSlugs = Array.from(new Set(filteredTeamSlugs)); + + if (dedupedTeamSlugs.length > 0) { + const loadedTeams = await security.withoutAuthorization(() => { + return security.listTeams({ + where: { slug_in: dedupedTeamSlugs } + }); + }); + + const groupSlugsFromTeams = loadedTeams.map(team => team.groups).flat(); + groupSlugs.push(...groupSlugsFromTeams); + } + } + + const filteredGroupSlugs = groupSlugs.filter(Boolean) as string[]; + const dedupedGroupSlugs = Array.from(new Set(filteredGroupSlugs)); + + if (dedupedGroupSlugs.length > 0) { + // Load groups coming from teams. + const loadedGroups = await security.withoutAuthorization(() => { + return security.listGroups({ + where: { slug_in: dedupedGroupSlugs } + }); + }); + + if (loadedGroups.length > 0) { + return getPermissionsFromSecurityGroupsForLocale(loadedGroups, localeCode); + } + } + + return null; +}; diff --git a/packages/api-tenancy/src/createTenancy.ts b/packages/api-tenancy/src/createTenancy.ts index 12ece1c1047..22d99959a64 100644 --- a/packages/api-tenancy/src/createTenancy.ts +++ b/packages/api-tenancy/src/createTenancy.ts @@ -85,6 +85,13 @@ export async function createTenancy({ } return results; }, + async withTenant(tenant, cb) { + const initialTenant = this.getCurrentTenant(); + this.setCurrentTenant(tenant); + const result = await cb(tenant); + this.setCurrentTenant(initialTenant); + return result; + }, ...createSystemMethods({ storageOperations }), ...createTenantsMethods({ storageOperations, incrementWcpTenants, decrementWcpTenants }) }; diff --git a/packages/api-tenancy/src/types.ts b/packages/api-tenancy/src/types.ts index 7f099bc69d7..9826b54c06e 100644 --- a/packages/api-tenancy/src/types.ts +++ b/packages/api-tenancy/src/types.ts @@ -53,6 +53,10 @@ export interface Tenancy { tenants: TTenant[], cb: (tenant: TTenant) => Promise ): Promise; + withTenant( + tenant: TTenant, + cb: (tenant: TTenant) => Promise + ): Promise; } export interface TenancyContext extends BaseContext, DbContext, WcpContext {