From 381c4e2a852adceb587c0b06494a81ebbabcb0d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20Jedlicska?= <57442769+gjedlicska@users.noreply.github.com> Date: Tue, 19 Nov 2024 15:44:43 +0100 Subject: [PATCH] feat(gatekeeper): feature access resolver (#3514) --- .../gatekeeper/typedefs/gatekeeper.graphql | 7 +++++++ .../modules/core/graph/generated/graphql.ts | 15 +++++++++++++++ .../cross-server-sync/graph/generated/graphql.ts | 13 +++++++++++++ .../modules/gatekeeper/graph/resolvers/index.ts | 16 ++++++++++++++++ .../server/test/graphql/generated/graphql.ts | 13 +++++++++++++ 5 files changed, 64 insertions(+) diff --git a/packages/server/assets/gatekeeper/typedefs/gatekeeper.graphql b/packages/server/assets/gatekeeper/typedefs/gatekeeper.graphql index e3f7e884c..c414cd8de 100644 --- a/packages/server/assets/gatekeeper/typedefs/gatekeeper.graphql +++ b/packages/server/assets/gatekeeper/typedefs/gatekeeper.graphql @@ -84,4 +84,11 @@ extend type Workspace { subscription: WorkspaceSubscription @hasScope(scope: "workspace:billing") # this can only be created if there is an active subscription customerPortalUrl: String @hasScope(scope: "workspace:billing") + hasAccessToFeature(featureName: WorkspaceFeatureName!): Boolean! +} + +enum WorkspaceFeatureName { + domainBasedSecurityPolicies + oidcSso + workspaceDataRegionSpecificity } diff --git a/packages/server/modules/core/graph/generated/graphql.ts b/packages/server/modules/core/graph/generated/graphql.ts index 1faf1dc87..f88cd3bb8 100644 --- a/packages/server/modules/core/graph/generated/graphql.ts +++ b/packages/server/modules/core/graph/generated/graphql.ts @@ -4049,6 +4049,7 @@ export type Workspace = { domainBasedMembershipProtectionEnabled: Scalars['Boolean']['output']; /** Verified workspace domains */ domains?: Maybe>; + hasAccessToFeature: Scalars['Boolean']['output']; id: Scalars['ID']['output']; /** Only available to workspace owners/members */ invitedTeam?: Maybe>; @@ -4068,6 +4069,11 @@ export type Workspace = { }; +export type WorkspaceHasAccessToFeatureArgs = { + featureName: WorkspaceFeatureName; +}; + + export type WorkspaceInvitedTeamArgs = { filter?: InputMaybe; }; @@ -4178,6 +4184,13 @@ export type WorkspaceDomainDeleteInput = { workspaceId: Scalars['ID']['input']; }; +export enum WorkspaceFeatureName { + DomainBasedSecurityPolicies = 'domainBasedSecurityPolicies', + OidcSso = 'oidcSso', + Workspace = 'workspace', + WorkspaceDataRegionSpecificity = 'workspaceDataRegionSpecificity' +} + export type WorkspaceInviteCreateInput = { /** Either this or userId must be filled */ email?: InputMaybe; @@ -4790,6 +4803,7 @@ export type ResolversTypes = { WorkspaceCreateInput: WorkspaceCreateInput; WorkspaceDomain: ResolverTypeWrapper; WorkspaceDomainDeleteInput: WorkspaceDomainDeleteInput; + WorkspaceFeatureName: WorkspaceFeatureName; WorkspaceInviteCreateInput: WorkspaceInviteCreateInput; WorkspaceInviteLookupOptions: WorkspaceInviteLookupOptions; WorkspaceInviteMutations: ResolverTypeWrapper; @@ -6463,6 +6477,7 @@ export type WorkspaceResolvers; domainBasedMembershipProtectionEnabled?: Resolver; domains?: Resolver>, ParentType, ContextType>; + hasAccessToFeature?: Resolver>; id?: Resolver; invitedTeam?: Resolver>, ParentType, ContextType, Partial>; logo?: Resolver, ParentType, ContextType>; diff --git a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts index 26bd441c7..babbfdf3e 100644 --- a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts +++ b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts @@ -4030,6 +4030,7 @@ export type Workspace = { domainBasedMembershipProtectionEnabled: Scalars['Boolean']['output']; /** Verified workspace domains */ domains?: Maybe>; + hasAccessToFeature: Scalars['Boolean']['output']; id: Scalars['ID']['output']; /** Only available to workspace owners/members */ invitedTeam?: Maybe>; @@ -4049,6 +4050,11 @@ export type Workspace = { }; +export type WorkspaceHasAccessToFeatureArgs = { + featureName: WorkspaceFeatureName; +}; + + export type WorkspaceInvitedTeamArgs = { filter?: InputMaybe; }; @@ -4159,6 +4165,13 @@ export type WorkspaceDomainDeleteInput = { workspaceId: Scalars['ID']['input']; }; +export enum WorkspaceFeatureName { + DomainBasedSecurityPolicies = 'domainBasedSecurityPolicies', + OidcSso = 'oidcSso', + Workspace = 'workspace', + WorkspaceDataRegionSpecificity = 'workspaceDataRegionSpecificity' +} + export type WorkspaceInviteCreateInput = { /** Either this or userId must be filled */ email?: InputMaybe; diff --git a/packages/server/modules/gatekeeper/graph/resolvers/index.ts b/packages/server/modules/gatekeeper/graph/resolvers/index.ts index 36c72fd16..aebed8b00 100644 --- a/packages/server/modules/gatekeeper/graph/resolvers/index.ts +++ b/packages/server/modules/gatekeeper/graph/resolvers/index.ts @@ -22,6 +22,7 @@ import { getWorkspaceSubscriptionFactory, saveCheckoutSessionFactory } from '@/modules/gatekeeper/repositories/billing' +import { canWorkspaceAccessFeatureFactory } from '@/modules/gatekeeper/services/featureAuthorization' const { FF_GATEKEEPER_MODULE_ENABLED } = getFeatureFlags() @@ -69,6 +70,21 @@ export = FF_GATEKEEPER_MODULE_ENABLED workspaceSlug: workspace.slug, customerId: workspaceSubscription.subscriptionData.customerId }) + }, + hasAccessToFeature: async (parent, args, ctx) => { + await authorizeResolver( + ctx.userId, + parent.id, + Roles.Workspace.Member, + ctx.resourceAccessRules + ) + const hasAccess = await canWorkspaceAccessFeatureFactory({ + getWorkspacePlan: getWorkspacePlanFactory({ db }) + })({ + workspaceId: parent.id, + workspaceFeature: args.featureName + }) + return hasAccess } }, WorkspaceMutations: () => ({}), diff --git a/packages/server/test/graphql/generated/graphql.ts b/packages/server/test/graphql/generated/graphql.ts index ae1ab8a91..a14fd3f55 100644 --- a/packages/server/test/graphql/generated/graphql.ts +++ b/packages/server/test/graphql/generated/graphql.ts @@ -4031,6 +4031,7 @@ export type Workspace = { domainBasedMembershipProtectionEnabled: Scalars['Boolean']['output']; /** Verified workspace domains */ domains?: Maybe>; + hasAccessToFeature: Scalars['Boolean']['output']; id: Scalars['ID']['output']; /** Only available to workspace owners/members */ invitedTeam?: Maybe>; @@ -4050,6 +4051,11 @@ export type Workspace = { }; +export type WorkspaceHasAccessToFeatureArgs = { + featureName: WorkspaceFeatureName; +}; + + export type WorkspaceInvitedTeamArgs = { filter?: InputMaybe; }; @@ -4160,6 +4166,13 @@ export type WorkspaceDomainDeleteInput = { workspaceId: Scalars['ID']['input']; }; +export enum WorkspaceFeatureName { + DomainBasedSecurityPolicies = 'domainBasedSecurityPolicies', + OidcSso = 'oidcSso', + Workspace = 'workspace', + WorkspaceDataRegionSpecificity = 'workspaceDataRegionSpecificity' +} + export type WorkspaceInviteCreateInput = { /** Either this or userId must be filled */ email?: InputMaybe;