mirror of
https://github.com/strapi/strapi.git
synced 2026-05-03 16:22:30 +00:00
1019 lines
32 KiB
TypeScript
1019 lines
32 KiB
TypeScript
import { createStrapiInstance } from 'api-tests/strapi';
|
|
import { createAuthRequest } from 'api-tests/request';
|
|
import { createAgent } from 'api-tests/agent';
|
|
import { createUtils } from 'api-tests/utils';
|
|
import type { Core } from '@strapi/types';
|
|
import constants from '../../../../packages/core/admin/server/src/services/constants';
|
|
|
|
describe('Admin Admin Token CRUD (api)', () => {
|
|
let strapi: Core.Strapi;
|
|
let rq: Awaited<ReturnType<typeof createAuthRequest>>;
|
|
let rqOther: Awaited<ReturnType<typeof createAuthRequest>>;
|
|
let rqEditor: Awaited<ReturnType<typeof createAuthRequest>>;
|
|
let editorUserId: number;
|
|
let saOtherUserId: number;
|
|
let editorRoleId: number;
|
|
let now: number;
|
|
let nowSpy: jest.SpyInstance;
|
|
|
|
// The two actions we assign to the editor role for ceiling tests.
|
|
// These must be real registered actions (no subject).
|
|
const EDITOR_ACTION = 'admin::webhooks.read';
|
|
const UNGRANTED_ACTION = 'admin::webhooks.create';
|
|
|
|
const deleteAllAdminTokens = async () => {
|
|
await strapi.db.query('admin::api-token').deleteMany({ where: { kind: 'admin' } });
|
|
};
|
|
|
|
beforeAll(async () => {
|
|
strapi = await createStrapiInstance({
|
|
// @ts-expect-error - the JS test helper supports `register`, but its TS type is incomplete
|
|
register({ strapi: instance }) {
|
|
instance.config.set('features.future.adminTokens', true);
|
|
},
|
|
});
|
|
strapi.config.set('admin.secrets.encryptionKey', 'test-encryption-key');
|
|
|
|
rq = await createAuthRequest({ strapi });
|
|
now = Date.now();
|
|
nowSpy = jest.spyOn(Date, 'now').mockImplementation(() => now);
|
|
|
|
const utils = createUtils(strapi);
|
|
|
|
const superAdminRole = await utils.getSuperAdminRole();
|
|
const saOtherUser = await utils.createUser({
|
|
email: 'sa-other@test.com',
|
|
firstname: 'Other',
|
|
lastname: 'SA',
|
|
isActive: true,
|
|
roles: [superAdminRole.id],
|
|
});
|
|
saOtherUserId = saOtherUser.id;
|
|
rqOther = await createAuthRequest({
|
|
strapi,
|
|
userInfo: {
|
|
email: 'sa-other@test.com',
|
|
firstname: 'Other',
|
|
lastname: 'SA',
|
|
password: 'Password123',
|
|
},
|
|
});
|
|
|
|
// Create a custom role with one known action for ceiling tests.
|
|
const editorRole = await utils.createRole({
|
|
name: 'token-ceiling-test-role',
|
|
description: 'Role used to test admin token permission ceiling',
|
|
});
|
|
editorRoleId = editorRole.id;
|
|
await utils.assignPermissionsToRole(editorRole.id, [
|
|
{ action: EDITOR_ACTION, subject: null, conditions: [], properties: {} },
|
|
{ action: 'admin::admin-tokens.create', subject: null, conditions: [], properties: {} },
|
|
{ action: 'admin::admin-tokens.read', subject: null, conditions: [], properties: {} },
|
|
{ action: 'admin::admin-tokens.update', subject: null, conditions: [], properties: {} },
|
|
{ action: 'admin::admin-tokens.delete', subject: null, conditions: [], properties: {} },
|
|
{ action: 'admin::admin-tokens.regenerate', subject: null, conditions: [], properties: {} },
|
|
]);
|
|
|
|
const editorUser = await utils.createUser({
|
|
email: 'editor@test.com',
|
|
firstname: 'Editor',
|
|
lastname: 'User',
|
|
isActive: true,
|
|
roles: [editorRole.id],
|
|
});
|
|
editorUserId = editorUser.id;
|
|
rqEditor = await createAuthRequest({
|
|
strapi,
|
|
userInfo: {
|
|
email: 'editor@test.com',
|
|
firstname: 'Editor',
|
|
lastname: 'User',
|
|
password: 'Password123',
|
|
},
|
|
});
|
|
|
|
await deleteAllAdminTokens();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
nowSpy.mockRestore();
|
|
if (editorUserId !== undefined)
|
|
await strapi.db.query('admin::user').delete({ where: { id: editorUserId } });
|
|
if (saOtherUserId !== undefined)
|
|
await strapi.db.query('admin::user').delete({ where: { id: saOtherUserId } });
|
|
if (editorRoleId !== undefined)
|
|
await strapi.db.query('admin::role').delete({ where: { id: editorRoleId } });
|
|
await strapi.destroy();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await deleteAllAdminTokens();
|
|
});
|
|
|
|
let currentTokens = 0;
|
|
const expectAdminTokenOwnerDto = (owner: Record<string, unknown>) => {
|
|
expect(owner).toBeDefined();
|
|
expect(owner.id).toEqual(expect.any(Number));
|
|
expect(owner.email).toEqual(expect.any(String));
|
|
expect(owner.firstname).toEqual(expect.any(String));
|
|
expect(owner.lastname).toEqual(expect.any(String));
|
|
expect(owner.isActive).toBeUndefined();
|
|
expect(owner.blocked).toBeUndefined();
|
|
expect(owner.roles).toBeUndefined();
|
|
expect(owner.password).toBeUndefined();
|
|
expect(owner.resetPasswordToken).toBeUndefined();
|
|
expect(owner.registrationToken).toBeUndefined();
|
|
};
|
|
|
|
const createValidSuperAdminAdminToken = async (token = {}) => {
|
|
currentTokens += 1;
|
|
|
|
const body = {
|
|
name: `admin_token_${String(currentTokens)}`,
|
|
description: 'generic description',
|
|
...token,
|
|
};
|
|
|
|
const req = await rq({
|
|
url: '/admin/admin-tokens',
|
|
method: 'POST',
|
|
body,
|
|
});
|
|
|
|
expect(req.status).toEqual(201);
|
|
return req.body.data;
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Create
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('POST /admin/admin-tokens', () => {
|
|
test('Creates an admin token with expected shape', async () => {
|
|
const body = {
|
|
name: 'admin-token_tests-create',
|
|
description: 'admin-token_tests-description',
|
|
};
|
|
|
|
const res = await rq({
|
|
url: '/admin/admin-tokens',
|
|
method: 'POST',
|
|
body,
|
|
});
|
|
|
|
expect(res.statusCode).toBe(201);
|
|
expect(res.body.data).toMatchObject({
|
|
name: body.name,
|
|
description: body.description,
|
|
kind: 'admin',
|
|
adminPermissions: [],
|
|
accessKey: expect.any(String),
|
|
id: expect.any(Number),
|
|
});
|
|
expect(res.body.data.adminUserOwner).toBeDefined();
|
|
expectAdminTokenOwnerDto(res.body.data.adminUserOwner);
|
|
});
|
|
|
|
test('Creates an admin token without description (defaults to empty string)', async () => {
|
|
const body = {
|
|
name: 'admin-token_tests-no-description',
|
|
};
|
|
|
|
const res = await rq({
|
|
url: '/admin/admin-tokens',
|
|
method: 'POST',
|
|
body,
|
|
});
|
|
|
|
expect(res.statusCode).toBe(201);
|
|
expect(res.body.data.description).toBe('');
|
|
});
|
|
|
|
test('Creates an admin token with trimmed name and description', async () => {
|
|
const body = {
|
|
name: ' admin-token_tests-trimmed ',
|
|
description: ' trimmed description ',
|
|
};
|
|
|
|
const res = await rq({
|
|
url: '/admin/admin-tokens',
|
|
method: 'POST',
|
|
body,
|
|
});
|
|
|
|
expect(res.statusCode).toBe(201);
|
|
expect(res.body.data.name).toBe('admin-token_tests-trimmed');
|
|
expect(res.body.data.description).toBe('trimmed description');
|
|
});
|
|
|
|
test('Creates a token with a 7-day lifespan', async () => {
|
|
const lifespan = constants.API_TOKEN_LIFESPANS.DAYS_7;
|
|
const body = {
|
|
name: 'admin-token_tests-lifespan7',
|
|
lifespan,
|
|
};
|
|
|
|
const res = await rq({
|
|
url: '/admin/admin-tokens',
|
|
method: 'POST',
|
|
body,
|
|
});
|
|
|
|
expect(res.statusCode).toBe(201);
|
|
expect(res.body.data.lifespan).toBe(String(lifespan));
|
|
// @ts-expect-error - Add `expect.toBeISODate()` to jest types
|
|
expect(res.body.data.expiresAt).toEqual(expect.toBeISODate());
|
|
expect(Date.parse(res.body.data.expiresAt)).toBeGreaterThan(now + lifespan - 2000);
|
|
expect(Date.parse(res.body.data.expiresAt)).toBeLessThan(now + lifespan + 2000);
|
|
});
|
|
|
|
test('Creates a token with a 30-day lifespan', async () => {
|
|
const lifespan = constants.API_TOKEN_LIFESPANS.DAYS_30;
|
|
const body = {
|
|
name: 'admin-token_tests-lifespan30',
|
|
lifespan,
|
|
};
|
|
|
|
const res = await rq({
|
|
url: '/admin/admin-tokens',
|
|
method: 'POST',
|
|
body,
|
|
});
|
|
|
|
expect(res.statusCode).toBe(201);
|
|
expect(res.body.data.lifespan).toBe(String(lifespan));
|
|
expect(Date.parse(res.body.data.expiresAt)).toBeGreaterThan(now + lifespan - 2000);
|
|
expect(Date.parse(res.body.data.expiresAt)).toBeLessThan(now + lifespan + 2000);
|
|
});
|
|
|
|
test('Creates a token with a 90-day lifespan', async () => {
|
|
const lifespan = constants.API_TOKEN_LIFESPANS.DAYS_90;
|
|
const body = {
|
|
name: 'admin-token_tests-lifespan90',
|
|
lifespan,
|
|
};
|
|
|
|
const res = await rq({
|
|
url: '/admin/admin-tokens',
|
|
method: 'POST',
|
|
body,
|
|
});
|
|
|
|
expect(res.statusCode).toBe(201);
|
|
expect(res.body.data.lifespan).toBe(String(lifespan));
|
|
expect(Date.parse(res.body.data.expiresAt)).toBeGreaterThan(now + lifespan - 2000);
|
|
expect(Date.parse(res.body.data.expiresAt)).toBeLessThan(now + lifespan + 2000);
|
|
});
|
|
|
|
test('Creates a token with null lifespan → expiresAt is null', async () => {
|
|
const body = {
|
|
name: 'admin-token_tests-null-lifespan',
|
|
lifespan: null,
|
|
};
|
|
|
|
const res = await rq({
|
|
url: '/admin/admin-tokens',
|
|
method: 'POST',
|
|
body,
|
|
});
|
|
|
|
expect(res.statusCode).toBe(201);
|
|
expect(res.body.data.expiresAt).toBeNull();
|
|
expect(res.body.data.lifespan).toBeNull();
|
|
});
|
|
|
|
test('Fails to create a token with invalid lifespan → 400', async () => {
|
|
const body = {
|
|
name: 'admin-token_tests-bad-lifespan',
|
|
lifespan: -1,
|
|
};
|
|
|
|
const res = await rq({
|
|
url: '/admin/admin-tokens',
|
|
method: 'POST',
|
|
body,
|
|
});
|
|
|
|
expect(res.statusCode).toBe(400);
|
|
});
|
|
|
|
test('Fails to create a token with duplicate name → 400 Name already taken', async () => {
|
|
await createValidSuperAdminAdminToken({ name: 'admin-token_tests-duplicate' });
|
|
|
|
const res = await rq({
|
|
url: '/admin/admin-tokens',
|
|
method: 'POST',
|
|
body: { name: 'admin-token_tests-duplicate' },
|
|
});
|
|
|
|
expect(res.statusCode).toBe(400);
|
|
expect(res.body.error.message).toBe('Name already taken');
|
|
});
|
|
|
|
test('Fails to create a token with `type` set → 400', async () => {
|
|
const body = {
|
|
name: 'admin-token_tests-with-type',
|
|
type: 'read-only',
|
|
};
|
|
|
|
const res = await rq({
|
|
url: '/admin/admin-tokens',
|
|
method: 'POST',
|
|
body,
|
|
});
|
|
|
|
expect(res.statusCode).toBe(400);
|
|
});
|
|
|
|
test('Fails to create a token with `permissions` set → 400', async () => {
|
|
const body = {
|
|
name: 'admin-token_tests-with-permissions',
|
|
permissions: ['api::foo.foo.find'],
|
|
};
|
|
|
|
const res = await rq({
|
|
url: '/admin/admin-tokens',
|
|
method: 'POST',
|
|
body,
|
|
});
|
|
|
|
expect(res.statusCode).toBe(400);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// List
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('GET /admin/admin-tokens', () => {
|
|
test('Lists admin tokens — no accessKey in any entry', async () => {
|
|
await createValidSuperAdminAdminToken();
|
|
await createValidSuperAdminAdminToken();
|
|
|
|
const res = await rq({
|
|
url: '/admin/admin-tokens',
|
|
method: 'GET',
|
|
});
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(Array.isArray(res.body.data)).toBe(true);
|
|
expect(res.body.data.length).toBe(2);
|
|
|
|
for (const token of res.body.data) {
|
|
expect(token.accessKey).toBeUndefined();
|
|
expect(token.kind).toBe('admin');
|
|
expectAdminTokenOwnerDto(token.adminUserOwner);
|
|
}
|
|
});
|
|
|
|
test('Super admin sees all admin tokens regardless of owner', async () => {
|
|
await createValidSuperAdminAdminToken({ name: 'admin-token_list-sa-a' });
|
|
|
|
const resOther = await rqOther({
|
|
url: '/admin/admin-tokens',
|
|
method: 'POST',
|
|
body: { name: 'admin-token_list-sa-b' },
|
|
});
|
|
expect(resOther.statusCode).toBe(201);
|
|
|
|
const res = await rq({
|
|
url: '/admin/admin-tokens',
|
|
method: 'GET',
|
|
});
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
const names = res.body.data.map((t: { name: string }) => t.name);
|
|
expect(names).toContain('admin-token_list-sa-a');
|
|
expect(names).toContain('admin-token_list-sa-b');
|
|
});
|
|
|
|
test('Bearer admin token — super admin lists all tokens (owner user has roles for isSuperAdmin)', async () => {
|
|
await createValidSuperAdminAdminToken({ name: 'admin-token_bearer-list-sa-a' });
|
|
|
|
// Token ability is scoped to adminPermissions; include read so hasPermissions allows the route.
|
|
// Listing another super-admin's token still requires isSuperAdmin(ctx.state.user) in api-token list().
|
|
const createOtherRes = await rqOther({
|
|
url: '/admin/admin-tokens',
|
|
method: 'POST',
|
|
body: {
|
|
name: 'admin-token_bearer-list-sa-b',
|
|
adminPermissions: [
|
|
{ action: 'admin::admin-tokens.read', subject: null, conditions: [], properties: {} },
|
|
],
|
|
},
|
|
});
|
|
expect(createOtherRes.statusCode).toBe(201);
|
|
const accessKey: string = createOtherRes.body.data.accessKey;
|
|
|
|
const bearerRq = createAgent(strapi, { token: accessKey });
|
|
|
|
const res = await bearerRq({
|
|
url: '/admin/admin-tokens',
|
|
method: 'GET',
|
|
});
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
const names = res.body.data.map((t: { name: string }) => t.name);
|
|
expect(names).toContain('admin-token_bearer-list-sa-a');
|
|
expect(names).toContain('admin-token_bearer-list-sa-b');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Get
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('GET /admin/admin-tokens/:id', () => {
|
|
test('Owner gets token with accessKey', async () => {
|
|
const token = await createValidSuperAdminAdminToken();
|
|
|
|
const res = await rq({
|
|
url: `/admin/admin-tokens/${token.id}`,
|
|
method: 'GET',
|
|
});
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.body.data.accessKey).toEqual(expect.any(String));
|
|
expect(res.body.data.id).toBe(token.id);
|
|
expectAdminTokenOwnerDto(res.body.data.adminUserOwner);
|
|
});
|
|
|
|
test('Different super admin (non-owner) gets token without accessKey', async () => {
|
|
const token = await createValidSuperAdminAdminToken();
|
|
|
|
const res = await rqOther({
|
|
url: `/admin/admin-tokens/${token.id}`,
|
|
method: 'GET',
|
|
});
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.body.data.accessKey).toBeUndefined();
|
|
expect(res.body.data.id).toBe(token.id);
|
|
expectAdminTokenOwnerDto(res.body.data.adminUserOwner);
|
|
});
|
|
|
|
test('Editor with read permission gets 404 for another user token by id', async () => {
|
|
const token = await createValidSuperAdminAdminToken({
|
|
name: 'admin-token_get-editor-forbidden',
|
|
});
|
|
|
|
const res = await rqEditor({
|
|
url: `/admin/admin-tokens/${token.id}`,
|
|
method: 'GET',
|
|
});
|
|
|
|
expect(res.statusCode).toBe(404);
|
|
});
|
|
|
|
test('Returns 404 for missing id', async () => {
|
|
const res = await rq({
|
|
url: '/admin/admin-tokens/999999',
|
|
method: 'GET',
|
|
});
|
|
|
|
expect(res.statusCode).toBe(404);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Revoke
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('DELETE /admin/admin-tokens/:id', () => {
|
|
test('Revokes a token → 200, returns deleted token without accessKey', async () => {
|
|
const token = await createValidSuperAdminAdminToken();
|
|
|
|
const res = await rq({
|
|
url: `/admin/admin-tokens/${token.id}`,
|
|
method: 'DELETE',
|
|
});
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.body.data).not.toBeNull();
|
|
expect(res.body.data.id).toBe(token.id);
|
|
expect(res.body.data.accessKey).toBeUndefined();
|
|
expectAdminTokenOwnerDto(res.body.data.adminUserOwner);
|
|
});
|
|
|
|
test('Non-owner super admin can revoke → 200', async () => {
|
|
const token = await createValidSuperAdminAdminToken({
|
|
name: 'admin-token_tests-delete-by-other-sa',
|
|
});
|
|
|
|
const res = await rqOther({
|
|
url: `/admin/admin-tokens/${token.id}`,
|
|
method: 'DELETE',
|
|
});
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.body.data).not.toBeNull();
|
|
expect(res.body.data.id).toBe(token.id);
|
|
expectAdminTokenOwnerDto(res.body.data.adminUserOwner);
|
|
});
|
|
|
|
test('Returns 404 for missing id', async () => {
|
|
const res = await rq({
|
|
url: '/admin/admin-tokens/999999',
|
|
method: 'DELETE',
|
|
});
|
|
|
|
expect(res.statusCode).toBe(404);
|
|
});
|
|
|
|
test('Revoking an admin token with permissions deletes the permission rows', async () => {
|
|
const createRes = await rq({
|
|
url: '/admin/admin-tokens',
|
|
method: 'POST',
|
|
body: {
|
|
name: 'admin-token_tests-revoke-permissions',
|
|
adminPermissions: [{ action: EDITOR_ACTION }],
|
|
},
|
|
});
|
|
expect(createRes.statusCode).toBe(201);
|
|
const tokenId = createRes.body.data.id;
|
|
const permissionIds = createRes.body.data.adminPermissions.map((p: { id: number }) => p.id);
|
|
expect(permissionIds.length).toBeGreaterThan(0);
|
|
|
|
const deleteRes = await rq({
|
|
url: `/admin/admin-tokens/${tokenId}`,
|
|
method: 'DELETE',
|
|
});
|
|
expect(deleteRes.statusCode).toBe(200);
|
|
|
|
const orphans = await strapi.db.query('admin::permission').findMany({
|
|
where: { id: { $in: permissionIds } },
|
|
});
|
|
expect(orphans).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Update
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('PUT /admin/admin-tokens/:id', () => {
|
|
test('Owner updates name/description → 200', async () => {
|
|
const token = await createValidSuperAdminAdminToken();
|
|
|
|
const res = await rq({
|
|
url: `/admin/admin-tokens/${token.id}`,
|
|
method: 'PUT',
|
|
body: {
|
|
name: 'admin-token_tests-updated-name',
|
|
description: 'updated description',
|
|
adminPermissions: [],
|
|
},
|
|
});
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.body.data.name).toBe('admin-token_tests-updated-name');
|
|
expect(res.body.data.description).toBe('updated description');
|
|
expectAdminTokenOwnerDto(res.body.data.adminUserOwner);
|
|
});
|
|
|
|
test('Non-owner super admin can update → 200', async () => {
|
|
const token = await createValidSuperAdminAdminToken();
|
|
|
|
const res = await rqOther({
|
|
url: `/admin/admin-tokens/${token.id}`,
|
|
method: 'PUT',
|
|
body: {
|
|
name: 'admin-token_tests-updated-by-other',
|
|
adminPermissions: [],
|
|
},
|
|
});
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.body.data.name).toBe('admin-token_tests-updated-by-other');
|
|
expectAdminTokenOwnerDto(res.body.data.adminUserOwner);
|
|
});
|
|
|
|
test('Partial body (description only) keeps other fields', async () => {
|
|
const token = await createValidSuperAdminAdminToken({
|
|
name: 'admin-token_tests-partial-update',
|
|
});
|
|
|
|
const res = await rq({
|
|
url: `/admin/admin-tokens/${token.id}`,
|
|
method: 'PUT',
|
|
body: {
|
|
description: 'only description updated',
|
|
adminPermissions: [],
|
|
},
|
|
});
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.body.data.name).toBe('admin-token_tests-partial-update');
|
|
expect(res.body.data.description).toBe('only description updated');
|
|
expectAdminTokenOwnerDto(res.body.data.adminUserOwner);
|
|
});
|
|
|
|
test('Returns 404 for missing id', async () => {
|
|
const res = await rq({
|
|
url: '/admin/admin-tokens/999999',
|
|
method: 'PUT',
|
|
body: { name: 'admin-token_tests-404', adminPermissions: [] },
|
|
});
|
|
|
|
expect(res.statusCode).toBe(404);
|
|
});
|
|
|
|
test('Invalid name (empty string) → 400', async () => {
|
|
const token = await createValidSuperAdminAdminToken();
|
|
|
|
const res = await rq({
|
|
url: `/admin/admin-tokens/${token.id}`,
|
|
method: 'PUT',
|
|
body: { name: '', adminPermissions: [] },
|
|
});
|
|
|
|
expect(res.statusCode).toBe(400);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Regenerate
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('POST /admin/admin-tokens/:id/regenerate', () => {
|
|
test('Owner regenerates → 201, new accessKey differs from original', async () => {
|
|
const token = await createValidSuperAdminAdminToken();
|
|
|
|
const res = await rq({
|
|
url: `/admin/admin-tokens/${token.id}/regenerate`,
|
|
method: 'POST',
|
|
});
|
|
|
|
expect(res.statusCode).toBe(201);
|
|
expect(res.body.data.accessKey).toEqual(expect.any(String));
|
|
expect(res.body.data.accessKey).not.toBe(token.accessKey);
|
|
});
|
|
|
|
test('Non-owner (different super admin) → 403', async () => {
|
|
const token = await createValidSuperAdminAdminToken();
|
|
|
|
const res = await rqOther({
|
|
url: `/admin/admin-tokens/${token.id}/regenerate`,
|
|
method: 'POST',
|
|
});
|
|
|
|
expect(res.statusCode).toBe(403);
|
|
});
|
|
|
|
test('Returns 404 for missing id', async () => {
|
|
const res = await rq({
|
|
url: '/admin/admin-tokens/999999/regenerate',
|
|
method: 'POST',
|
|
});
|
|
|
|
expect(res.statusCode).toBe(404);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// getOwnerPermissions
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('GET /admin/admin-tokens/:id/owner-permissions', () => {
|
|
test('Owner can read → 200, returns owner effective permissions array', async () => {
|
|
const token = await createValidSuperAdminAdminToken();
|
|
|
|
const res = await rq({
|
|
url: `/admin/admin-tokens/${token.id}/owner-permissions`,
|
|
method: 'GET',
|
|
});
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(Array.isArray(res.body.data)).toBe(true);
|
|
});
|
|
|
|
test('Non-owner super admin can read → 200', async () => {
|
|
const token = await createValidSuperAdminAdminToken();
|
|
|
|
const res = await rqOther({
|
|
url: `/admin/admin-tokens/${token.id}/owner-permissions`,
|
|
method: 'GET',
|
|
});
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(Array.isArray(res.body.data)).toBe(true);
|
|
});
|
|
|
|
test('Returns 404 for missing id', async () => {
|
|
const res = await rq({
|
|
url: '/admin/admin-tokens/999999/owner-permissions',
|
|
method: 'GET',
|
|
});
|
|
|
|
expect(res.statusCode).toBe(404);
|
|
});
|
|
});
|
|
|
|
describe('B — 403 for non-owner non-super-admin', () => {
|
|
test('PUT /admin/admin-tokens/:id — rqEditor (not owner) → 403', async () => {
|
|
const token = await createValidSuperAdminAdminToken({ name: 'admin-token_b-update' });
|
|
|
|
const res = await rqEditor({
|
|
url: `/admin/admin-tokens/${token.id}`,
|
|
method: 'PUT',
|
|
body: { name: 'admin-token_b-update-new', adminPermissions: [] },
|
|
});
|
|
|
|
expect(res.statusCode).toBe(403);
|
|
});
|
|
|
|
test('GET /admin/admin-tokens/:id/owner-permissions — rqEditor (not owner) → 403', async () => {
|
|
const token = await createValidSuperAdminAdminToken({ name: 'admin-token_b-owner-perms' });
|
|
|
|
const res = await rqEditor({
|
|
url: `/admin/admin-tokens/${token.id}/owner-permissions`,
|
|
method: 'GET',
|
|
});
|
|
|
|
expect(res.statusCode).toBe(403);
|
|
});
|
|
|
|
test('DELETE /admin/admin-tokens/:id — rqEditor (not owner) → 403', async () => {
|
|
const token = await createValidSuperAdminAdminToken({ name: 'admin-token_b-delete' });
|
|
|
|
const res = await rqEditor({
|
|
url: `/admin/admin-tokens/${token.id}`,
|
|
method: 'DELETE',
|
|
});
|
|
|
|
expect(res.statusCode).toBe(403);
|
|
});
|
|
});
|
|
|
|
describe('C — List ownership filter', () => {
|
|
test('Non-super-admin only sees their own tokens', async () => {
|
|
// rq (super-admin) creates one token
|
|
await rq({ url: '/admin/admin-tokens', method: 'POST', body: { name: 'admin-token_c-sa' } });
|
|
|
|
// rqEditor creates their own token
|
|
const editorRes = await rqEditor({
|
|
url: '/admin/admin-tokens',
|
|
method: 'POST',
|
|
body: { name: 'admin-token_c-editor' },
|
|
});
|
|
expect(editorRes.statusCode).toBe(201);
|
|
|
|
const listRes = await rqEditor({ url: '/admin/admin-tokens', method: 'GET' });
|
|
|
|
expect(listRes.statusCode).toBe(200);
|
|
const names = listRes.body.data.map((t: { name: string }) => t.name);
|
|
expect(names).toContain('admin-token_c-editor');
|
|
expect(names).not.toContain('admin-token_c-sa');
|
|
});
|
|
|
|
test('Super-admin list returns all tokens from all owners', async () => {
|
|
await rq({
|
|
url: '/admin/admin-tokens',
|
|
method: 'POST',
|
|
body: { name: 'admin-token_c-all-sa' },
|
|
});
|
|
const editorRes = await rqEditor({
|
|
url: '/admin/admin-tokens',
|
|
method: 'POST',
|
|
body: { name: 'admin-token_c-all-editor' },
|
|
});
|
|
expect(editorRes.statusCode).toBe(201);
|
|
|
|
const listRes = await rq({ url: '/admin/admin-tokens', method: 'GET' });
|
|
expect(listRes.statusCode).toBe(200);
|
|
const names = listRes.body.data.map((t: { name: string }) => t.name);
|
|
expect(names).toContain('admin-token_c-all-sa');
|
|
expect(names).toContain('admin-token_c-all-editor');
|
|
});
|
|
});
|
|
|
|
describe('D — Permission ceiling on create', () => {
|
|
test('Editor creates token with a permission they hold → 201, permission persisted', async () => {
|
|
const res = await rqEditor({
|
|
url: '/admin/admin-tokens',
|
|
method: 'POST',
|
|
body: {
|
|
name: 'admin-token_d-within-ceiling',
|
|
adminPermissions: [
|
|
{ action: EDITOR_ACTION, subject: null, conditions: [], properties: {} },
|
|
],
|
|
},
|
|
});
|
|
|
|
expect(res.statusCode).toBe(201);
|
|
const persisted = res.body.data.adminPermissions;
|
|
expect(Array.isArray(persisted)).toBe(true);
|
|
expect(persisted.some((p: { action: string }) => p.action === EDITOR_ACTION)).toBe(true);
|
|
});
|
|
|
|
test('Editor creates token with a permission they do NOT hold → 400 ValidationError', async () => {
|
|
const res = await rqEditor({
|
|
url: '/admin/admin-tokens',
|
|
method: 'POST',
|
|
body: {
|
|
name: 'admin-token_d-exceeds-ceiling',
|
|
adminPermissions: [
|
|
{ action: UNGRANTED_ACTION, subject: null, conditions: [], properties: {} },
|
|
],
|
|
},
|
|
});
|
|
|
|
expect(res.statusCode).toBe(400);
|
|
expect(res.body.error.name).toBe('ValidationError');
|
|
});
|
|
|
|
test('Editor creates token with an unknown action → 400', async () => {
|
|
const res = await rqEditor({
|
|
url: '/admin/admin-tokens',
|
|
method: 'POST',
|
|
body: {
|
|
name: 'admin-token_d-unknown-action',
|
|
adminPermissions: [
|
|
{ action: 'admin::nonexistent.action', subject: null, conditions: [], properties: {} },
|
|
],
|
|
},
|
|
});
|
|
|
|
expect(res.statusCode).toBe(400);
|
|
});
|
|
|
|
test('Editor creates token; conditions are inherited from their role (not caller-supplied)', async () => {
|
|
// Assign the action with a condition to the role, then verify round-trip strips supplied conditions
|
|
const res = await rqEditor({
|
|
url: '/admin/admin-tokens',
|
|
method: 'POST',
|
|
body: {
|
|
name: 'admin-token_d-condition-inherit',
|
|
// Caller tries to supply no conditions — ceiling should enforce role's conditions
|
|
adminPermissions: [
|
|
{ action: EDITOR_ACTION, subject: null, conditions: [], properties: {} },
|
|
],
|
|
},
|
|
});
|
|
|
|
expect(res.statusCode).toBe(201);
|
|
// Conditions on the token must match what the role has (empty since we assigned with no conditions)
|
|
const persisted = res.body.data.adminPermissions;
|
|
const perm = persisted.find((p: { action: string }) => p.action === EDITOR_ACTION);
|
|
expect(perm).toBeDefined();
|
|
// Role was assigned with empty conditions → token should also have empty conditions
|
|
expect(perm.conditions).toStrictEqual([]);
|
|
});
|
|
|
|
test('Super-admin can create token with any permission (bypasses ceiling) → 201', async () => {
|
|
const res = await rq({
|
|
url: '/admin/admin-tokens',
|
|
method: 'POST',
|
|
body: {
|
|
name: 'admin-token_d-sa-bypass',
|
|
adminPermissions: [
|
|
{ action: UNGRANTED_ACTION, subject: null, conditions: [], properties: {} },
|
|
],
|
|
},
|
|
});
|
|
|
|
expect(res.statusCode).toBe(201);
|
|
});
|
|
|
|
test('Super-admin: unregistered condition is stripped, not persisted → 201 with empty conditions', async () => {
|
|
const res = await rq({
|
|
url: '/admin/admin-tokens',
|
|
method: 'POST',
|
|
body: {
|
|
name: 'admin-token_d-sa-bogus-condition',
|
|
adminPermissions: [
|
|
{
|
|
action: EDITOR_ACTION,
|
|
subject: null,
|
|
conditions: ['plugin::unknown.bogus-condition'],
|
|
properties: {},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
expect(res.statusCode).toBe(201);
|
|
const perm = res.body.data.adminPermissions.find(
|
|
(p: { action: string }) => p.action === EDITOR_ACTION
|
|
);
|
|
expect(perm).toBeDefined();
|
|
expect(perm.conditions).toStrictEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('G — Owner immutability', () => {
|
|
test('Update with different adminUserOwner → 400 ValidationError', async () => {
|
|
const token = await createValidSuperAdminAdminToken({
|
|
name: 'admin-token_g-owner-immutable',
|
|
});
|
|
|
|
const res = await rq({
|
|
url: `/admin/admin-tokens/${token.id}`,
|
|
method: 'PUT',
|
|
body: {
|
|
name: 'admin-token_g-owner-immutable',
|
|
adminPermissions: [],
|
|
adminUserOwner: editorUserId,
|
|
},
|
|
});
|
|
|
|
expect(res.statusCode).toBe(400);
|
|
});
|
|
});
|
|
|
|
describe('I — User lifecycle', () => {
|
|
test('Deleting a user removes their admin tokens', async () => {
|
|
const utils = createUtils(strapi);
|
|
|
|
const editorRole = await strapi.db
|
|
.query('admin::role')
|
|
.findOne({ where: { name: 'token-ceiling-test-role' } });
|
|
|
|
const tempUser = await utils.createUser({
|
|
email: 'temp-deletable@test.com',
|
|
firstname: 'Temp',
|
|
lastname: 'Delete',
|
|
isActive: true,
|
|
roles: [editorRole.id],
|
|
});
|
|
|
|
const tempRq = await createAuthRequest({
|
|
strapi,
|
|
userInfo: {
|
|
email: 'temp-deletable@test.com',
|
|
firstname: 'Temp',
|
|
lastname: 'Delete',
|
|
password: 'Password123',
|
|
},
|
|
});
|
|
|
|
const createRes = await tempRq({
|
|
url: '/admin/admin-tokens',
|
|
method: 'POST',
|
|
body: { name: 'admin-token_i-lifecycle' },
|
|
});
|
|
expect(createRes.statusCode).toBe(201);
|
|
const tokenId = createRes.body.data.id;
|
|
|
|
await utils.deleteUserById(tempUser.id);
|
|
|
|
const token = await strapi.db.query('admin::api-token').findOne({ where: { id: tokenId } });
|
|
expect(token).toBeNull();
|
|
});
|
|
|
|
test('Blocked owner — admin token is rejected (401)', async () => {
|
|
const utils = createUtils(strapi);
|
|
|
|
const editorRole = await strapi.db
|
|
.query('admin::role')
|
|
.findOne({ where: { name: 'token-ceiling-test-role' } });
|
|
|
|
const blockedUser = await utils.createUser({
|
|
email: 'temp-blocked@test.com',
|
|
firstname: 'Blocked',
|
|
lastname: 'User',
|
|
isActive: true,
|
|
roles: [editorRole.id],
|
|
});
|
|
|
|
const blockedRq = await createAuthRequest({
|
|
strapi,
|
|
userInfo: {
|
|
email: 'temp-blocked@test.com',
|
|
firstname: 'Blocked',
|
|
lastname: 'User',
|
|
password: 'Password123',
|
|
},
|
|
});
|
|
|
|
// Create an admin token while still active — store the raw accessKey
|
|
const createRes = await blockedRq({
|
|
url: '/admin/admin-tokens',
|
|
method: 'POST',
|
|
body: { name: 'admin-token_i-blocked' },
|
|
});
|
|
expect(createRes.statusCode).toBe(201);
|
|
const accessKey: string = createRes.body.data.accessKey;
|
|
|
|
// Block the user
|
|
await strapi.db.query('admin::user').update({
|
|
where: { id: blockedUser.id },
|
|
data: { isActive: false },
|
|
});
|
|
|
|
// A request authenticated with the admin token should now be rejected
|
|
const bearerRq = createAgent(strapi, { token: accessKey });
|
|
|
|
const res = await bearerRq({
|
|
url: '/admin/admin-tokens',
|
|
method: 'GET',
|
|
});
|
|
|
|
expect(res.statusCode).toBe(401);
|
|
|
|
// Cleanup
|
|
await utils.deleteUserById(blockedUser.id);
|
|
});
|
|
});
|
|
});
|