@cran/gql.core
Version:
Cran/GraphQL Core Utilities
123 lines (122 loc) • 5.42 kB
JavaScript
/* eslint-disable max-lines */
import { PluginPhase } from "./Plugin";
import { createDirective } from "../utilities/createDirective";
import { reportMissing } from "../utilities/reportMissing";
import { withSchemaFiltering } from "../addons/withSchemaFiltering";
import { MapperKind, mapSchema } from "@graphql-tools/utils";
import { defaultFieldResolver, getNamedType } from "graphql";
import { getExtension, setExtension } from "../utilities/extensions";
// eslint-disable-next-line max-lines-per-function
export function withRbac(options) {
const { name = "rbac", enumName = "role", publicName = "public", rolesParameter = "roles", defaultGrants = {}, } = options;
const roles = [...options.roles, publicName,].map(upper);
if (!options.denyPublic) {
defaultGrants[publicName] = true;
}
withSchemaFiltering.filter.types.push(function filterType(type, context, schema) {
if (!context) {
return true;
} // internal call
const usedTypes = getExtension(schema, "usedTypes");
const userRoles = (context[rolesParameter] || [publicName,]).map(upper);
for (const role of userRoles) {
const types = usedTypes[role] || usedTypes[publicName];
if (types[type.name]) {
return true;
}
}
return false;
});
withSchemaFiltering.filter.fields.push(function filterField(fieldConfig, context) {
if (!context) {
return true;
} // internal call
return isAuthorized(getExtension(fieldConfig, "granted"), context[rolesParameter]);
});
return [{
typeDefs: [
`enum ${enumName}{${roles.map(upper).join(" ")}}`,
],
transformer: {
// eslint-disable-next-line max-statements, max-lines-per-function
[PluginPhase.FINALIZE](schema) {
const directives = schema.getDirectives()
.filter(function filterDirective(directive) {
return withSchemaFiltering
.filterDirectives(directive, null, schema);
});
for (const directive of directives) {
for (const arg of directive.args) {
setExtension(getNamedType(arg.type), "authRequired", true);
}
}
const emptyAuth = {
grant: [], revoke: [],
};
const usedTypes = {};
for (const role of roles) {
usedTypes[role] = {};
}
setExtension(schema, "usedTypes", usedTypes);
return mapSchema(schema, {
[MapperKind.COMPOSITE_FIELD](fieldConfig, _fieldName, typeName) {
const { resolve = defaultFieldResolver, } = fieldConfig;
const typeExt = getExtension(schema.getType(typeName), "auth", emptyAuth);
const fieldExt = getExtension(fieldConfig, "auth", emptyAuth);
const granted = grants(fieldExt.grant, fieldExt.revoke, grants(typeExt.grant, typeExt.revoke, defaultGrants));
setExtension(fieldConfig, "granted", granted);
for (const grant in granted) {
usedTypes[grant][getNamedType(fieldConfig.type).name] = true;
for (const arg in (fieldConfig.args || {})) {
usedTypes[grant][getNamedType(fieldConfig.args[arg].type).name] = true;
}
}
fieldConfig.resolve = function resolver(source, args, context, info) {
if (!isAuthorized(granted, context[rolesParameter])) {
return void reportMissing(info);
}
return resolve.call(this, source, args, context, info);
};
return fieldConfig;
},
});
},
},
}, createDirective(name, {
grant: `[${enumName}!]`, revoke: `[${enumName}!]`,
}, {
[MapperKind.TYPE]([directive,], type) {
setExtension(type, "auth", {
grant: directive.grant || [], revoke: directive.revoke || [],
});
},
[MapperKind.COMPOSITE_FIELD]([directive,], fieldConfig) {
setExtension(fieldConfig, "auth", {
grant: directive.grant || [], revoke: directive.revoke || [],
});
},
}),];
}
function isAuthorized(granted, roles) {
if (roles) {
for (const role of roles) {
if (granted[role]) {
return true;
}
}
} // TODO else public
return false;
}
function upper(value) {
return value.toUpperCase();
}
function grants(grant, revoke, initial) {
const roles = { ...initial, };
for (const role of grant) {
roles[role] = true;
}
for (const role of revoke) {
delete roles[role];
}
return roles;
}