mirror of
https://github.com/strapi/strapi.git
synced 2025-12-13 07:55:33 +00:00
Validate permissions for api-tokens & throw on unknown permissions
This commit is contained in:
parent
d95151a556
commit
e7ca50f6b9
@ -18,6 +18,17 @@ describe('API Token Controller', () => {
|
|||||||
const ctx = createContext({ body });
|
const ctx = createContext({ body });
|
||||||
|
|
||||||
global.strapi = {
|
global.strapi = {
|
||||||
|
contentAPI: {
|
||||||
|
permissions: {
|
||||||
|
providers: {
|
||||||
|
action: {
|
||||||
|
keys() {
|
||||||
|
return ['foo', 'bar'];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
admin: {
|
admin: {
|
||||||
services: {
|
services: {
|
||||||
'api-token': {
|
'api-token': {
|
||||||
|
|||||||
@ -1,10 +1,16 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { NotFoundError } = require('@strapi/utils/lib/errors');
|
const { NotFoundError, ApplicationError } = require('@strapi/utils/lib/errors');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const { omit, uniq } = require('lodash/fp');
|
const { omit, uniq } = require('lodash/fp');
|
||||||
const apiTokenService = require('../api-token');
|
const apiTokenService = require('../api-token');
|
||||||
|
|
||||||
|
const getActionProvider = (actions = []) => {
|
||||||
|
return {
|
||||||
|
contentAPI: { permissions: { providers: { action: { keys: jest.fn(() => actions) } } } },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
describe('API Token', () => {
|
describe('API Token', () => {
|
||||||
const mockedApiToken = {
|
const mockedApiToken = {
|
||||||
randomBytes: 'api-token_test-random-bytes',
|
randomBytes: 'api-token_test-random-bytes',
|
||||||
@ -133,6 +139,7 @@ describe('API Token', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
global.strapi = {
|
global.strapi = {
|
||||||
|
...getActionProvider(['admin::content.content.read']),
|
||||||
query() {
|
query() {
|
||||||
return {
|
return {
|
||||||
findOne,
|
findOne,
|
||||||
@ -215,6 +222,7 @@ describe('API Token', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
global.strapi = {
|
global.strapi = {
|
||||||
|
...getActionProvider(['api::foo.foo.find', 'api::foo.foo.create']),
|
||||||
query() {
|
query() {
|
||||||
return {
|
return {
|
||||||
findOne,
|
findOne,
|
||||||
@ -234,6 +242,56 @@ describe('API Token', () => {
|
|||||||
expect(res.permissions).toHaveLength(2);
|
expect(res.permissions).toHaveLength(2);
|
||||||
expect(res.permissions).toEqual(['api::foo.foo.find', 'api::foo.foo.create']);
|
expect(res.permissions).toEqual(['api::foo.foo.find', 'api::foo.foo.create']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Creates a custom token with invalid permissions should throw', async () => {
|
||||||
|
const attributes = {
|
||||||
|
name: 'api-token_tests-name',
|
||||||
|
description: 'api-token_tests-description',
|
||||||
|
type: 'custom',
|
||||||
|
permissions: ['valid-permission', 'unknown-permission-A', 'unknown-permission-B'],
|
||||||
|
};
|
||||||
|
const createTokenResult = {
|
||||||
|
...attributes,
|
||||||
|
lifespan: null,
|
||||||
|
expiresAt: null,
|
||||||
|
id: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const create = jest.fn().mockResolvedValue(createTokenResult);
|
||||||
|
const load = jest.fn().mockResolvedValueOnce(
|
||||||
|
Promise.resolve(
|
||||||
|
uniq(attributes.permissions).map((p) => {
|
||||||
|
return {
|
||||||
|
action: p,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
global.strapi = {
|
||||||
|
...getActionProvider(['valid-permission']),
|
||||||
|
query() {
|
||||||
|
return {
|
||||||
|
create,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
get: jest.fn(() => ''),
|
||||||
|
},
|
||||||
|
entityService: {
|
||||||
|
load,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() => apiTokenService.create(attributes)).rejects.toThrowError(
|
||||||
|
new ApplicationError(
|
||||||
|
`Unknown permissions provided: unknown-permission-A, unknown-permission-B`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(load).not.toHaveBeenCalled();
|
||||||
|
expect(create).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('checkSaltIsDefined', () => {
|
describe('checkSaltIsDefined', () => {
|
||||||
@ -580,6 +638,12 @@ describe('API Token', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
global.strapi = {
|
global.strapi = {
|
||||||
|
...getActionProvider([
|
||||||
|
'admin::subject.keepThisAction',
|
||||||
|
'admin::subject.newAction',
|
||||||
|
'admin::subject.newAction',
|
||||||
|
'admin::subject.otherAction',
|
||||||
|
]),
|
||||||
query() {
|
query() {
|
||||||
return {
|
return {
|
||||||
update,
|
update,
|
||||||
@ -717,6 +781,45 @@ describe('API Token', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Updates permissions field of a custom token with unknown permissions', async () => {
|
||||||
|
const id = 1;
|
||||||
|
|
||||||
|
const updatedAttributes = {
|
||||||
|
permissions: ['valid-permission-A', 'unknown-permission'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const update = jest.fn(({ data }) => Promise.resolve(data));
|
||||||
|
const deleteFn = jest.fn();
|
||||||
|
const create = jest.fn();
|
||||||
|
const load = jest.fn();
|
||||||
|
|
||||||
|
global.strapi = {
|
||||||
|
...getActionProvider(['valid-permission-A']),
|
||||||
|
query() {
|
||||||
|
return {
|
||||||
|
update,
|
||||||
|
delete: deleteFn,
|
||||||
|
create,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
get: jest.fn(() => ''),
|
||||||
|
},
|
||||||
|
entityService: {
|
||||||
|
load,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() => apiTokenService.update(id, updatedAttributes)).rejects.toThrowError(
|
||||||
|
new ApplicationError(`Unknown permissions provided: unknown-permission`)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(update).not.toHaveBeenCalled();
|
||||||
|
expect(deleteFn).not.toHaveBeenCalled();
|
||||||
|
expect(create).not.toHaveBeenCalled();
|
||||||
|
expect(load).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
describe('getByName', () => {
|
describe('getByName', () => {
|
||||||
const token = {
|
const token = {
|
||||||
id: 1,
|
id: 1,
|
||||||
|
|||||||
@ -65,6 +65,16 @@ const assertCustomTokenPermissionsValidity = (attributes) => {
|
|||||||
if (attributes.type === constants.API_TOKEN_TYPE.CUSTOM && isEmpty(attributes.permissions)) {
|
if (attributes.type === constants.API_TOKEN_TYPE.CUSTOM && isEmpty(attributes.permissions)) {
|
||||||
throw new ValidationError('Missing permissions attribute for custom token');
|
throw new ValidationError('Missing permissions attribute for custom token');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Permissions provided for a custom type token should be valid/registered permissions UID
|
||||||
|
if (attributes.type === constants.API_TOKEN_TYPE.CUSTOM) {
|
||||||
|
const validPermissions = strapi.contentAPI.permissions.providers.action.keys();
|
||||||
|
const invalidPermissions = difference(attributes.permissions, validPermissions);
|
||||||
|
|
||||||
|
if (!isEmpty(invalidPermissions)) {
|
||||||
|
throw new ValidationError(`Unknown permissions provided: ${invalidPermissions.join(', ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -304,11 +304,6 @@ describe('Admin API Token v2 CRUD (e2e)', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: Discuss: Which behaviour do we want? Should an empty array be treated the same as omitted/undefined?
|
|
||||||
* Easy to change in assertCustomTokenPermissionsValidity by checking isEmpty (to allow empty) vs !attributes.permissions
|
|
||||||
*/
|
|
||||||
|
|
||||||
test('Creates a non-custom api token with empty permissions attribute', async () => {
|
test('Creates a non-custom api token with empty permissions attribute', async () => {
|
||||||
const body = {
|
const body = {
|
||||||
name: 'api-token_tests-fullAccessFailWithEmptyPermissions',
|
name: 'api-token_tests-fullAccessFailWithEmptyPermissions',
|
||||||
@ -340,6 +335,11 @@ describe('Admin API Token v2 CRUD (e2e)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Creates a custom api token', async () => {
|
test('Creates a custom api token', async () => {
|
||||||
|
strapi.contentAPI.permissions.providers.action.keys = jest.fn(() => [
|
||||||
|
'admin::subject.action',
|
||||||
|
'plugin::foo.bar.action',
|
||||||
|
]);
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
name: 'api-token_tests-customSuccess',
|
name: 'api-token_tests-customSuccess',
|
||||||
description: 'api-token_tests-description',
|
description: 'api-token_tests-description',
|
||||||
@ -395,6 +395,34 @@ describe('Admin API Token v2 CRUD (e2e)', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Fails to create a custom api token with unknown permissions', async () => {
|
||||||
|
strapi.contentAPI.permissions.providers.action.keys = jest.fn(() => ['action-A', 'action-B']);
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
name: 'api-token_tests-customFail',
|
||||||
|
description: 'api-token_tests-description',
|
||||||
|
type: 'custom',
|
||||||
|
permissions: ['action-A', 'action-C'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await rq({
|
||||||
|
url: '/admin/api-tokens',
|
||||||
|
method: 'POST',
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
expect(res.body).toMatchObject({
|
||||||
|
data: null,
|
||||||
|
error: {
|
||||||
|
status: 400,
|
||||||
|
name: 'ValidationError',
|
||||||
|
message: 'Unknown permissions provided: action-C',
|
||||||
|
details: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('Creates an api token without a description (successfully)', async () => {
|
test('Creates an api token without a description (successfully)', async () => {
|
||||||
const body = {
|
const body = {
|
||||||
name: 'api-token_tests-without-description',
|
name: 'api-token_tests-without-description',
|
||||||
@ -455,6 +483,11 @@ describe('Admin API Token v2 CRUD (e2e)', () => {
|
|||||||
test('List all tokens (successfully)', async () => {
|
test('List all tokens (successfully)', async () => {
|
||||||
await deleteAllTokens();
|
await deleteAllTokens();
|
||||||
|
|
||||||
|
strapi.contentAPI.permissions.providers.action.keys = jest.fn(() => [
|
||||||
|
'admin::model.model.read',
|
||||||
|
'admin::model.model.create',
|
||||||
|
]);
|
||||||
|
|
||||||
// create 4 tokens
|
// create 4 tokens
|
||||||
const tokens = [];
|
const tokens = [];
|
||||||
tokens.push(
|
tokens.push(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user