mirror of
https://github.com/strapi/strapi.git
synced 2025-12-17 10:14:19 +00:00
Handle authorization for GraphQL queries & mutations
This commit is contained in:
parent
ee5e7a026f
commit
9c48580991
@ -115,6 +115,10 @@ class Strapi {
|
|||||||
return this.container.get('apis').getAll();
|
return this.container.get('apis').getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get auth() {
|
||||||
|
return this.container.get('auth');
|
||||||
|
}
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
try {
|
try {
|
||||||
if (!this.isLoaded) {
|
if (!this.isLoaded) {
|
||||||
@ -400,7 +404,7 @@ class Strapi {
|
|||||||
/**
|
/**
|
||||||
* Binds queries with a specific model
|
* Binds queries with a specific model
|
||||||
* @param {string} uid
|
* @param {string} uid
|
||||||
* @returns {}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
query(uid) {
|
query(uid) {
|
||||||
return this.db.query(uid);
|
return this.db.query(uid);
|
||||||
|
|||||||
52
packages/plugins/graphql/server/bootstrap.js
vendored
52
packages/plugins/graphql/server/bootstrap.js
vendored
@ -3,8 +3,8 @@
|
|||||||
const { isEmpty, mergeWith, isArray } = require('lodash/fp');
|
const { isEmpty, mergeWith, isArray } = require('lodash/fp');
|
||||||
const { ApolloServer } = require('apollo-server-koa');
|
const { ApolloServer } = require('apollo-server-koa');
|
||||||
const {
|
const {
|
||||||
ApolloServerPluginLandingPageLocalDefault,
|
ApolloServerPluginLandingPageDisabled,
|
||||||
ApolloServerPluginLandingPageProductionDefault,
|
ApolloServerPluginLandingPageGraphQLPlayground,
|
||||||
} = require('apollo-server-core');
|
} = require('apollo-server-core');
|
||||||
const depthLimit = require('graphql-depth-limit');
|
const depthLimit = require('graphql-depth-limit');
|
||||||
const { graphqlUploadKoa } = require('graphql-upload');
|
const { graphqlUploadKoa } = require('graphql-upload');
|
||||||
@ -35,15 +35,10 @@ module.exports = async strapi => {
|
|||||||
schema,
|
schema,
|
||||||
|
|
||||||
// Initialize loaders for this request.
|
// Initialize loaders for this request.
|
||||||
context: ({ ctx }) => {
|
context: ({ ctx }) => ({
|
||||||
// TODO: set loaders in the context not globally
|
state: ctx.state,
|
||||||
strapi
|
koaContext: ctx,
|
||||||
.plugin('graphql')
|
}),
|
||||||
.service('old')
|
|
||||||
['data-loaders'].initializeLoader();
|
|
||||||
|
|
||||||
return ctx;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Validation
|
// Validation
|
||||||
validationRules: [depthLimit(config('depthLimit'))],
|
validationRules: [depthLimit(config('depthLimit'))],
|
||||||
@ -54,10 +49,9 @@ module.exports = async strapi => {
|
|||||||
bodyParserConfig: true,
|
bodyParserConfig: true,
|
||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
// Specify which GraphQL landing page we want for the different env.
|
process.env.NODE_ENV === 'production'
|
||||||
process.env.NODE_ENV !== 'production'
|
? ApolloServerPluginLandingPageDisabled()
|
||||||
? ApolloServerPluginLandingPageLocalDefault({ footer: false })
|
: ApolloServerPluginLandingPageGraphQLPlayground(),
|
||||||
: ApolloServerPluginLandingPageProductionDefault({ footer: false }),
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -77,10 +71,30 @@ module.exports = async strapi => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Link the Apollo server & the Strapi app
|
// Link the Apollo server & the Strapi app
|
||||||
server.applyMiddleware({
|
strapi.server.routes([
|
||||||
app: strapi.server.app,
|
{
|
||||||
path: config('endpoint', '/graphql'),
|
method: 'ALL',
|
||||||
});
|
path: config('endpoint', '/graphql'),
|
||||||
|
handler: [
|
||||||
|
(ctx, next) => {
|
||||||
|
ctx.state.route = {
|
||||||
|
info: {
|
||||||
|
// Indicate it's a content API route
|
||||||
|
type: 'content-api',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return strapi.auth.authenticate(ctx, next);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Apollo Server
|
||||||
|
server.getMiddleware({ path: config('endpoint', '/graphql') }),
|
||||||
|
],
|
||||||
|
config: {
|
||||||
|
auth: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
// Register destroy behavior
|
// Register destroy behavior
|
||||||
// We're doing it here instead of exposing a destroy method to the strapi-server.js
|
// We're doing it here instead of exposing a destroy method to the strapi-server.js
|
||||||
|
|||||||
@ -133,6 +133,30 @@ module.exports = ({ strapi }) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
buildCollectionTypeMutations(contentType) {
|
buildCollectionTypeMutations(contentType) {
|
||||||
|
getService('extension')
|
||||||
|
.for('content-api')
|
||||||
|
.use(() => ({
|
||||||
|
resolversConfig: {
|
||||||
|
[`Mutation.${getCreateMutationTypeName(contentType)}`]: {
|
||||||
|
auth: {
|
||||||
|
scope: [`${contentType.uid}.create`],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
[`Mutation.${getUpdateMutationTypeName(contentType)}`]: {
|
||||||
|
auth: {
|
||||||
|
scope: [`${contentType.uid}.update`],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
[`Mutation.${getDeleteMutationTypeName(contentType)}`]: {
|
||||||
|
auth: {
|
||||||
|
scope: [`${contentType.uid}.delete`],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
return extendType({
|
return extendType({
|
||||||
type: 'Mutation',
|
type: 'Mutation',
|
||||||
|
|
||||||
|
|||||||
@ -92,6 +92,24 @@ module.exports = ({ strapi }) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
buildSingleTypeMutations(contentType) {
|
buildSingleTypeMutations(contentType) {
|
||||||
|
getService('extension')
|
||||||
|
.for('content-api')
|
||||||
|
.use(() => ({
|
||||||
|
resolversConfig: {
|
||||||
|
[`Mutation.${getUpdateMutationTypeName(contentType)}`]: {
|
||||||
|
auth: {
|
||||||
|
scope: [`${contentType.uid}.update`],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
[`Mutation.${getDeleteMutationTypeName(contentType)}`]: {
|
||||||
|
auth: {
|
||||||
|
scope: [`${contentType.uid}.delete`],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
return extendType({
|
return extendType({
|
||||||
type: 'Mutation',
|
type: 'Mutation',
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,24 @@ module.exports = ({ strapi }) => {
|
|||||||
} = naming;
|
} = naming;
|
||||||
|
|
||||||
const buildCollectionTypeQueries = contentType => {
|
const buildCollectionTypeQueries = contentType => {
|
||||||
|
getService('extension')
|
||||||
|
.for('content-api')
|
||||||
|
.use(() => ({
|
||||||
|
resolversConfig: {
|
||||||
|
[`Query.${getFindOneQueryName(contentType)}`]: {
|
||||||
|
auth: {
|
||||||
|
scope: [`${contentType.uid}.findOne`],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
[`Query.${getFindQueryName(contentType)}`]: {
|
||||||
|
auth: {
|
||||||
|
scope: [{ name: `${contentType.uid}.find`, type: 'read-only' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
return extendType({
|
return extendType({
|
||||||
type: 'Query',
|
type: 'Query',
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,18 @@ module.exports = ({ strapi }) => {
|
|||||||
const { getFindOneQueryName, getEntityResponseName } = naming;
|
const { getFindOneQueryName, getEntityResponseName } = naming;
|
||||||
|
|
||||||
const buildSingleTypeQueries = contentType => {
|
const buildSingleTypeQueries = contentType => {
|
||||||
|
getService('extension')
|
||||||
|
.for('content-api')
|
||||||
|
.use(() => ({
|
||||||
|
resolversConfig: {
|
||||||
|
[`Query.${getFindOneQueryName(contentType)}`]: {
|
||||||
|
auth: {
|
||||||
|
scope: [`${contentType.uid}.findOne`],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
return extendType({
|
return extendType({
|
||||||
type: 'Query',
|
type: 'Query',
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,19 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { isNil, get, getOr, isFunction, first } = require('lodash/fp');
|
const { get, getOr, isFunction, first } = require('lodash/fp');
|
||||||
|
|
||||||
|
const { GraphQLObjectType } = require('graphql');
|
||||||
const { createPoliciesMiddleware } = require('./policy');
|
const { createPoliciesMiddleware } = require('./policy');
|
||||||
|
|
||||||
|
const ignoredObjectTypes = [
|
||||||
|
'__Schema',
|
||||||
|
'__Type',
|
||||||
|
'__Field',
|
||||||
|
'__InputValue',
|
||||||
|
'__EnumValue',
|
||||||
|
'__Directive',
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrap the schema's resolvers if they've been
|
* Wrap the schema's resolvers if they've been
|
||||||
* customized using the GraphQL extension service
|
* customized using the GraphQL extension service
|
||||||
@ -19,31 +29,31 @@ const wrapResolvers = ({ schema, strapi, extension = {} }) => {
|
|||||||
|
|
||||||
// Fields filters
|
// Fields filters
|
||||||
const isValidFieldName = ([field]) => !field.startsWith('__');
|
const isValidFieldName = ([field]) => !field.startsWith('__');
|
||||||
const hasResolverConfig = type => ([field]) => !isNil(resolversConfig[`${type}.${field}`]);
|
|
||||||
|
|
||||||
const typeMap = get('_typeMap', schema);
|
const typeMap = schema.getTypeMap();
|
||||||
|
|
||||||
// Iterate over every field from every type within the
|
// Iterate over every field from every type within the
|
||||||
// schema's type map and wrap its resolve attribute if needed
|
// schema's type map and wrap its resolve attribute if needed
|
||||||
Object.entries(typeMap).forEach(([type, definition]) => {
|
Object.entries(typeMap).forEach(([type, definition]) => {
|
||||||
const fields = get('_fields', definition);
|
const isGraphQLObjectType = definition instanceof GraphQLObjectType;
|
||||||
|
const isIgnoredType = ignoredObjectTypes.includes(type);
|
||||||
|
|
||||||
if (isNil(fields)) {
|
if (!isGraphQLObjectType || isIgnoredType) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fieldsToProcess = Object.entries(fields)
|
const fields = definition.getFields();
|
||||||
// Ignore patterns such as "__FooBar"
|
const fieldsToProcess = Object.entries(fields).filter(isValidFieldName);
|
||||||
.filter(isValidFieldName)
|
|
||||||
// Don't augment the types if there isn't any configuration defined for them
|
|
||||||
.filter(hasResolverConfig(type));
|
|
||||||
|
|
||||||
for (const [fieldName, fieldDefinition] of fieldsToProcess) {
|
for (const [fieldName, fieldDefinition] of fieldsToProcess) {
|
||||||
|
const defaultResolver = get(fieldName);
|
||||||
|
|
||||||
const path = `${type}.${fieldName}`;
|
const path = `${type}.${fieldName}`;
|
||||||
const resolverConfig = resolversConfig[path];
|
const resolverConfig = getOr({}, path, resolversConfig);
|
||||||
|
|
||||||
const { resolve: baseResolver = get(fieldName) } = fieldDefinition;
|
const { resolve: baseResolver = defaultResolver } = fieldDefinition;
|
||||||
|
|
||||||
|
// Parse & initialize the middlewares
|
||||||
const middlewares = parseMiddlewares(resolverConfig, strapi);
|
const middlewares = parseMiddlewares(resolverConfig, strapi);
|
||||||
|
|
||||||
// Generate the policy middleware
|
// Generate the policy middleware
|
||||||
@ -62,16 +72,35 @@ const wrapResolvers = ({ schema, strapi, extension = {} }) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Replace the base resolver by a custom function which will handle authorization, middlewares & policies
|
/**
|
||||||
fieldDefinition.resolve = async (parent, args, context, info) => {
|
* GraphQL authorization flow
|
||||||
if (resolverConfig.auth !== false) {
|
* @param {object} context
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
const authorize = async ({ context }) => {
|
||||||
|
const authConfig = get('auth', resolverConfig);
|
||||||
|
const authContext = get('state.auth', context);
|
||||||
|
|
||||||
|
if (authConfig !== false) {
|
||||||
try {
|
try {
|
||||||
await strapi.auth.verify(context.state.auth, resolverConfig.auth);
|
await strapi.auth.verify(authContext, authConfig);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// TODO: [v4] Throw GraphQL Error instead
|
// TODO: [v4] Throw GraphQL Error instead
|
||||||
throw new Error('Forbidden access');
|
throw new Error('Forbidden access');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base resolver wrapper that handles authorization, middlewares & policies
|
||||||
|
* @param {object} parent
|
||||||
|
* @param {object} args
|
||||||
|
* @param {object} context
|
||||||
|
* @param {object} info
|
||||||
|
* @return {Promise<any>}
|
||||||
|
*/
|
||||||
|
fieldDefinition.resolve = async (parent, args, context, info) => {
|
||||||
|
await authorize({ context });
|
||||||
|
|
||||||
// Execute middlewares (including the policy middleware which will always be included)
|
// Execute middlewares (including the policy middleware which will always be included)
|
||||||
return first(boundMiddlewares).call(null, parent, args, context, info);
|
return first(boundMiddlewares).call(null, parent, args, context, info);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user