mirror of
https://github.com/strapi/strapi.git
synced 2025-11-02 19:04:38 +00:00
Attach permission model to tokens, update api token' strategy & services
This commit is contained in:
parent
701aa19911
commit
3a9709bdbf
@ -47,5 +47,12 @@ module.exports = {
|
||||
configurable: false,
|
||||
required: true,
|
||||
},
|
||||
permissions: {
|
||||
type: 'relation',
|
||||
target: 'admin::token-permission',
|
||||
relation: 'oneToMany',
|
||||
mappedBy: 'token',
|
||||
configurable: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -5,4 +5,5 @@ module.exports = {
|
||||
user: { schema: require('./User') },
|
||||
role: { schema: require('./Role') },
|
||||
'api-token': { schema: require('./api-token') },
|
||||
'token-permission': { schema: require('./token-permission') },
|
||||
};
|
||||
|
||||
36
packages/core/admin/server/content-types/token-permission.js
Normal file
36
packages/core/admin/server/content-types/token-permission.js
Normal file
@ -0,0 +1,36 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
collectionName: 'strapi_token_permissions',
|
||||
info: {
|
||||
name: 'API Token Permission',
|
||||
description: '',
|
||||
singularName: 'token-permission',
|
||||
pluralName: 'token-permissions',
|
||||
displayName: 'API Token Permission',
|
||||
},
|
||||
options: {},
|
||||
pluginOptions: {
|
||||
'content-manager': {
|
||||
visible: false,
|
||||
},
|
||||
'content-type-builder': {
|
||||
visible: false,
|
||||
},
|
||||
},
|
||||
attributes: {
|
||||
action: {
|
||||
type: 'string',
|
||||
minLength: 1,
|
||||
configurable: false,
|
||||
required: true,
|
||||
},
|
||||
token: {
|
||||
configurable: false,
|
||||
type: 'relation',
|
||||
relation: 'manyToOne',
|
||||
inversedBy: 'permissions',
|
||||
target: 'admin::api-token',
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -1,9 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
const crypto = require('crypto');
|
||||
const { omit } = require('lodash/fp');
|
||||
|
||||
/**
|
||||
* @typedef {'read-only'|'full-access'} TokenType
|
||||
* @typedef {'read-only'|'full-access'|'custom'} TokenType
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -14,11 +15,35 @@ const crypto = require('crypto');
|
||||
* @property {string} [description]
|
||||
* @property {string} accessKey
|
||||
* @property {TokenType} type
|
||||
* @property {(number|ApiTokenPermission)[]} [permissions]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef ApiTokenPermission
|
||||
*
|
||||
* @property {number|string} id
|
||||
* @property {string} action
|
||||
* @property {ApiToken|number} [token]
|
||||
*/
|
||||
|
||||
/** @constant {Array<string>} */
|
||||
const SELECT_FIELDS = ['id', 'name', 'description', 'type', 'createdAt'];
|
||||
|
||||
/** @constant {Array<string>} */
|
||||
const POPULATE_FIELDS = ['permissions'];
|
||||
|
||||
const assertCustomTokenPermissionsValidity = attributes => {
|
||||
// Ensure non-custom tokens doesn't have permissions
|
||||
if (attributes.type !== 'custom' && attributes.permissions) {
|
||||
throw new Error('Non-custom tokens should not references permissions');
|
||||
}
|
||||
|
||||
// Custom type tokens should always have permissions attached to them
|
||||
if (attributes.type === 'custom' && !attributes.permissions) {
|
||||
throw new Error('Missing permissions attributes for custom token');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} whereParams
|
||||
* @param {string|number} [whereParams.id]
|
||||
@ -50,6 +75,7 @@ const hash = accessKey => {
|
||||
* @param {Object} attributes
|
||||
* @param {TokenType} attributes.type
|
||||
* @param {string} attributes.name
|
||||
* @param {string[]} [attributes.permissions]
|
||||
* @param {string} [attributes.description]
|
||||
*
|
||||
* @returns {Promise<ApiToken>}
|
||||
@ -57,18 +83,29 @@ const hash = accessKey => {
|
||||
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({
|
||||
select: SELECT_FIELDS,
|
||||
data: {
|
||||
...attributes,
|
||||
...omit('permissions', attributes),
|
||||
accessKey: hash(accessKey),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...apiToken,
|
||||
accessKey,
|
||||
};
|
||||
const result = { ...apiToken, accessKey };
|
||||
|
||||
// If this is a custom type token, create and link the associated permissions
|
||||
if (attributes.type === 'custom') {
|
||||
const permissions = await strapi
|
||||
.query('admin::token-permissions')
|
||||
.createMany({ data: permissions.map(action => ({ action, token: apiToken.id })) });
|
||||
|
||||
Object.assign(result, { permissions });
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -97,6 +134,7 @@ For security reasons, prefer storing the secret in an environment variable and r
|
||||
const list = async () => {
|
||||
return strapi.query('admin::api-token').findMany({
|
||||
select: SELECT_FIELDS,
|
||||
populate: POPULATE_FIELDS,
|
||||
orderBy: { name: 'ASC' },
|
||||
});
|
||||
};
|
||||
@ -107,7 +145,9 @@ const list = async () => {
|
||||
* @returns {Promise<Omit<ApiToken, 'accessKey'>>}
|
||||
*/
|
||||
const revoke = async id => {
|
||||
return strapi.query('admin::api-token').delete({ select: SELECT_FIELDS, where: { id } });
|
||||
return strapi
|
||||
.query('admin::api-token')
|
||||
.delete({ select: SELECT_FIELDS, populate: POPULATE_FIELDS, where: { id } });
|
||||
};
|
||||
|
||||
/**
|
||||
@ -138,9 +178,14 @@ const getByName = async name => {
|
||||
* @returns {Promise<Omit<ApiToken, 'accessKey'>>}
|
||||
*/
|
||||
const update = async (id, attributes) => {
|
||||
return strapi
|
||||
.query('admin::api-token')
|
||||
.update({ where: { id }, data: attributes, select: SELECT_FIELDS });
|
||||
assertCustomTokenPermissionsValidity(attributes);
|
||||
|
||||
return strapi.query('admin::api-token').update({
|
||||
select: SELECT_FIELDS,
|
||||
populate: POPULATE_FIELDS,
|
||||
where: { id },
|
||||
data: omit('permissions', attributes),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@ -157,7 +202,9 @@ const getBy = async (whereParams = {}) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
return strapi.query('admin::api-token').findOne({ select: SELECT_FIELDS, where: whereParams });
|
||||
return strapi
|
||||
.query('admin::api-token')
|
||||
.findOne({ select: SELECT_FIELDS, populate: POPULATE_FIELDS, where: whereParams });
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
||||
@ -13,5 +13,6 @@ module.exports = {
|
||||
API_TOKEN_TYPE: {
|
||||
READ_ONLY: 'read-only',
|
||||
FULL_ACCESS: 'full-access',
|
||||
CUSTOM: 'custom',
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const { castArray } = require('lodash/fp');
|
||||
const { UnauthorizedError, ForbiddenError } = require('@strapi/utils').errors;
|
||||
const constants = require('../services/constants');
|
||||
const { getService } = require('../utils');
|
||||
@ -37,29 +38,52 @@ const authenticate = async ctx => {
|
||||
return { authenticated: false };
|
||||
}
|
||||
|
||||
if (apiToken.type === constants.API_TOKEN_TYPE.CUSTOM) {
|
||||
const ability = await strapi.contentAPI.permissions.engine.generateAbility(
|
||||
apiToken.permissions.map(({ action }) => ({ action }))
|
||||
);
|
||||
|
||||
return { authenticated: true, ability, credentials: apiToken };
|
||||
}
|
||||
|
||||
return { authenticated: true, credentials: apiToken };
|
||||
};
|
||||
|
||||
/** @type {import('.').VerifyFunction} */
|
||||
const verify = (auth, config) => {
|
||||
const { credentials: apiToken } = auth;
|
||||
const { credentials: apiToken, ability } = auth;
|
||||
|
||||
if (!apiToken) {
|
||||
throw new UnauthorizedError();
|
||||
}
|
||||
|
||||
// Full access
|
||||
if (apiToken.type === constants.API_TOKEN_TYPE.FULL_ACCESS) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* If you don't have `full-access` you can only access `find` and `findOne`
|
||||
* scopes. If the route has no scope, then you can't get access to it.
|
||||
*/
|
||||
// Read only
|
||||
else if (apiToken.type === constants.API_TOKEN_TYPE.READ_ONLY) {
|
||||
/**
|
||||
* If you don't have `full-access` you can only access `find` and `findOne`
|
||||
* scopes. If the route has no scope, then you can't get access to it.
|
||||
*/
|
||||
const scopes = castArray(config.scope);
|
||||
|
||||
const scopes = Array.isArray(config.scope) ? config.scope : [config.scope];
|
||||
if (config.scope && scopes.every(isReadScope)) {
|
||||
return;
|
||||
if (config.scope && scopes.every(isReadScope)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Custom
|
||||
else if (apiToken.type === constants.API_TOKEN_TYPE.CUSTOM && ability) {
|
||||
const scopes = castArray(config.scope);
|
||||
|
||||
const isAllowed = scopes.every(scope => ability.can(scope));
|
||||
|
||||
if (isAllowed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new ForbiddenError();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user