Skip to content

Commit

Permalink
feat: type safety for gql perms (#9798)
Browse files Browse the repository at this point in the history
Signed-off-by: Matt Krick <[email protected]>
  • Loading branch information
mattkrick authored May 30, 2024
1 parent 9487c1e commit 712f79e
Show file tree
Hide file tree
Showing 5 changed files with 48 additions and 9 deletions.
17 changes: 13 additions & 4 deletions packages/server/graphql/public/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,16 @@ const permissionMap: PermissionMap<Resolvers> = {
verifyEmail: rateLimit({perMinute: 50, perHour: 100}),
addApprovedOrganizationDomains: or(
isSuperUser,
and(isViewerBillingLeader('args.orgId'), isOrgTier('args.orgId', 'enterprise'))
and(
isViewerBillingLeader<'Mutation.addApprovedOrganizationDomains'>('args.orgId'),
isOrgTier<'Mutation.addApprovedOrganizationDomains'>('args.orgId', 'enterprise')
)
),
removeApprovedOrganizationDomains: or(isSuperUser, isViewerBillingLeader('args.orgId')),
uploadIdPMetadata: isViewerOnOrg('args.orgId'),
removeApprovedOrganizationDomains: or(
isSuperUser,
isViewerBillingLeader<'Mutation.removeApprovedOrganizationDomains'>('args.orgId')
),
uploadIdPMetadata: isViewerOnOrg<'Mutation.uploadIdPMetadata'>('args.orgId'),
updateTemplateCategory: isViewerOnTeam(getTeamIdFromArgTemplateId)
},
Query: {
Expand All @@ -63,7 +69,10 @@ const permissionMap: PermissionMap<Resolvers> = {
SAMLIdP: rateLimit({perMinute: 120, perHour: 3600})
},
Organization: {
saml: and(isViewerBillingLeader('source.id'), isOrgTier('source.id', 'enterprise'))
saml: and(
isViewerBillingLeader<'Organization.saml'>('source.id'),
isOrgTier<'Organization.saml'>('source.id', 'enterprise')
)
},
User: {
domains: or(isSuperUser, isUserViewer)
Expand Down
34 changes: 32 additions & 2 deletions packages/server/graphql/public/rules/getResolverDotPath.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,39 @@
import {FirstParam} from '../../../../client/types/generics'
import {Resolvers} from '../resolverTypes'

export const getResolverDotPath = (
dotPath: ResolverDotPath,
dotPath: `${'source' | 'args'}.${string}`,
source: Record<string, any>,
args: Record<string, any>
) => {
return dotPath.split('.').reduce((val: any, key) => val?.[key], {source, args})
}

export type ResolverDotPath = `source.${string}` | `args.${string}`
type SecondParam<T> = T extends (arg1: any, arg2: infer A, ...args: any[]) => any ? A : never

type ParseParent<T> = T extends `${infer Parent extends string}.${string}` ? Parent : never
type ParseChild<T> = T extends `${string}.${infer Child extends string}` ? Child : never

type ExtractTypeof<T extends keyof Resolvers> = '__isTypeOf' extends keyof NonNullable<Resolvers[T]>
? NonNullable<Resolvers[T]>['__isTypeOf']
: never
type ExtractParent<T extends keyof Resolvers> = FirstParam<NonNullable<ExtractTypeof<T>>>

type Source<T> =
ParseParent<T> extends keyof Resolvers
? ExtractParent<ParseParent<T>> extends never
? never
: keyof ExtractParent<ParseParent<T>> & string
: never

type ExtractChild<TOp, TChild extends string> = TChild extends keyof TOp
? NonNullable<TOp[TChild]>
: never

type Arg<T> =
ParseParent<T> extends keyof Resolvers
? keyof SecondParam<ExtractChild<NonNullable<Resolvers[ParseParent<T>]>, ParseChild<T>>> &
string
: never

export type ResolverDotPath<T> = `source.${Source<T>}` | `args.${Arg<T>}`
2 changes: 1 addition & 1 deletion packages/server/graphql/public/rules/isOrgTier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {GQLContext} from '../../graphql'
import {TierEnum} from '../resolverTypes'
import {ResolverDotPath, getResolverDotPath} from './getResolverDotPath'

export const isOrgTier = (orgIdDotPath: ResolverDotPath, requiredTier: TierEnum) =>
export const isOrgTier = <T>(orgIdDotPath: ResolverDotPath<T>, requiredTier: TierEnum) =>
rule(`isViewerOnOrg-${orgIdDotPath}-${requiredTier}`, {cache: 'strict'})(
async (source, args, {dataLoader}: GQLContext) => {
const orgId = getResolverDotPath(orgIdDotPath, source, args)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {getUserId} from '../../../utils/authorization'
import {GQLContext} from '../../graphql'
import {ResolverDotPath, getResolverDotPath} from './getResolverDotPath'

export const isViewerBillingLeader = (orgIdDotPath: ResolverDotPath) =>
export const isViewerBillingLeader = <T>(orgIdDotPath: ResolverDotPath<T>) =>
rule(`isViewerBillingLeader-${orgIdDotPath}`, {cache: 'strict'})(
async (source, args, {authToken, dataLoader}: GQLContext) => {
const orgId = getResolverDotPath(orgIdDotPath, source, args)
Expand Down
2 changes: 1 addition & 1 deletion packages/server/graphql/public/rules/isViewerOnOrg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {getUserId} from '../../../utils/authorization'
import {GQLContext} from '../../graphql'
import {ResolverDotPath, getResolverDotPath} from './getResolverDotPath'

export const isViewerOnOrg = (orgIdDotPath: ResolverDotPath) =>
export const isViewerOnOrg = <T>(orgIdDotPath: ResolverDotPath<T>) =>
rule(`isViewerOnOrg-${orgIdDotPath}`, {cache: 'strict'})(
async (source, args, {authToken, dataLoader}: GQLContext) => {
const orgId = getResolverDotPath(orgIdDotPath, source, args)
Expand Down

0 comments on commit 712f79e

Please sign in to comment.