263 lines
7.6 KiB
JavaScript
Raw Normal View History

'use strict';
const crypto = require('crypto');
2022-08-09 10:49:44 +02:00
const { map, omit, differenceBy, isEmpty } = require('lodash/fp');
const { ValidationError, NotFoundError } = require('@strapi/utils').errors;
2022-08-08 17:06:38 +02:00
const constants = require('../services/constants');
2021-08-30 14:00:53 +02:00
/**
* @typedef {'read-only'|'full-access'|'custom'} TokenType
2021-08-30 14:00:53 +02:00
*/
2021-08-27 09:44:29 +02:00
/**
* @typedef ApiToken
*
2021-08-30 14:00:53 +02:00
* @property {number|string} id
2021-08-27 09:44:29 +02:00
* @property {string} name
* @property {string} [description]
* @property {string} accessKey
2021-08-30 14:00:53 +02:00
* @property {TokenType} type
* @property {(number|ApiTokenPermission)[]} [permissions]
*/
/**
* @typedef ApiTokenPermission
*
* @property {number|string} id
* @property {string} action
* @property {ApiToken|number} [token]
2021-08-27 09:44:29 +02:00
*/
2021-09-02 10:47:06 +02:00
/** @constant {Array<string>} */
const SELECT_FIELDS = ['id', 'name', 'description', 'type', 'createdAt'];
2021-09-02 10:47:06 +02:00
/** @constant {Array<string>} */
const POPULATE_FIELDS = ['permissions'];
const assertCustomTokenPermissionsValidity = attributes => {
// Ensure non-custom tokens doesn't have permissions
if (attributes.type !== constants.API_TOKEN_TYPE.CUSTOM && !isEmpty(attributes.permissions)) {
2022-08-09 10:28:42 +02:00
throw new ValidationError('Non-custom tokens should not references permissions');
}
// Custom type tokens should always have permissions attached to them
2022-08-09 10:49:44 +02:00
if (attributes.type === constants.API_TOKEN_TYPE.CUSTOM && isEmpty(attributes.permissions)) {
2022-08-09 10:28:42 +02:00
throw new ValidationError('Missing permissions attributes for custom token');
}
};
/**
* @param {Object} whereParams
* @param {string|number} [whereParams.id]
* @param {string} [whereParams.name]
* @param {string} [whereParams.description]
* @param {string} [whereParams.accessKey]
*
* @returns {Promise<boolean>}
*/
const exists = async (whereParams = {}) => {
const apiToken = await getBy(whereParams);
return !!apiToken;
};
/**
* @param {string} accessKey
*
* @returns {string}
*/
const hash = accessKey => {
return crypto
.createHmac('sha512', strapi.config.get('admin.apiToken.salt'))
.update(accessKey)
.digest('hex');
};
/**
* @param {Object} attributes
2021-08-30 14:00:53 +02:00
* @param {TokenType} attributes.type
* @param {string} attributes.name
* @param {string[]} [attributes.permissions]
* @param {string} [attributes.description]
*
2021-08-27 09:44:29 +02:00
* @returns {Promise<ApiToken>}
*/
const create = async attributes => {
const accessKey = crypto.randomBytes(128).toString('hex');
assertCustomTokenPermissionsValidity(attributes);
// Create the token
const apiToken = await strapi.query('admin::api-token').create({
2021-09-02 10:47:06 +02:00
select: SELECT_FIELDS,
2022-08-05 12:31:16 +02:00
populate: POPULATE_FIELDS,
data: {
...omit('permissions', attributes),
accessKey: hash(accessKey),
},
});
const result = { ...apiToken, accessKey };
// If this is a custom type token, create and link the associated permissions
2022-08-08 17:06:38 +02:00
if (attributes.type === constants.API_TOKEN_TYPE.CUSTOM) {
const permissionsCount = await strapi
2022-08-05 12:31:16 +02:00
.query('admin::token-permission')
.createMany({ data: attributes.permissions.map(action => ({ action, token: apiToken.id })) });
// TODO: should we select the permissions again to ensure it worked?
if (permissionsCount) {
Object.assign(result, { permissions: attributes.permissions });
}
}
return result;
};
/**
* @returns {void}
*/
const checkSaltIsDefined = () => {
if (!strapi.config.get('admin.apiToken.salt')) {
// TODO V5: stop reading API_TOKEN_SALT
if (process.env.API_TOKEN_SALT) {
2022-03-04 15:48:49 +01:00
process.emitWarning(`[deprecated] In future versions, Strapi will stop reading directly from the environment variable API_TOKEN_SALT. Please set apiToken.salt in config/admin.js instead.
For security reasons, keep storing the secret in an environment variable and use env() to read it in config/admin.js (ex: \`apiToken: { salt: env('API_TOKEN_SALT') }\`). See https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/configurations/optional/environment.html#configuration-using-environment-variables.`);
strapi.config.set('admin.apiToken.salt', process.env.API_TOKEN_SALT);
} else {
throw new Error(
2022-03-18 17:55:22 +01:00
`Missing apiToken.salt. Please set apiToken.salt in config/admin.js (ex: you can generate one using Node with \`crypto.randomBytes(16).toString('base64')\`).
For security reasons, prefer storing the secret in an environment variable and read it in config/admin.js. See https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/configurations/optional/environment.html#configuration-using-environment-variables.`
);
}
}
};
/**
2021-09-02 10:47:06 +02:00
* @returns {Promise<Omit<ApiToken, 'accessKey'>>}
*/
const list = async () => {
2021-08-30 14:00:53 +02:00
return strapi.query('admin::api-token').findMany({
2021-09-02 10:47:06 +02:00
select: SELECT_FIELDS,
populate: POPULATE_FIELDS,
2021-08-27 08:39:08 +02:00
orderBy: { name: 'ASC' },
});
};
2021-08-31 15:31:54 +02:00
/**
* @param {string|number} id
*
2021-09-02 10:47:06 +02:00
* @returns {Promise<Omit<ApiToken, 'accessKey'>>}
2021-08-31 15:31:54 +02:00
*/
const revoke = async id => {
return strapi
.query('admin::api-token')
.delete({ select: SELECT_FIELDS, populate: POPULATE_FIELDS, where: { id } });
2021-08-31 15:31:54 +02:00
};
/**
* @param {string|number} id
*
* @returns {Promise<Omit<ApiToken, 'accessKey'>>}
*/
2021-09-06 15:14:45 +02:00
const getById = async id => {
return getBy({ id });
};
/**
* @param {string} name
*
* @returns {Promise<Omit<ApiToken, 'accessKey'>>}
*/
const getByName = async name => {
return getBy({ name });
};
/**
* @param {string|number} id
* @param {Object} attributes
* @param {TokenType} attributes.type
* @param {string} attributes.name
* @param {string} [attributes.description]
*
* @returns {Promise<Omit<ApiToken, 'accessKey'>>}
*/
const update = async (id, attributes) => {
// retrieve token without permissions
2022-08-05 12:31:16 +02:00
const oldToken = await strapi.query('admin::api-token').findOne({ where: { id } });
if (!oldToken) {
throw new NotFoundError('Token not found');
2022-08-05 12:31:16 +02:00
}
assertCustomTokenPermissionsValidity({
...oldToken,
...attributes,
type: attributes.type || oldToken.type,
});
2022-08-05 12:31:16 +02:00
const token = await strapi.query('admin::api-token').update({
select: SELECT_FIELDS,
populate: POPULATE_FIELDS,
where: { id },
data: omit('permissions', attributes),
});
2022-08-05 12:31:16 +02:00
let permissions = {};
2022-08-09 10:49:44 +02:00
if (token.type === constants.API_TOKEN_TYPE.CUSTOM) {
2022-08-05 12:31:16 +02:00
const permissionsToDelete = differenceBy('action', token.permissions, attributes.permissions);
const permissionsToCreate = differenceBy('action', attributes.permissions, token.permissions);
// TODO: this is deleting the permission, but not the link to this token
2022-08-05 12:31:16 +02:00
await strapi
.query('admin::token-permission')
.deleteMany({ where: { action: map('action', permissionsToDelete) } });
// TODO: This is only creating the permission, not linking it to this token
2022-08-05 12:31:16 +02:00
await strapi
.query('admin::token-permission')
.createMany({ data: permissionsToCreate.map(action => ({ action, token: id })) });
2022-08-05 12:31:16 +02:00
permissions = {
permissions: await strapi.entityService.load('admin::api-token', token, 'permissions'),
};
} else {
// TODO: if type is changing from custom, make sure old permissions get removed
}
2022-08-05 12:31:16 +02:00
return { ...token, ...permissions };
};
/**
* @param {Object} whereParams
* @param {string|number} [whereParams.id]
* @param {string} [whereParams.name]
* @param {string} [whereParams.description]
* @param {string} [whereParams.accessKey]
*
* @returns {Promise<Omit<ApiToken, 'accessKey'> | null>}
*/
const getBy = async (whereParams = {}) => {
if (Object.keys(whereParams).length === 0) {
return null;
}
return strapi
.query('admin::api-token')
.findOne({ select: SELECT_FIELDS, populate: POPULATE_FIELDS, where: whereParams });
};
module.exports = {
create,
exists,
checkSaltIsDefined,
hash,
list,
2021-08-31 15:31:54 +02:00
revoke,
2021-09-06 15:14:45 +02:00
getById,
update,
getByName,
getBy,
};