Merge pull request #14230 from strapi/api-token-v2/restrict-lifespans

Api token v2/restrict lifespans
This commit is contained in:
Ben Irvin 2022-08-29 17:28:20 +02:00 committed by GitHub
commit 4bb8e1be34
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 107 additions and 10 deletions

View File

@ -3,6 +3,7 @@
const { ApplicationError } = require('@strapi/utils').errors;
const { omit } = require('lodash/fp');
const createContext = require('../../../../../../test/helpers/create-context');
const constants = require('../../services/constants');
const apiTokenController = require('../api-token');
describe('API Token Controller', () => {
@ -65,8 +66,8 @@ describe('API Token Controller', () => {
expect(created).toHaveBeenCalled();
});
test('Create API Token with lifespan', async () => {
const lifespan = 90 * 24 * 60 * 60 * 1000; // 90 days
test('Create API Token with valid lifespan', async () => {
const lifespan = constants.API_TOKEN_LIFESPANS.DAYS_7;
const createBody = {
...body,
lifespan,
@ -103,6 +104,34 @@ describe('API Token Controller', () => {
});
test('Throws with invalid lifespan', async () => {
const lifespan = 1235; // not in constants.API_TOKEN_LIFESPANS
const createBody = {
...body,
lifespan,
};
const create = jest.fn();
const created = jest.fn();
const ctx = createContext({ body: createBody }, { created });
global.strapi = {
admin: {
services: {
'api-token': {
create,
},
},
},
};
expect(async () => {
await apiTokenController.create(ctx);
}).rejects.toThrow(/lifespan must be one of the following values/);
expect(create).not.toHaveBeenCalled();
expect(created).not.toHaveBeenCalled();
});
test('Throws with negative lifespan', async () => {
const lifespan = -1;
const createBody = {
...body,
@ -125,13 +154,14 @@ describe('API Token Controller', () => {
expect(async () => {
await apiTokenController.create(ctx);
}).rejects.toThrow('lifespan must be greater than or equal to 1');
}).rejects.toThrow(/lifespan must be one of the following values/);
expect(create).not.toHaveBeenCalled();
expect(created).not.toHaveBeenCalled();
});
test('Ignores a received expiresAt', async () => {
const lifespan = 90 * 24 * 60 * 60 * 1000; // 90 days
const lifespan = constants.API_TOKEN_LIFESPANS.DAYS_7;
const createBody = {
...body,
expiresAt: 1234,

View File

@ -4,6 +4,7 @@ const { NotFoundError } = require('@strapi/utils/lib/errors');
const crypto = require('crypto');
const { omit, uniq } = require('lodash/fp');
const apiTokenService = require('../api-token');
const constants = require('../constants');
describe('API Token', () => {
const mockedApiToken = {
@ -70,7 +71,7 @@ describe('API Token', () => {
name: 'api-token_tests-name',
description: 'api-token_tests-description',
type: 'read-only',
lifespan: 123456,
lifespan: constants.API_TOKEN_LIFESPANS.DAYS_90,
};
const expectedExpires = Date.now() + attributes.lifespan;
@ -106,6 +107,31 @@ describe('API Token', () => {
expect(res.expiresAt).toBe(expectedExpires);
});
test('It throws when creating a token with invalid lifespan', async () => {
const attributes = {
name: 'api-token_tests-name',
description: 'api-token_tests-description',
type: 'read-only',
lifespan: 12345,
};
const create = jest.fn(({ data }) => Promise.resolve(data));
global.strapi = {
query() {
return { create };
},
config: {
get: jest.fn(() => ''),
},
};
expect(async () => {
await apiTokenService.create(attributes);
}).rejects.toThrow(/lifespan/);
expect(create).not.toHaveBeenCalled();
});
test('Creates a custom token', async () => {
const attributes = {
name: 'api-token_tests-name',

View File

@ -67,6 +67,24 @@ const assertCustomTokenPermissionsValidity = (attributes) => {
}
};
/**
* Assert that a token's permissions attribute is valid for its type
*
* @param {ApiToken} token
*/
const assertValidLifespan = ({ lifespan }) => {
if (isNil(lifespan)) {
return;
}
if (!Object.values(constants.API_TOKEN_LIFESPANS).includes(lifespan)) {
throw new ValidationError(
`lifespan must be one of the following values:
${Object.values(constants.API_TOKEN_LIFESPANS).join(', ')}`
);
}
};
/**
* Flatten a token's database permissions objects to an array of strings
*
@ -173,6 +191,7 @@ const create = async (attributes) => {
const accessKey = crypto.randomBytes(128).toString('hex');
assertCustomTokenPermissionsValidity(attributes);
assertValidLifespan(attributes);
// Create the token
const apiToken = await strapi.query('admin::api-token').create({
@ -348,6 +367,8 @@ const update = async (id, attributes) => {
});
}
assertValidLifespan(attributes);
const updatedToken = await strapi.query('admin::api-token').update({
select: SELECT_FIELDS,
populate: POPULATE_FIELDS,

View File

@ -1,5 +1,7 @@
'use strict';
const DAY_IN_MS = 24 * 60 * 60 * 1000;
module.exports = {
CONTENT_TYPE_SECTION: 'contentTypes',
SUPER_ADMIN_CODE: 'strapi-super-admin',
@ -15,4 +17,11 @@ module.exports = {
FULL_ACCESS: 'full-access',
CUSTOM: 'custom',
},
// The front-end only displays these values
API_TOKEN_LIFESPANS: {
UNLIMITED: null,
DAYS_7: 7 * DAY_IN_MS,
DAYS_30: 30 * DAY_IN_MS,
DAYS_90: 90 * DAY_IN_MS,
},
};

View File

@ -3,6 +3,7 @@
const { omit } = require('lodash');
const { createStrapiInstance } = require('../../../../../test/helpers/strapi');
const { createAuthRequest } = require('../../../../../test/helpers/request');
const constants = require('../services/constants');
describe('Admin API Token v2 CRUD (e2e)', () => {
let rq;
@ -265,11 +266,11 @@ describe('Admin API Token v2 CRUD (e2e)', () => {
error: {
status: 400,
name: 'ValidationError',
message: 'lifespan must be greater than or equal to 1',
message: expect.stringContaining('lifespan must be one of the following values'),
details: {
errors: expect.arrayContaining([
expect.objectContaining({
message: 'lifespan must be greater than or equal to 1',
message: expect.stringContaining('lifespan must be one of the following values'),
name: 'ValidationError',
}),
]),
@ -459,7 +460,7 @@ describe('Admin API Token v2 CRUD (e2e)', () => {
);
tokens.push(await createValidToken({ type: 'full-access' }));
tokens.push(await createValidToken({ type: 'read-only' }));
tokens.push(await createValidToken({ lifespan: 12345 }));
tokens.push(await createValidToken({ lifespan: constants.API_TOKEN_LIFESPANS.DAYS_7 }));
tokens.push(await createValidToken());
const res = await rq({

View File

@ -10,7 +10,12 @@ const apiTokenCreationSchema = yup
description: yup.string().optional(),
type: yup.string().oneOf(Object.values(constants.API_TOKEN_TYPE)).required(),
permissions: yup.array().of(yup.string()).nullable(),
lifespan: yup.number().integer().min(1).nullable(),
lifespan: yup
.number()
.integer()
.min(1)
.oneOf(Object.values(constants.API_TOKEN_LIFESPANS))
.nullable(),
})
.noUnknown()
.strict();
@ -22,7 +27,12 @@ const apiTokenUpdateSchema = yup
description: yup.string().nullable(),
type: yup.string().oneOf(Object.values(constants.API_TOKEN_TYPE)).notNull(),
permissions: yup.array().of(yup.string()).nullable(),
lifespan: yup.number().integer().min(1).nullable(),
lifespan: yup
.number()
.integer()
.min(1)
.oneOf(Object.values(constants.API_TOKEN_LIFESPANS))
.nullable(),
})
.noUnknown()
.strict();