Skip to content
This repository was archived by the owner on Sep 16, 2025. It is now read-only.

Commit 3fb1b0b

Browse files
fix(findSlug utility): respect foreign model permissions (#33)
* fix(slugController): respect foreign model access permissions (#32) * feat(utils): add `sanitizeOutput` * feat(utils): add `isValidFindSlugParams` * feat(utils): add `hasRequiredModelsScope` * refactor(slugController): utilise util functions * fix(Query.findSlug): respect model access permissions refactor(Query.findSlug): utilise utils functions Co-authored-by: Jean-Sébastien Herbaux <[email protected]>
1 parent 1bf0670 commit 3fb1b0b

File tree

5 files changed

+102
-55
lines changed

5 files changed

+102
-55
lines changed
Lines changed: 36 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,48 @@
11
'use strict';
22

33
const _ = require('lodash');
4-
const { sanitize } = require('@strapi/utils');
4+
const { NotFoundError } = require('@strapi/utils/lib/errors');
55
const { getPluginService } = require('../utils/getPluginService');
66
const { transformResponse } = require('@strapi/strapi/lib/core-api/controller/transform');
7+
const { isValidFindSlugParams } = require('../utils/isValidFindSlugParams');
8+
const { sanitizeOutput } = require('../utils/sanitizeOutput');
9+
const { hasRequiredModelScopes } = require('../utils/hasRequiredModelScopes');
710

811
module.exports = ({ strapi }) => ({
912
async findSlug(ctx) {
1013
const { models } = getPluginService(strapi, 'settingsService').get();
11-
const { params } = ctx.request;
12-
const { modelName, slug } = params;
13-
14-
try {
15-
if (!modelName) {
16-
throw Error('A model name path variable is required.');
17-
}
18-
19-
if (!slug) {
20-
throw Error('A slug path variable is required.');
21-
}
22-
23-
const model = models[modelName];
24-
if (!model) {
25-
throw Error(
26-
`${modelName} model name not found, all models must be defined in the settings and are case sensitive.`
27-
);
28-
}
29-
30-
const { uid, field, contentType } = model;
31-
32-
// add slug filter to any already existing query restrictions
33-
let query = ctx.query || {};
34-
if (!query.filters) {
35-
query.filters = {};
36-
}
37-
query.filters[field] = slug;
38-
39-
// only return published entries by default if content type has draftAndPublish enabled
40-
if (_.get(contentType, ['options', 'draftAndPublish'], false) && !query.publicationState) {
41-
query.publicationState = 'live';
42-
}
43-
44-
const data = await getPluginService(strapi, 'slugService').findOne(uid, query);
45-
46-
if (data) {
47-
const sanitizedEntity = await sanitize.contentAPI.output(data, contentType);
48-
ctx.body = transformResponse(sanitizedEntity);
49-
} else {
50-
ctx.notFound();
51-
}
52-
} catch (error) {
53-
ctx.badRequest(error.message);
14+
const { modelName, slug } = ctx.request.params;
15+
const { auth } = ctx.state;
16+
17+
isValidFindSlugParams({
18+
modelName,
19+
slug,
20+
models,
21+
});
22+
23+
const { uid, field, contentType } = models[modelName];
24+
25+
await hasRequiredModelScopes(strapi, uid, auth);
26+
27+
// add slug filter to any already existing query restrictions
28+
let query = ctx.query || {};
29+
if (!query.filters) {
30+
query.filters = {};
31+
}
32+
query.filters[field] = slug;
33+
34+
// only return published entries by default if content type has draftAndPublish enabled
35+
if (_.get(contentType, ['options', 'draftAndPublish'], false) && !query.publicationState) {
36+
query.publicationState = 'live';
37+
}
38+
39+
const data = await getPluginService(strapi, 'slugService').findOne(uid, query);
40+
41+
if (data) {
42+
const sanitizedEntity = await sanitizeOutput(data, contentType, auth);
43+
ctx.body = transformResponse(sanitizedEntity);
44+
} else {
45+
throw new NotFoundError();
5446
}
5547
},
5648
});

server/graphql/types.js

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
const _ = require('lodash');
22
const { getPluginService } = require('../utils/getPluginService');
3-
const { ValidationError } = require('@strapi/utils').errors;
3+
const { isValidFindSlugParams } = require('../utils/isValidFindSlugParams');
4+
const { hasRequiredModelScopes } = require('../utils/hasRequiredModelScopes');
5+
const { sanitizeOutput } = require('../utils/sanitizeOutput');
46

57
const getCustomTypes = (strapi, nexus) => {
68
const { naming } = getPluginService(strapi, 'utils', 'graphql');
@@ -46,20 +48,22 @@ const getCustomTypes = (strapi, nexus) => {
4648
modelName: nexus.stringArg('The model name of the content type'),
4749
slug: nexus.stringArg('The slug to query for'),
4850
},
49-
resolve: async (_parent, args) => {
51+
resolve: async (_parent, args, ctx) => {
5052
const { models } = getPluginService(strapi, 'settingsService').get();
5153
const { modelName, slug } = args;
54+
const { auth } = ctx.state;
5255

53-
const model = models[modelName];
56+
isValidFindSlugParams({
57+
modelName,
58+
slug,
59+
models,
60+
});
5461

55-
// ensure valid model is passed
56-
if (!model) {
57-
throw new ValidationError(
58-
`${modelName} model name not found, all models must be defined in the settings and are case sensitive.`
59-
);
60-
}
62+
const { uid, field, contentType } = models[modelName];
63+
64+
await hasRequiredModelScopes(strapi, uid, auth);
6165

62-
const { uid, field, contentType } = model;
66+
// build query
6367
let query = {
6468
filters: {
6569
[field]: slug,
@@ -72,7 +76,8 @@ const getCustomTypes = (strapi, nexus) => {
7276
}
7377

7478
const data = await getPluginService(strapi, 'slugService').findOne(uid, query);
75-
return toEntityResponse(data, { resourceUID: uid });
79+
const sanitizedEntity = await sanitizeOutput(data, contentType, auth);
80+
return toEntityResponse(sanitizedEntity, { resourceUID: uid });
7681
},
7782
});
7883
},
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const { ForbiddenError } = require('@strapi/utils/lib/errors');
2+
3+
const hasRequiredModelScopes = async (strapi, uid, auth) => {
4+
try {
5+
await strapi.auth.verify(auth, { scope: `${uid}.find` });
6+
} catch (e) {
7+
throw new ForbiddenError();
8+
}
9+
};
10+
11+
module.exports = {
12+
hasRequiredModelScopes,
13+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
const { ValidationError } = require('@strapi/utils/lib/errors');
2+
3+
const isValidFindSlugParams = (params) => {
4+
if (!params) {
5+
throw new ValidationError('A model and slug must be provided.');
6+
}
7+
8+
const { modelName, slug, models } = params;
9+
10+
if (!modelName) {
11+
throw new ValidationError('A model name path variable is required.');
12+
}
13+
14+
if (!slug) {
15+
throw new ValidationError('A slug path variable is required.');
16+
}
17+
18+
const model = models[modelName];
19+
20+
// ensure valid model is passed
21+
if (!model) {
22+
throw new ValidationError(
23+
`${modelName} model name not found, all models must be defined in the settings and are case sensitive.`
24+
);
25+
}
26+
};
27+
28+
module.exports = {
29+
isValidFindSlugParams,
30+
};

server/utils/sanitizeOutput.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const { contentAPI } = require('@strapi/utils/lib/sanitize');
2+
3+
const sanitizeOutput = (data, contentType, auth) => contentAPI.output(data, contentType, { auth });
4+
5+
module.exports = {
6+
sanitizeOutput,
7+
};

0 commit comments

Comments
 (0)