maticzav / graphql-shield

🛡 A GraphQL tool to ease the creation of permission layer.

Home Page:https://graphql-shield.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

How to modularize GraphQL Shield permissions

tsongas opened this issue · comments

I'm new to GraphQL and am using the technique described in this post to modularize my GraphQL schema code so the types, queries, mutations, and resolvers for each model are in a separate file.

Since my project has ~45 models, I'm happy with this approach and would like to also include the permissions for each model in that model's file (i.e. author.js and book.js in that linked post) and I'm wondering if anyone has an example of breaking up permissions between multiple files? In the advanced example everything is lumped together in one file.

Hey @tsongas 👋,

Thank you for opening an issue. We will get back to you as soon as we can. Have you seen our Open Collective page? Please consider contributing financially to our project. This will help us involve more contributors and get to issues like yours faster.

https://opencollective.com/graphql-shield

We offer priority support for all financial contributors. Don't forget to add priority label once you become one! 😄

Hmm I donated but I think only contributors can add labels?

I was going to ask something similar since I am using graphql-modules and both are supported by The Guild. Modules has its own middleware approach and does not use graphql-middleware, so I was wondering what is the convention of using both shield and graphql-modules together?

Here's some code I used for my personal project, which handles merging rules. Hope it can help someone:

import type {
	ShieldRule,
	IRuleFieldMap,
	IRuleTypeMap,
} from 'graphql-shield/dist/types'
import { merge, capitalize } from 'lodash'
import { graphqlLogger } from './util'

export const Rules: IRuleTypeMap = {}

function isShieldRule(
	rule: ShieldRule | IRuleFieldMap | IRuleTypeMap
): rule is ShieldRule {
	// FieldMaps and TypeMaps have the Object constructor, whereas
	// Rule and logicRule have a dedicated constructor (e.g. Rule, RuleAnd, RuleTrue, ...).
	return rule.constructor.name !== 'Object'
}

enum ParentType {
	NONE,
	QUERY,
	MUTATION,
	SUBSCRIPTION,
}

export function mergeShieldRule(
	type: ParentType,
	field: string,
	rule: ShieldRule | IRuleFieldMap
) {
	let existingRule
	const fieldFullName =
		type === ParentType.NONE
			? field
			: `${capitalize(ParentType[type])}.${field}`

	if (type === ParentType.NONE) {
		existingRule = Rules[field]
	} else {
		const accessor = capitalize(ParentType[type])

		// Query, Mutation and Subscription are already RuleFieldMaps.
		// Fields inside must be ShieldRules.
		// We can't have a RuleFieldMap inside a RuleFieldMap.
		if (!isShieldRule(rule)) {
			throw new Error(`Tried to assign RuleFieldMap to ${fieldFullName}`)
		}

		// Convert a ShieldRule on Query, Mutation or Subscription to a RuleFieldMap.
		// So { Query: allow } becomes { Query: { '*': allow }}.
		// This ensures the new field can correctly be added.
		const existingQMSRule = Rules[accessor]
		if (existingQMSRule === undefined) {
			graphqlLogger.debug(
				`${accessor} type does not exist currently, initiating RuleFieldMap`
			)
			Rules[accessor] = {}
		} else if (isShieldRule(existingQMSRule)) {
			graphqlLogger.debug(
				`${accessor} type is currently a ShieldRule, converting to a RuleFieldMap`
			)
			Rules[accessor] = { '*': existingQMSRule }
		}

		existingRule = (Rules[accessor] as IRuleFieldMap)[field]
	}

	// If there is no rule that currently exist, just assign the new rule.
	let newRule = rule

	if (!existingRule) {
		graphqlLogger.debug(
			`Assigning ${
				isShieldRule(newRule) ? 'ShieldRule' : 'RuleFieldMap'
			} to type ${fieldFullName}`
		)
	} else if (isShieldRule(existingRule)) {
		if (isShieldRule(rule)) {
			// If the existing rule and the new rule are both single rules that apply to the whole type, we will overwrite the existing rule with the new rule.
			graphqlLogger.debug(
				`Type ${fieldFullName} already has an active ShieldRule. Overwriting with another ShieldRule`
			)
		} else {
			graphqlLogger.debug(
				`Type ${fieldFullName} already has an active ShieldRule. Overwriting with a RuleFieldMap. The old rule will still be the default rule if no other default rule has been specified.`
			)

			newRule = merge({ '*': existingRule }, rule)
		}
	} else if (isShieldRule(rule)) {
		graphqlLogger.debug(
			`Type ${fieldFullName} already has an active RuleFieldMap. Assigning new ShieldRule as default rule.`
		)
		newRule = merge(existingRule, {
			'*': rule,
		})
	} else {
		graphqlLogger.debug(
			`Type ${fieldFullName} already has an active RuleFieldMap. Merging with new RuleFieldMap.`
		)
		newRule = merge(existingRule, rule)
	}

	if (type === ParentType.NONE) {
		Rules[field] = newRule
	} else {
		// We already ensured that Query, Mutation and Subscription are IRuleFieldMaps
		// and the new rule for the field is a ShieldRule.
		;(Rules[capitalize(ParentType[type])] as IRuleFieldMap)[
			field
		] = newRule as ShieldRule
	}
}

Here's how you could use the above function for a nexus integration. That way you can define the rules with your types in a modular way.

import { objectType, extendType, queryField, mutationField } from 'nexus'
import type {
	NexusObjectTypeConfig,
	NexusExtendTypeConfig,
	GetGen,
	FieldOutConfig,
} from 'nexus/dist/core'

export const shieldedObjectType = <TypeName extends string>(
	config: NexusObjectTypeConfig<TypeName> & {
		rules: ShieldRule | IRuleFieldMap
	}
) => {
	mergeShieldRule(ParentType.NONE, config.name, config.rules)

	return objectType(config)
}

export const shieldedExtendType = <
	TypeName extends GetGen<'objectNames', string> | 'Query' | 'Mutation'
>(
	config: NexusExtendTypeConfig<TypeName> & {
		rules?: ShieldRule | IRuleFieldMap
	}
) => {
	if (config.rules) {
		mergeShieldRule(ParentType.NONE, config.type, config.rules)
	}

	return extendType(config)
}

export const shieldedQueryField = <FieldName extends string>(
	fieldName: FieldName,
	config:
		| (FieldOutConfig<'Query', FieldName> & {
				rule: ShieldRule
		  })
		| (() => FieldOutConfig<'Query', FieldName> & {
				rule: ShieldRule
		  })
) => {
	if (typeof config === 'function') {
		config = config()
	}

	mergeShieldRule(ParentType.QUERY, fieldName, config.rule)

	return queryField(fieldName, config)
}

export const shieldedMutationField = <FieldName extends string>(
	fieldName: FieldName,
	config:
		| (FieldOutConfig<'Mutation', FieldName> & { rule: ShieldRule })
		| (() => FieldOutConfig<'Mutation', FieldName> & { rule: ShieldRule })
) => {
	if (typeof config === 'function') {
		config = config()
	}

	mergeShieldRule(ParentType.MUTATION, fieldName, config.rule)

	return mutationField(fieldName, config)
}
commented

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.