mirror of
https://github.com/strapi/strapi.git
synced 2025-07-25 18:05:07 +00:00
store the hashed accessKey in the database
This commit is contained in:
parent
5305f2e757
commit
e9b897b66b
3
packages/core/admin/server/bootstrap.js
vendored
3
packages/core/admin/server/bootstrap.js
vendored
@ -44,6 +44,7 @@ module.exports = async () => {
|
|||||||
const permissionService = getService('permission');
|
const permissionService = getService('permission');
|
||||||
const userService = getService('user');
|
const userService = getService('user');
|
||||||
const roleService = getService('role');
|
const roleService = getService('role');
|
||||||
|
const apiTokenService = getService('api-token');
|
||||||
|
|
||||||
await roleService.createRolesIfNoneExist();
|
await roleService.createRolesIfNoneExist();
|
||||||
await roleService.resetSuperAdminPermissions();
|
await roleService.resetSuperAdminPermissions();
|
||||||
@ -55,4 +56,6 @@ module.exports = async () => {
|
|||||||
await userService.displayWarningIfUsersDontHaveRole();
|
await userService.displayWarningIfUsersDontHaveRole();
|
||||||
|
|
||||||
await syncAuthSettings();
|
await syncAuthSettings();
|
||||||
|
|
||||||
|
apiTokenService.createSaltIfNotDefined();
|
||||||
};
|
};
|
||||||
|
7
packages/core/admin/server/config/api-token.js
Normal file
7
packages/core/admin/server/config/api-token.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { env } = require('../../../utils/lib');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
salt: env('API_TOKEN_SALT'),
|
||||||
|
};
|
@ -6,4 +6,5 @@ module.exports = {
|
|||||||
forgotPassword: {
|
forgotPassword: {
|
||||||
emailTemplate: forgotPasswordTemplate,
|
emailTemplate: forgotPasswordTemplate,
|
||||||
},
|
},
|
||||||
|
'api-token': require('./api-token'),
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
|
||||||
* Lifecycle callbacks for the `ApiToken` model.
|
|
||||||
*/
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
collectionName: 'strapi_api_tokens',
|
collectionName: 'strapi_api_tokens',
|
||||||
info: {
|
info: {
|
||||||
|
@ -4,5 +4,5 @@ module.exports = {
|
|||||||
permission: { schema: require('./Permission') },
|
permission: { schema: require('./Permission') },
|
||||||
user: { schema: require('./User') },
|
user: { schema: require('./User') },
|
||||||
role: { schema: require('./Role') },
|
role: { schema: require('./Role') },
|
||||||
['api-token']: { schema: require('./api-token') },
|
'api-token': { schema: require('./api-token') },
|
||||||
};
|
};
|
||||||
|
@ -11,7 +11,7 @@ describe('API Token Controller', () => {
|
|||||||
type: 'read-only',
|
type: 'read-only',
|
||||||
};
|
};
|
||||||
|
|
||||||
test('Fails if API Token already exist', async () => {
|
test('Fails if API Token already exists', async () => {
|
||||||
const exists = jest.fn(() => true);
|
const exists = jest.fn(() => true);
|
||||||
const badRequest = jest.fn();
|
const badRequest = jest.fn();
|
||||||
const ctx = createContext({ body }, { badRequest });
|
const ctx = createContext({ body }, { badRequest });
|
||||||
@ -19,7 +19,7 @@ describe('API Token Controller', () => {
|
|||||||
global.strapi = {
|
global.strapi = {
|
||||||
admin: {
|
admin: {
|
||||||
services: {
|
services: {
|
||||||
['api-token']: {
|
'api-token': {
|
||||||
exists,
|
exists,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -42,7 +42,7 @@ describe('API Token Controller', () => {
|
|||||||
global.strapi = {
|
global.strapi = {
|
||||||
admin: {
|
admin: {
|
||||||
services: {
|
services: {
|
||||||
['api-token']: {
|
'api-token': {
|
||||||
exists,
|
exists,
|
||||||
create,
|
create,
|
||||||
},
|
},
|
||||||
|
@ -14,12 +14,12 @@ module.exports = {
|
|||||||
return ctx.badRequest('ValidationError', err);
|
return ctx.badRequest('ValidationError', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await apiTokenService.exists({ name: attributes.name })) {
|
const alreadyExists = await apiTokenService.exists({ name: attributes.name });
|
||||||
|
if (alreadyExists) {
|
||||||
return ctx.badRequest('Name already taken');
|
return ctx.badRequest('Name already taken');
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiToken = await apiTokenService.create(attributes);
|
const apiToken = await apiTokenService.create(attributes);
|
||||||
|
|
||||||
ctx.created({ data: apiToken });
|
ctx.created({ data: apiToken });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -23,6 +23,9 @@ describe('API Token', () => {
|
|||||||
query() {
|
query() {
|
||||||
return { create };
|
return { create };
|
||||||
},
|
},
|
||||||
|
config: {
|
||||||
|
get: jest.fn(() => ({})),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const attributes = {
|
const attributes = {
|
||||||
@ -34,10 +37,10 @@ describe('API Token', () => {
|
|||||||
const res = await apiTokenService.create(attributes);
|
const res = await apiTokenService.create(attributes);
|
||||||
|
|
||||||
expect(create).toHaveBeenCalledWith({
|
expect(create).toHaveBeenCalledWith({
|
||||||
select: ['id', 'name', 'description', 'type', 'accessKey'],
|
select: ['id', 'name', 'description', 'type'],
|
||||||
data: {
|
data: {
|
||||||
...attributes,
|
...attributes,
|
||||||
accessKey: mockedApiToken.hexedString,
|
accessKey: apiTokenService.hash(mockedApiToken.hexedString),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res).toEqual({
|
expect(res).toEqual({
|
||||||
@ -46,4 +49,46 @@ describe('API Token', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('createSaltIfNotDefined', () => {
|
||||||
|
test('It does nothing if the salt is alread defined', () => {
|
||||||
|
const mockedAppendFile = jest.fn();
|
||||||
|
const mockedConfigSet = jest.fn();
|
||||||
|
|
||||||
|
global.strapi = {
|
||||||
|
config: {
|
||||||
|
get: jest.fn(() => ({
|
||||||
|
server: {
|
||||||
|
admin: { 'api-token': { salt: 'api-token_tests-salt' } },
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
set: mockedConfigSet,
|
||||||
|
},
|
||||||
|
fs: { appendFile: mockedAppendFile },
|
||||||
|
};
|
||||||
|
|
||||||
|
apiTokenService.createSaltIfNotDefined();
|
||||||
|
|
||||||
|
expect(mockedAppendFile).not.toHaveBeenCalled();
|
||||||
|
expect(mockedConfigSet).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('It creates a new salt, appendit to the .env file and sets it in the configuration', () => {
|
||||||
|
const mockedAppendFile = jest.fn();
|
||||||
|
const mockedConfigSet = jest.fn();
|
||||||
|
|
||||||
|
global.strapi = {
|
||||||
|
config: {
|
||||||
|
get: jest.fn(() => null),
|
||||||
|
set: mockedConfigSet,
|
||||||
|
},
|
||||||
|
fs: { appendFile: mockedAppendFile },
|
||||||
|
};
|
||||||
|
|
||||||
|
apiTokenService.createSaltIfNotDefined();
|
||||||
|
|
||||||
|
expect(mockedAppendFile).toHaveBeenCalled();
|
||||||
|
expect(mockedConfigSet).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -13,14 +13,28 @@ const crypto = require('crypto');
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} attributes
|
* @param {Object} whereParams
|
||||||
* @param {string} attributes.name
|
* @param {string} whereParams.name
|
||||||
* @param {string} [attributes.description]
|
* @param {string} [whereParams.description]
|
||||||
*
|
*
|
||||||
* @returns {Promise<boolean>}
|
* @returns {Promise<boolean>}
|
||||||
*/
|
*/
|
||||||
const exists = async (attributes = {}) => {
|
const exists = async (whereParams = {}) => {
|
||||||
return (await strapi.query('admin::api-token').count({ where: attributes })) > 0;
|
const apiToken = await strapi.query('admin::api-token').findOne({ where: whereParams });
|
||||||
|
|
||||||
|
return !!apiToken;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} accessKey
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const hash = accessKey => {
|
||||||
|
return crypto
|
||||||
|
.createHash('sha512')
|
||||||
|
.update(`${strapi.config.get('server.admin.api-token.salt')}${accessKey}`)
|
||||||
|
.digest('hex');
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -34,16 +48,39 @@ const exists = async (attributes = {}) => {
|
|||||||
const create = async attributes => {
|
const create = async attributes => {
|
||||||
const accessKey = crypto.randomBytes(128).toString('hex');
|
const accessKey = crypto.randomBytes(128).toString('hex');
|
||||||
|
|
||||||
return strapi.query('admin::api-token').create({
|
const apiToken = await strapi.query('admin::api-token').create({
|
||||||
select: ['id', 'name', 'description', 'type', 'accessKey'],
|
select: ['id', 'name', 'description', 'type'],
|
||||||
data: {
|
data: {
|
||||||
...attributes,
|
...attributes,
|
||||||
accessKey,
|
accessKey: hash(accessKey),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...apiToken,
|
||||||
|
accessKey,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const createSaltIfNotDefined = () => {
|
||||||
|
if (strapi.config.get('server.admin.api-token.salt')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const salt = crypto.randomBytes(16).toString('hex');
|
||||||
|
|
||||||
|
if (!process.env.API_TOKEN_SALT) {
|
||||||
|
strapi.fs.appendFile('.env', `API_TOKEN_SALT=${salt}\n`);
|
||||||
|
strapi.config.set('server.admin.api-token.salt', salt);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
create,
|
create,
|
||||||
exists,
|
exists,
|
||||||
|
createSaltIfNotDefined,
|
||||||
|
hash,
|
||||||
};
|
};
|
||||||
|
@ -12,5 +12,5 @@ module.exports = {
|
|||||||
condition: require('./condition'),
|
condition: require('./condition'),
|
||||||
auth: require('./auth'),
|
auth: require('./auth'),
|
||||||
action: require('./action'),
|
action: require('./action'),
|
||||||
['api-token']: require('./api-token'),
|
'api-token': require('./api-token'),
|
||||||
};
|
};
|
||||||
|
@ -8,8 +8,10 @@ const { createAuthRequest } = require('../../../../../test/helpers/request');
|
|||||||
*
|
*
|
||||||
* N° Description
|
* N° Description
|
||||||
* -------------------------------------------
|
* -------------------------------------------
|
||||||
* 1. Creates an api token (wrong body)
|
* 1. Fails to creates an api token (missing parameters from the body)
|
||||||
* 2. Creates an api token (successfully)
|
* 2. Fails to creates an api token (invalid `type` in the body)
|
||||||
|
* 3. Creates an api token (successfully)
|
||||||
|
* 4. Creates an api token without a description (successfully)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
describe('Admin API Token CRUD (e2e)', () => {
|
describe('Admin API Token CRUD (e2e)', () => {
|
||||||
@ -27,7 +29,7 @@ describe('Admin API Token CRUD (e2e)', () => {
|
|||||||
await strapi.destroy();
|
await strapi.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('1. Creates an api token (wrong body)', async () => {
|
test('1. Fails to creates an api token (missing parameters from the body)', async () => {
|
||||||
const body = {
|
const body = {
|
||||||
name: 'api-token_tests-name',
|
name: 'api-token_tests-name',
|
||||||
description: 'api-token_tests-description',
|
description: 'api-token_tests-description',
|
||||||
@ -50,7 +52,31 @@ describe('Admin API Token CRUD (e2e)', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('2. Creates an api token (successfully)', async () => {
|
test('2. Fails to creates an api token (invalid `type` in the body)', async () => {
|
||||||
|
const body = {
|
||||||
|
name: 'api-token_tests-name',
|
||||||
|
description: 'api-token_tests-description',
|
||||||
|
type: 'invalid-type',
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await rq({
|
||||||
|
url: '/admin/api-tokens',
|
||||||
|
method: 'POST',
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
expect(res.body).toMatchObject({
|
||||||
|
statusCode: 400,
|
||||||
|
error: 'Bad Request',
|
||||||
|
message: 'ValidationError',
|
||||||
|
data: {
|
||||||
|
type: ['type must be one of the following values: read-only, full-access'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('3. Creates an api token (successfully)', async () => {
|
||||||
const body = {
|
const body = {
|
||||||
name: 'api-token_tests-name',
|
name: 'api-token_tests-name',
|
||||||
description: 'api-token_tests-description',
|
description: 'api-token_tests-description',
|
||||||
@ -64,15 +90,16 @@ describe('Admin API Token CRUD (e2e)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(res.statusCode).toBe(201);
|
expect(res.statusCode).toBe(201);
|
||||||
expect(res.body.data).not.toBeNull();
|
expect(res.body.data).toMatchObject({
|
||||||
expect(res.body.data.id).toBe(1);
|
accessKey: expect.any(String),
|
||||||
expect(res.body.data.accessKey).toBeDefined();
|
name: body.name,
|
||||||
expect(res.body.data.name).toBe(body.name);
|
description: body.description,
|
||||||
expect(res.body.data.description).toBe(body.description);
|
type: body.type,
|
||||||
expect(res.body.data.type).toBe(body.type);
|
id: expect.any(Number),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('3. Creates an api token without a description (successfully)', async () => {
|
test('4. Creates an api token without a description (successfully)', async () => {
|
||||||
const body = {
|
const body = {
|
||||||
name: 'api-token_tests-name-without-description',
|
name: 'api-token_tests-name-without-description',
|
||||||
type: 'read-only',
|
type: 'read-only',
|
||||||
@ -85,11 +112,12 @@ describe('Admin API Token CRUD (e2e)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(res.statusCode).toBe(201);
|
expect(res.statusCode).toBe(201);
|
||||||
expect(res.body.data).not.toBeNull();
|
expect(res.body.data).toMatchObject({
|
||||||
expect(res.body.data.id).toBe(2);
|
accessKey: expect.any(String),
|
||||||
expect(res.body.data.accessKey).toBeDefined();
|
name: body.name,
|
||||||
expect(res.body.data.name).toBe(body.name);
|
description: '',
|
||||||
expect(res.body.data.description).toBe('');
|
type: body.type,
|
||||||
expect(res.body.data.type).toBe(body.type);
|
id: expect.any(Number),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
4
packages/core/admin/server/utils/index.d.ts
vendored
4
packages/core/admin/server/utils/index.d.ts
vendored
@ -11,11 +11,11 @@ type S = {
|
|||||||
role: typeof role;
|
role: typeof role;
|
||||||
user: typeof user;
|
user: typeof user;
|
||||||
permission: typeof permission;
|
permission: typeof permission;
|
||||||
['content-type']: typeof contentType;
|
'content-type': typeof contentType;
|
||||||
token: typeof token;
|
token: typeof token;
|
||||||
auth: typeof auth;
|
auth: typeof auth;
|
||||||
metrics: typeof metrics;
|
metrics: typeof metrics;
|
||||||
['api-token']: typeof apiToken;
|
'api-token': typeof apiToken;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getService<T extends keyof S>(name: T): ReturnType<S[T]>;
|
export function getService<T extends keyof S>(name: T): ReturnType<S[T]>;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user