mirror of
https://github.com/strapi/strapi.git
synced 2025-09-26 17:00:55 +00:00
add lastUsed
This commit is contained in:
parent
1436345e78
commit
e28c9e7ec9
@ -10,6 +10,8 @@ describe('API Token', () => {
|
|||||||
hexedString: '6170692d746f6b656e5f746573742d72616e646f6d2d6279746573',
|
hexedString: '6170692d746f6b656e5f746573742d72616e646f6d2d6279746573',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SELECT_FIELDS = ['id', 'name', 'description', 'lastUsed', 'type', 'createdAt', 'updatedAt'];
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
jest
|
jest
|
||||||
.spyOn(crypto, 'randomBytes')
|
.spyOn(crypto, 'randomBytes')
|
||||||
@ -42,7 +44,7 @@ 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', 'createdAt'],
|
select: SELECT_FIELDS,
|
||||||
data: {
|
data: {
|
||||||
...attributes,
|
...attributes,
|
||||||
accessKey: apiTokenService.hash(mockedApiToken.hexedString),
|
accessKey: apiTokenService.hash(mockedApiToken.hexedString),
|
||||||
@ -138,7 +140,7 @@ describe('API Token', () => {
|
|||||||
const res = await apiTokenService.list();
|
const res = await apiTokenService.list();
|
||||||
|
|
||||||
expect(findMany).toHaveBeenCalledWith({
|
expect(findMany).toHaveBeenCalledWith({
|
||||||
select: ['id', 'name', 'description', 'type', 'createdAt'],
|
select: SELECT_FIELDS,
|
||||||
orderBy: { name: 'ASC' },
|
orderBy: { name: 'ASC' },
|
||||||
populate: ['permissions'],
|
populate: ['permissions'],
|
||||||
});
|
});
|
||||||
@ -166,7 +168,7 @@ describe('API Token', () => {
|
|||||||
const res = await apiTokenService.revoke(token.id);
|
const res = await apiTokenService.revoke(token.id);
|
||||||
|
|
||||||
expect(mockedDelete).toHaveBeenCalledWith({
|
expect(mockedDelete).toHaveBeenCalledWith({
|
||||||
select: ['id', 'name', 'description', 'type', 'createdAt'],
|
select: SELECT_FIELDS,
|
||||||
where: { id: token.id },
|
where: { id: token.id },
|
||||||
populate: ['permissions'],
|
populate: ['permissions'],
|
||||||
});
|
});
|
||||||
@ -185,7 +187,7 @@ describe('API Token', () => {
|
|||||||
const res = await apiTokenService.revoke(42);
|
const res = await apiTokenService.revoke(42);
|
||||||
|
|
||||||
expect(mockedDelete).toHaveBeenCalledWith({
|
expect(mockedDelete).toHaveBeenCalledWith({
|
||||||
select: ['id', 'name', 'description', 'type', 'createdAt'],
|
select: SELECT_FIELDS,
|
||||||
where: { id: 42 },
|
where: { id: 42 },
|
||||||
populate: ['permissions'],
|
populate: ['permissions'],
|
||||||
});
|
});
|
||||||
@ -213,7 +215,7 @@ describe('API Token', () => {
|
|||||||
const res = await apiTokenService.getById(token.id);
|
const res = await apiTokenService.getById(token.id);
|
||||||
|
|
||||||
expect(findOne).toHaveBeenCalledWith({
|
expect(findOne).toHaveBeenCalledWith({
|
||||||
select: ['id', 'name', 'description', 'type', 'createdAt'],
|
select: SELECT_FIELDS,
|
||||||
where: { id: token.id },
|
where: { id: token.id },
|
||||||
populate: ['permissions'],
|
populate: ['permissions'],
|
||||||
});
|
});
|
||||||
@ -232,7 +234,7 @@ describe('API Token', () => {
|
|||||||
const res = await apiTokenService.getById(42);
|
const res = await apiTokenService.getById(42);
|
||||||
|
|
||||||
expect(findOne).toHaveBeenCalledWith({
|
expect(findOne).toHaveBeenCalledWith({
|
||||||
select: ['id', 'name', 'description', 'type', 'createdAt'],
|
select: SELECT_FIELDS,
|
||||||
where: { id: 42 },
|
where: { id: 42 },
|
||||||
populate: ['permissions'],
|
populate: ['permissions'],
|
||||||
});
|
});
|
||||||
@ -285,7 +287,7 @@ describe('API Token', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(update).toHaveBeenCalledWith({
|
expect(update).toHaveBeenCalledWith({
|
||||||
select: ['id', 'name', 'description', 'type', 'createdAt'],
|
select: SELECT_FIELDS,
|
||||||
where: { id },
|
where: { id },
|
||||||
data: attributes,
|
data: attributes,
|
||||||
populate: ['permissions'],
|
populate: ['permissions'],
|
||||||
@ -397,7 +399,7 @@ describe('API Token', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(update).toHaveBeenCalledWith({
|
expect(update).toHaveBeenCalledWith({
|
||||||
select: ['id', 'name', 'description', 'type', 'createdAt'],
|
select: SELECT_FIELDS,
|
||||||
where: { id },
|
where: { id },
|
||||||
data: omit(['permissions'], updatedAttributes),
|
data: omit(['permissions'], updatedAttributes),
|
||||||
populate: expect.anything(), // it doesn't matter how this is used
|
populate: expect.anything(), // it doesn't matter how this is used
|
||||||
@ -406,6 +408,81 @@ describe('API Token', () => {
|
|||||||
expect(res).toEqual(updatedAttributes);
|
expect(res).toEqual(updatedAttributes);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Updates a non-permissions field of a custom token', async () => {
|
||||||
|
const id = 1;
|
||||||
|
|
||||||
|
const originalToken = {
|
||||||
|
id,
|
||||||
|
name: 'api-token_tests-name',
|
||||||
|
description: 'api-token_tests-description',
|
||||||
|
type: 'custom',
|
||||||
|
permissions: ['admin::subject.keepThisAction', 'admin::subject.oldAction'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedAttributes = {
|
||||||
|
name: 'api-token_tests-updated-name',
|
||||||
|
type: 'custom',
|
||||||
|
};
|
||||||
|
|
||||||
|
const update = jest.fn(({ data }) => Promise.resolve(data));
|
||||||
|
const findOne = jest.fn().mockResolvedValue(omit('permissions', originalToken));
|
||||||
|
const deleteFn = jest.fn();
|
||||||
|
const create = jest.fn();
|
||||||
|
const load = jest
|
||||||
|
.fn()
|
||||||
|
// first call to load original permissions
|
||||||
|
.mockResolvedValueOnce(
|
||||||
|
Promise.resolve(
|
||||||
|
originalToken.permissions.map(p => {
|
||||||
|
return {
|
||||||
|
action: p,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// second call to check new permissions
|
||||||
|
.mockResolvedValueOnce(
|
||||||
|
Promise.resolve(
|
||||||
|
originalToken.permissions.map(p => {
|
||||||
|
return {
|
||||||
|
action: p,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
global.strapi = {
|
||||||
|
query() {
|
||||||
|
return {
|
||||||
|
update,
|
||||||
|
findOne,
|
||||||
|
delete: deleteFn,
|
||||||
|
create,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
get: jest.fn(() => ''),
|
||||||
|
},
|
||||||
|
entityService: {
|
||||||
|
load,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await apiTokenService.update(id, updatedAttributes);
|
||||||
|
|
||||||
|
expect(update).toHaveBeenCalledWith({
|
||||||
|
select: SELECT_FIELDS,
|
||||||
|
where: { id },
|
||||||
|
data: omit(['permissions'], updatedAttributes),
|
||||||
|
populate: expect.anything(), // it doesn't matter how this is used
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res).toEqual({
|
||||||
|
permissions: originalToken.permissions,
|
||||||
|
...updatedAttributes,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('getByName', () => {
|
describe('getByName', () => {
|
||||||
const token = {
|
const token = {
|
||||||
id: 1,
|
id: 1,
|
||||||
@ -426,7 +503,7 @@ describe('API Token', () => {
|
|||||||
const res = await apiTokenService.getByName(token.name);
|
const res = await apiTokenService.getByName(token.name);
|
||||||
|
|
||||||
expect(findOne).toHaveBeenCalledWith({
|
expect(findOne).toHaveBeenCalledWith({
|
||||||
select: ['id', 'name', 'description', 'type', 'createdAt'],
|
select: SELECT_FIELDS,
|
||||||
where: { name: token.name },
|
where: { name: token.name },
|
||||||
populate: ['permissions'],
|
populate: ['permissions'],
|
||||||
});
|
});
|
||||||
@ -445,7 +522,7 @@ describe('API Token', () => {
|
|||||||
const res = await apiTokenService.getByName('unexistant-name');
|
const res = await apiTokenService.getByName('unexistant-name');
|
||||||
|
|
||||||
expect(findOne).toHaveBeenCalledWith({
|
expect(findOne).toHaveBeenCalledWith({
|
||||||
select: ['id', 'name', 'description', 'type', 'createdAt'],
|
select: SELECT_FIELDS,
|
||||||
where: { name: 'unexistant-name' },
|
where: { name: 'unexistant-name' },
|
||||||
populate: ['permissions'],
|
populate: ['permissions'],
|
||||||
});
|
});
|
||||||
|
@ -16,6 +16,7 @@ const constants = require('../services/constants');
|
|||||||
* @property {string} name
|
* @property {string} name
|
||||||
* @property {string} [description]
|
* @property {string} [description]
|
||||||
* @property {string} accessKey
|
* @property {string} accessKey
|
||||||
|
* @property {number} lastUsed
|
||||||
* @property {TokenType} type
|
* @property {TokenType} type
|
||||||
* @property {(number|ApiTokenPermission)[]} [permissions]
|
* @property {(number|ApiTokenPermission)[]} [permissions]
|
||||||
*/
|
*/
|
||||||
@ -29,7 +30,7 @@ const constants = require('../services/constants');
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/** @constant {Array<string>} */
|
/** @constant {Array<string>} */
|
||||||
const SELECT_FIELDS = ['id', 'name', 'description', 'type', 'createdAt'];
|
const SELECT_FIELDS = ['id', 'name', 'description', 'lastUsed', 'type', 'createdAt', 'updatedAt'];
|
||||||
|
|
||||||
/** @constant {Array<string>} */
|
/** @constant {Array<string>} */
|
||||||
const POPULATE_FIELDS = ['permissions'];
|
const POPULATE_FIELDS = ['permissions'];
|
||||||
@ -50,6 +51,7 @@ const assertCustomTokenPermissionsValidity = attributes => {
|
|||||||
* @param {Object} whereParams
|
* @param {Object} whereParams
|
||||||
* @param {string|number} [whereParams.id]
|
* @param {string|number} [whereParams.id]
|
||||||
* @param {string} [whereParams.name]
|
* @param {string} [whereParams.name]
|
||||||
|
* @param {number} [whereParams.lastUsed]
|
||||||
* @param {string} [whereParams.description]
|
* @param {string} [whereParams.description]
|
||||||
* @param {string} [whereParams.accessKey]
|
* @param {string} [whereParams.accessKey]
|
||||||
*
|
*
|
||||||
@ -77,6 +79,7 @@ const hash = accessKey => {
|
|||||||
* @param {Object} attributes
|
* @param {Object} attributes
|
||||||
* @param {TokenType} attributes.type
|
* @param {TokenType} attributes.type
|
||||||
* @param {string} attributes.name
|
* @param {string} attributes.name
|
||||||
|
* @param {number} attributes.lastUsed
|
||||||
* @param {string[]} [attributes.permissions]
|
* @param {string[]} [attributes.permissions]
|
||||||
* @param {string} [attributes.description]
|
* @param {string} [attributes.description]
|
||||||
*
|
*
|
||||||
@ -199,6 +202,8 @@ const getByName = async name => {
|
|||||||
* @param {Object} attributes
|
* @param {Object} attributes
|
||||||
* @param {TokenType} attributes.type
|
* @param {TokenType} attributes.type
|
||||||
* @param {string} attributes.name
|
* @param {string} attributes.name
|
||||||
|
* @param {number} attributes.lastUsed
|
||||||
|
* @param {string[]} [attributes.permissions]
|
||||||
* @param {string} [attributes.description]
|
* @param {string} [attributes.description]
|
||||||
*
|
*
|
||||||
* @returns {Promise<Omit<ApiToken, 'accessKey'>>}
|
* @returns {Promise<Omit<ApiToken, 'accessKey'>>}
|
||||||
@ -211,12 +216,19 @@ const update = async (id, attributes) => {
|
|||||||
throw new NotFoundError('Token not found');
|
throw new NotFoundError('Token not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: allow updating only the non-permissions attributes of a custom token
|
const changingTypeToCustom =
|
||||||
|
attributes.type === constants.API_TOKEN_TYPE.custom &&
|
||||||
|
originalToken.type !== constants.API_TOKEN_TYPE.custom;
|
||||||
|
|
||||||
|
// if we're updating the permissions on any token type, or changing from non-custom to custom, ensure they're still valid
|
||||||
|
// if neither type nor permissions are changing, we don't need to validate again or else we can't allow partial update
|
||||||
|
if (attributes.permissions || changingTypeToCustom) {
|
||||||
assertCustomTokenPermissionsValidity({
|
assertCustomTokenPermissionsValidity({
|
||||||
...omit(['permissions'], originalToken),
|
...originalToken,
|
||||||
...attributes,
|
...attributes,
|
||||||
type: attributes.type || originalToken.type,
|
type: attributes.type || originalToken.type,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const updatedToken = await strapi.query('admin::api-token').update({
|
const updatedToken = await strapi.query('admin::api-token').update({
|
||||||
select: SELECT_FIELDS,
|
select: SELECT_FIELDS,
|
||||||
@ -225,7 +237,8 @@ const update = async (id, attributes) => {
|
|||||||
data: omit('permissions', attributes),
|
data: omit('permissions', attributes),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (updatedToken.type === constants.API_TOKEN_TYPE.CUSTOM) {
|
// custom tokens need to have their permissions updated as well
|
||||||
|
if (updatedToken.type === constants.API_TOKEN_TYPE.CUSTOM && attributes.permissions) {
|
||||||
const currentPermissionsResult =
|
const currentPermissionsResult =
|
||||||
(await strapi.entityService.load('admin::api-token', updatedToken, 'permissions')) || [];
|
(await strapi.entityService.load('admin::api-token', updatedToken, 'permissions')) || [];
|
||||||
|
|
||||||
@ -284,7 +297,7 @@ const update = async (id, attributes) => {
|
|||||||
// method attempting to createMany permissions, then update token with those permissions -- createMany doesn't return the ids, and we can't query for them
|
// method attempting to createMany permissions, then update token with those permissions -- createMany doesn't return the ids, and we can't query for them
|
||||||
}
|
}
|
||||||
// if type is not custom, make sure any old permissions get removed
|
// if type is not custom, make sure any old permissions get removed
|
||||||
else {
|
else if (updatedToken.type !== constants.API_TOKEN_TYPE.CUSTOM) {
|
||||||
await strapi.query('admin::token-permission').delete({
|
await strapi.query('admin::token-permission').delete({
|
||||||
where: { token: id },
|
where: { token: id },
|
||||||
});
|
});
|
||||||
@ -307,6 +320,7 @@ const update = async (id, attributes) => {
|
|||||||
* @param {Object} whereParams
|
* @param {Object} whereParams
|
||||||
* @param {string|number} [whereParams.id]
|
* @param {string|number} [whereParams.id]
|
||||||
* @param {string} [whereParams.name]
|
* @param {string} [whereParams.name]
|
||||||
|
* @param {number} [whereParams.lastUsed]
|
||||||
* @param {string} [whereParams.description]
|
* @param {string} [whereParams.description]
|
||||||
* @param {string} [whereParams.accessKey]
|
* @param {string} [whereParams.accessKey]
|
||||||
*
|
*
|
||||||
|
Loading…
x
Reference in New Issue
Block a user