mirror of
https://github.com/strapi/strapi.git
synced 2025-12-12 15:32:42 +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();
|
||||
}
|
||||
|
||||
get auth() {
|
||||
return this.container.get('auth');
|
||||
}
|
||||
|
||||
async start() {
|
||||
try {
|
||||
if (!this.isLoaded) {
|
||||
@ -400,7 +404,7 @@ class Strapi {
|
||||
/**
|
||||
* Binds queries with a specific model
|
||||
* @param {string} uid
|
||||
* @returns {}
|
||||
* @returns {*}
|
||||
*/
|
||||
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 { ApolloServer } = require('apollo-server-koa');
|
||||
const {
|
||||
ApolloServerPluginLandingPageLocalDefault,
|
||||
ApolloServerPluginLandingPageProductionDefault,
|
||||
ApolloServerPluginLandingPageDisabled,
|
||||
ApolloServerPluginLandingPageGraphQLPlayground,
|
||||
} = require('apollo-server-core');
|
||||
const depthLimit = require('graphql-depth-limit');
|
||||
const { graphqlUploadKoa } = require('graphql-upload');
|
||||
@ -35,15 +35,10 @@ module.exports = async strapi => {
|
||||
schema,
|
||||
|
||||
// Initialize loaders for this request.
|
||||
context: ({ ctx }) => {
|
||||
// TODO: set loaders in the context not globally
|
||||
strapi
|
||||
.plugin('graphql')
|
||||
.service('old')
|
||||
['data-loaders'].initializeLoader();
|
||||
|
||||
return ctx;
|
||||
},
|
||||
context: ({ ctx }) => ({
|
||||
state: ctx.state,
|
||||
koaContext: ctx,
|
||||
}),
|
||||
|
||||
// Validation
|
||||
validationRules: [depthLimit(config('depthLimit'))],
|
||||
@ -54,10 +49,9 @@ module.exports = async strapi => {
|
||||
bodyParserConfig: true,
|
||||
|
||||
plugins: [
|
||||
// Specify which GraphQL landing page we want for the different env.
|
||||
process.env.NODE_ENV !== 'production'
|
||||
? ApolloServerPluginLandingPageLocalDefault({ footer: false })
|
||||
: ApolloServerPluginLandingPageProductionDefault({ footer: false }),
|
||||
process.env.NODE_ENV === 'production'
|
||||
? ApolloServerPluginLandingPageDisabled()
|
||||
: ApolloServerPluginLandingPageGraphQLPlayground(),
|
||||
],
|
||||
};
|
||||
|
||||
@ -77,10 +71,30 @@ module.exports = async strapi => {
|
||||
}
|
||||
|
||||
// Link the Apollo server & the Strapi app
|
||||
server.applyMiddleware({
|
||||
app: strapi.server.app,
|
||||
path: config('endpoint', '/graphql'),
|
||||
});
|
||||
strapi.server.routes([
|
||||
{
|
||||
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
|
||||
// We're doing it here instead of exposing a destroy method to the strapi-server.js
|
||||
|
||||
@ -133,6 +133,30 @@ module.exports = ({ strapi }) => {
|
||||
|
||||
return {
|
||||
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({
|
||||
type: 'Mutation',
|
||||
|
||||
|
||||
@ -92,6 +92,24 @@ module.exports = ({ strapi }) => {
|
||||
|
||||
return {
|
||||
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({
|
||||
type: 'Mutation',
|
||||
|
||||
|
||||
@ -16,6 +16,24 @@ module.exports = ({ strapi }) => {
|
||||
} = naming;
|
||||
|
||||
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({
|
||||
type: 'Query',
|
||||
|
||||
|
||||
@ -11,6 +11,18 @@ module.exports = ({ strapi }) => {
|
||||
const { getFindOneQueryName, getEntityResponseName } = naming;
|
||||
|
||||
const buildSingleTypeQueries = contentType => {
|
||||
getService('extension')
|
||||
.for('content-api')
|
||||
.use(() => ({
|
||||
resolversConfig: {
|
||||
[`Query.${getFindOneQueryName(contentType)}`]: {
|
||||
auth: {
|
||||
scope: [`${contentType.uid}.findOne`],
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
return extendType({
|
||||
type: 'Query',
|
||||
|
||||
|
||||
@ -1,9 +1,19 @@
|
||||
'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 ignoredObjectTypes = [
|
||||
'__Schema',
|
||||
'__Type',
|
||||
'__Field',
|
||||
'__InputValue',
|
||||
'__EnumValue',
|
||||
'__Directive',
|
||||
];
|
||||
|
||||
/**
|
||||
* Wrap the schema's resolvers if they've been
|
||||
* customized using the GraphQL extension service
|
||||
@ -19,31 +29,31 @@ const wrapResolvers = ({ schema, strapi, extension = {} }) => {
|
||||
|
||||
// Fields filters
|
||||
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
|
||||
// schema's type map and wrap its resolve attribute if needed
|
||||
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;
|
||||
}
|
||||
|
||||
const fieldsToProcess = Object.entries(fields)
|
||||
// Ignore patterns such as "__FooBar"
|
||||
.filter(isValidFieldName)
|
||||
// Don't augment the types if there isn't any configuration defined for them
|
||||
.filter(hasResolverConfig(type));
|
||||
const fields = definition.getFields();
|
||||
const fieldsToProcess = Object.entries(fields).filter(isValidFieldName);
|
||||
|
||||
for (const [fieldName, fieldDefinition] of fieldsToProcess) {
|
||||
const defaultResolver = get(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);
|
||||
|
||||
// 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) => {
|
||||
if (resolverConfig.auth !== false) {
|
||||
/**
|
||||
* GraphQL authorization flow
|
||||
* @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 {
|
||||
await strapi.auth.verify(context.state.auth, resolverConfig.auth);
|
||||
await strapi.auth.verify(authContext, authConfig);
|
||||
} catch (error) {
|
||||
// TODO: [v4] Throw GraphQL Error instead
|
||||
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)
|
||||
return first(boundMiddlewares).call(null, parent, args, context, info);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user