Skip to main content

Rules

Rules represents what GraphQL operations can be performed by the requested user. Rules are generated by two simple steps, defining resource access and then mapping those resource access to respective GraphQL operations.

  • Control who can access what, we are using users's permission together with the CASL to define their abilities to manage specific resource
  • Whether Graphql request can be performed or not GraphQL Shield

Access Control

We use User's Permissions to evaluate what he/she can or can't access. By convention we have 4 types of permissions

  • Mange (Can perform complete CRUD)
  • View
  • Edit
  • Delete

So we need to map each of them with the cas ability, how its done is using a simple function which takes permission as param and define abilities for the users using its permissions.

import { AbilityBuilder, Ability } from '@casl/ability';
import { IModule, IPermissionType, IPermissionTypes, IPreDefinePermissions } from '@adminide-stack/core';
import { get } from 'lodash';

export function defineAbilityFor(permissions: Record<string, any>) {
const { can, build } = new AbilityBuilder(Ability);

/***
* IPreDefinePermissions = {
* manageResource: 'module.resource.manage'
* viewResource: 'module.resource.view',
* editResource: 'module.resource.edit',
* deleteResource: 'module.resource.delete'
* }
*/
const canManageResource = get(permissions, IPreDefinePermissions.manageResource) === IPermissionType.Allow;
const canViewResource = get(permissions, IPreDefinePermissions.viewResource) === IPermissionType.Allow;
const canEditResource = get(permissions, IPreDefinePermissions.editResource) === IPermissionType.Allow;
const canDeleteResource = get(permissions, IPreDefinePermissions.deleteResource) === IPermissionType.Allow;

if (canManageResource) {
can(IPermissionTypes.manage, IModule.Resource);
can(IPermissionTypes.view, IModule.Resource);
can(IPermissionTypes.delete, IModule.Resource);
can(IPermissionTypes.edit, IModule.Resource);
}
if (canViewResource) {
can(IPermissionTypes.view, IModule.Resource);
}
if (canEditResource) {
can(IPermissionTypes.edit, IModule.Resource);
}
if (canDeleteResource) {
can(IPermissionTypes.delete, IModule.Resource);
}
return build();
}

GraphQL Shield

Once we have abilities in place next step is to use them with the Graphql operations, this is where GraphQL Shield comes in. It lets you defined one to one mapping with the GraphQL operations you have, a simple resolver function which will then determine whether this operation can be performed or not

import { rule } from 'graphql-shield';
import { getPermissionsFromContext } from '@adminide-stack/platform-server';
// This if the function we've just defined above
import { defineAbilityFor } from '../ability';

const options = { cache: 'contextual' };
const rules = {
Query: {
getResource: rule(options)(async (parent, args, ctx) => {
const permissions = await getPermissionsFromContext(ctx);
return defineAbilityFor(permissions).can(IPermissionTypes.view, IModule.Resource);
}),
getResources: rule(options)(async (parent, args, ctx) => {
const permissions = await getPermissionsFromContext(ctx);
return defineAbilityFor(permissions).can(IPermissionTypes.view, IModule.Resource);
}),
},
Mutation: {
createResource: rule(options)(async (parent, args, ctx) => {
const permissions = await getPermissionsFromContext(ctx);
return defineAbilityFor(permissions).can(IPermissionTypes.create, IModule.Resource);
}),
updateResource: rule(options)(async (parent, args, ctx) => {
const permissions = await getPermissionsFromContext(ctx);
return defineAbilityFor(permissions).can(IPermissionTypes.edit, IModule.Resource);
}),
delete: rule(options)(async (parent, args, ctx) => {
const permissions = await getPermissionsFromContext(ctx);
return defineAbilityFor(permissions).can(IPermissionTypes.delete, IModule.Resource);
}),
},
Resource: {
nestedResource: rule(options)(async (parent, args, ctx) => {
const permissions = await getPermissionsFromContext(ctx);
return defineAbilityFor(permissions).can(IPermissionTypes.view, IModule.NestedResource);
}),
}
};

Once we have the rules object in place, the last step is to export it and pass it on to the Feature API

import { rules } from './rules';

export default new Feature({
// rest of the config
rules
})