Skip to content

Commit

Permalink
Add parseCustomAccess to access-control (#1643)
Browse files Browse the repository at this point in the history
  • Loading branch information
timleslie authored Sep 16, 2019
1 parent 9ece715 commit b61289b
Show file tree
Hide file tree
Showing 11 changed files with 283 additions and 89 deletions.
94 changes: 94 additions & 0 deletions .changeset/empty-dolls-fly/changes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
{
"releases": [{ "name": "@keystone-alpha/keystone", "type": "major" }],
"dependents": [
{
"name": "@keystone-alpha/api-tests",
"type": "patch",
"dependencies": [
"@keystone-alpha/adapter-knex",
"@keystone-alpha/adapter-mongoose",
"@keystone-alpha/test-utils",
"@keystone-alpha/keystone"
]
},
{
"name": "@keystone-alpha/demo-project-blog",
"type": "patch",
"dependencies": ["@keystone-alpha/adapter-mongoose", "@keystone-alpha/keystone"]
},
{
"name": "@keystone-alpha/demo-project-meetup",
"type": "patch",
"dependencies": ["@keystone-alpha/adapter-mongoose", "@keystone-alpha/keystone"]
},
{
"name": "@keystone-alpha/demo-project-todo",
"type": "patch",
"dependencies": ["@keystone-alpha/adapter-mongoose", "@keystone-alpha/keystone"]
},
{
"name": "@keystone-alpha/adapter-knex",
"type": "patch",
"dependencies": ["@keystone-alpha/keystone"]
},
{
"name": "@keystone-alpha/adapter-mongoose",
"type": "patch",
"dependencies": ["@keystone-alpha/keystone"]
},
{
"name": "@keystone-alpha/example-projects-blank",
"type": "patch",
"dependencies": ["@keystone-alpha/adapter-mongoose", "@keystone-alpha/keystone"]
},
{
"name": "@keystone-alpha/example-projects-nuxt",
"type": "patch",
"dependencies": ["@keystone-alpha/adapter-mongoose", "@keystone-alpha/keystone"]
},
{
"name": "@keystone-alpha/example-projects-starter",
"type": "patch",
"dependencies": ["@keystone-alpha/adapter-mongoose", "@keystone-alpha/keystone"]
},
{
"name": "@keystone-alpha/example-projects-todo",
"type": "patch",
"dependencies": ["@keystone-alpha/adapter-mongoose", "@keystone-alpha/keystone"]
},
{
"name": "@keystone-alpha/test-utils",
"type": "patch",
"dependencies": [
"@keystone-alpha/adapter-knex",
"@keystone-alpha/adapter-mongoose",
"@keystone-alpha/keystone"
]
},
{
"name": "@keystone-alpha/cypress-project-access-control",
"type": "patch",
"dependencies": ["@keystone-alpha/adapter-mongoose", "@keystone-alpha/keystone"]
},
{
"name": "@keystone-alpha/cypress-project-basic",
"type": "patch",
"dependencies": ["@keystone-alpha/adapter-mongoose", "@keystone-alpha/keystone"]
},
{
"name": "@keystone-alpha/cypress-project-client-validation",
"type": "patch",
"dependencies": ["@keystone-alpha/adapter-mongoose", "@keystone-alpha/keystone"]
},
{
"name": "@keystone-alpha/cypress-project-login",
"type": "patch",
"dependencies": ["@keystone-alpha/adapter-mongoose", "@keystone-alpha/keystone"]
},
{
"name": "@keystone-alpha/cypress-project-social-login",
"type": "patch",
"dependencies": ["@keystone-alpha/adapter-mongoose", "@keystone-alpha/keystone"]
}
]
}
1 change: 1 addition & 0 deletions .changeset/empty-dolls-fly/changes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow passing `{ access: ...}` when calling `keystone.extendGraphQLSchema()`. The `types` argument is now a list of objects of the form `{ access: ..., type: ...}`, rather than a list of strings.
1 change: 1 addition & 0 deletions .changeset/old-teachers-drum/changes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "releases": [{ "name": "@keystone-alpha/access-control", "type": "minor" }], "dependents": [] }
1 change: 1 addition & 0 deletions .changeset/old-teachers-drum/changes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `parseCustomAccess()` for parsing the access control directives on custom types/queries/mutations.
14 changes: 14 additions & 0 deletions api-tests/extend-graphql-schema/extend-graphql-schema.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ function setupKeystone(adapterName) {
{
schema: 'double(x: Int): Int',
resolver: (_, { x }) => 2 * x,
access: true,
},
],
mutations: [
{
schema: 'triple(x: Int): Int',
resolver: (_, { x }) => 3 * x,
access: { testing: true },
},
],
});
Expand All @@ -30,6 +32,18 @@ function setupKeystone(adapterName) {
multiAdapterRunners().map(({ runner, adapterName }) =>
describe(`Adapter: ${adapterName}`, () => {
describe('keystone.extendGraphQLSchema()', () => {
it(
'Sets up access control properly',
runner(setupKeystone, async ({ keystone }) => {
expect(keystone._extendedQueries.map(({ access }) => access)).toEqual([
{ testing: true },
]);
expect(keystone._extendedMutations.map(({ access }) => access)).toEqual([
{ testing: true },
]);
})
);

it(
'Executes custom queries correctly',
runner(setupKeystone, async ({ keystone }) => {
Expand Down
2 changes: 2 additions & 0 deletions packages/access-control/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
const {
parseCustomAccess,
parseListAccess,
parseFieldAccess,
validateListAccessControl,
validateFieldAccessControl,
} = require('./lib/access-control');

module.exports = {
parseCustomAccess,
parseListAccess,
parseFieldAccess,
validateListAccessControl,
Expand Down
135 changes: 70 additions & 65 deletions packages/access-control/lib/access-control.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,7 @@ const parseAccessCore = ({ accessTypes, access, defaultAccess, onGranularParseEr
}
};

const parseAccess = ({
schemaNames,
accessTypes,
access,
defaultAccess,
onGranularParseError,
validateGranularType,
}) => {
const parseAccess = ({ schemaNames, accessTypes, access, defaultAccess, parseAndValidate }) => {
// Check that none of the schemaNames match the accessTypes
if (intersection(schemaNames, accessTypes).length > 0) {
throw new Error(
Expand Down Expand Up @@ -71,79 +64,91 @@ const parseAccess = ({
return schemaNames.reduce(
(acc, schemaName) => ({
...acc,
[schemaName]: validateGranularConfigTypes(
parseAccessCore({
accessTypes,
access: namesProvided
? access.hasOwnProperty(schemaName) // If all the keys are in schemaNames, parse each on their own
? access[schemaName]
: defaultAccess
: access, // Otherwise, treat it as common across all schemaNames
defaultAccess,
onGranularParseError,
}),
validateGranularType
[schemaName]: parseAndValidate(
namesProvided
? access.hasOwnProperty(schemaName) // If all the keys are in schemaNames, parse each on their own
? access[schemaName]
: defaultAccess
: access
),
}),
{}
);
};

module.exports = {
parseListAccess({ listKey, defaultAccess, access = defaultAccess, schemaNames }) {
const accessTypes = ['create', 'read', 'update', 'delete', 'auth'];

return parseAccess({
schemaNames,
accessTypes,
access,
defaultAccess,
onGranularParseError: () => {
parseCustomAccess({ defaultAccess, access = defaultAccess, schemaNames }) {
const accessTypes = [];
const parseAndValidate = access => {
const type = getType(access);
if (!['Boolean', 'Function', 'Object'].includes(type)) {
throw new Error(
`Must specify one of ${JSON.stringify(
accessTypes
)} access configs, but got ${JSON.stringify(
Object.keys(access)
)}. (Did you mean to specify a declarative access control config? This can be done on a granular basis only)`
`Expected a Boolean, Object, or Function for custom access, but got ${type}`
);
},
validateGranularType: (type, accessType) => {
if (accessType === 'create') {
if (!['Boolean', 'Function'].includes(type)) {
return `Expected a Boolean, or Function for ${listKey}.access.${accessType}, but got ${type}. (NOTE: 'create' cannot have a Declarative access control config)`;
}
} else {
if (!['Object', 'Boolean', 'Function'].includes(type)) {
return `Expected a Boolean, Object, or Function for ${listKey}.access.${accessType}, but got ${type}`;
}
return access;
};
return parseAccess({ schemaNames, accessTypes, access, defaultAccess, parseAndValidate });
},

parseListAccess({ listKey, defaultAccess, access = defaultAccess, schemaNames }) {
const accessTypes = ['create', 'read', 'update', 'delete', 'auth'];
const parseAndValidate = access =>
validateGranularConfigTypes(
parseAccessCore({
accessTypes,
access,
defaultAccess,
onGranularParseError: () => {
throw new Error(
`Must specify one of ${JSON.stringify(
accessTypes
)} access configs, but got ${JSON.stringify(
Object.keys(access)
)}. (Did you mean to specify a declarative access control config? This can be done on a granular basis only)`
);
},
}),
(type, accessType) => {
if (accessType === 'create') {
if (!['Boolean', 'Function'].includes(type)) {
return `Expected a Boolean, or Function for ${listKey}.access.${accessType}, but got ${type}. (NOTE: 'create' cannot have a Declarative access control config)`;
}
} else {
if (!['Object', 'Boolean', 'Function'].includes(type)) {
return `Expected a Boolean, Object, or Function for ${listKey}.access.${accessType}, but got ${type}`;
}
}
}
},
});
);
return parseAccess({ schemaNames, accessTypes, access, defaultAccess, parseAndValidate });
},

parseFieldAccess({ listKey, fieldKey, defaultAccess, access = defaultAccess, schemaNames }) {
const accessTypes = ['create', 'read', 'update'];

return parseAccess({
schemaNames,
accessTypes,
access,
defaultAccess,
onGranularParseError: () => {
throw new Error(
`Must specify one of ${JSON.stringify(
accessTypes
)} access configs, but got ${JSON.stringify(
Object.keys(access)
)}. (Did you mean to specify a declarative access control config? This can be done on lists only)`
);
},
validateGranularType: (type, accessType) => {
if (!['Boolean', 'Function'].includes(type)) {
return `Expected a Boolean or Function for ${listKey}.fields.${fieldKey}.access.${accessType}, but got ${type}. (NOTE: Fields cannot have declarative access control config)`;
const parseAndValidate = access =>
validateGranularConfigTypes(
parseAccessCore({
accessTypes,
access,
defaultAccess,
onGranularParseError: () => {
throw new Error(
`Must specify one of ${JSON.stringify(
accessTypes
)} access configs, but got ${JSON.stringify(
Object.keys(access)
)}. (Did you mean to specify a declarative access control config? This can be done on lists only)`
);
},
}),
(type, accessType) => {
if (!['Boolean', 'Function'].includes(type)) {
return `Expected a Boolean or Function for ${listKey}.fields.${fieldKey}.access.${accessType}, but got ${type}. (NOTE: Fields cannot have declarative access control config)`;
}
}
},
});
);
return parseAccess({ schemaNames, accessTypes, access, defaultAccess, parseAndValidate });
},

validateListAccessControl({ access, listKey, operation, authentication = {}, originalInput }) {
Expand Down
56 changes: 56 additions & 0 deletions packages/access-control/tests/access-control.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
parseListAccess,
parseFieldAccess,
parseCustomAccess,
validateListAccessControl,
validateFieldAccessControl,
} from '../';
Expand Down Expand Up @@ -196,6 +197,61 @@ describe('Access control package tests', () => {
});
});

describe('parseCustomAccess', () => {
const statics = [true, false]; // type StaticAccess = boolean;
const imperatives = [() => true, () => false]; // type ImperativeAccess = AccessInput => boolean;
const where = { name: 'foo' }; // GraphQLWhere
const whereFn = () => where; // (AccessInput => GraphQLWhere)
const declaratives = [where, whereFn]; // type DeclarativeAccess = GraphQLWhere | (AccessInput => GraphQLWhere);
const schemaNames = ['public'];

test('StaticAccess | ImperativeAccess are valid defaults', () => {
[...statics, ...imperatives].forEach(defaultAccess => {
expect(parseCustomAccess({ defaultAccess, schemaNames })).toEqual({
public: defaultAccess,
});
});
});

test('StaticAccess | ImperativeAccess | DeclarativeAccess are valid access modes, and should override the defaults', () => {
[...statics, ...imperatives, ...declaratives].forEach(defaultAccess => {
[...statics, ...imperatives, ...declaratives].forEach(access => {
expect(parseCustomAccess({ defaultAccess, access, schemaNames })).toEqual({
public: access,
});
});

// Misc values are not valid per-operation access modes
expect(() => parseCustomAccess({ defaultAccess, access: 10, schemaNames })).toThrow(Error);
});
});

test('Misc values are not value inputs for defaultAccess', () => {
expect(() => parseCustomAccess({ defaultAccess: 10, schemaNames })).toThrow(Error);
});

test('Misc values are not value inputs for access', () => {
expect(() => parseCustomAccess({ access: 10, schemaNames })).toThrow(Error);
});

test('Schema names matching the access keys', () => {
const schemaNames = ['public', 'internal'];
const access = { public: true };
const defaultAccess = false;
expect(parseCustomAccess({ defaultAccess, access, schemaNames })).toEqual({
public: true,
internal: false,
});
});

test('Access keys which dont match the schema keys should throw', () => {
const schemaNames = ['public', 'internal'];
const access = { public: true, missing: false };
const defaultAccess = false;
expect(() => parseCustomAccess({ defaultAccess, access, schemaNames })).toThrow(Error);
});
});

test('validateListAccessControl', () => {
let operation = 'read';
const access = { [operation]: true };
Expand Down
Loading

0 comments on commit b61289b

Please sign in to comment.