mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-26 13:48:33 +02:00
feat: Separate api token roles (#4019)
## What As part of the move to enable custom-root-roles, our permissions model was found to not be granular enough to allow service accounts to only be allowed to create read-only tokens (client, frontend), but not be allowed to create admin tokens to avoid opening up a path for privilege escalation. ## How This PR adds 12 new roles, a CRUD set for each of the three token types (admin, client, frontend). To access the `/api/admin/api-tokens` endpoints you will still need the existing permission (CREATE_API_TOKEN, DELETE_API_TOKEN, READ_API_TOKEN, UPDATE_API_TOKEN). Once this PR has been merged the token type you're modifying will also be checked, so if you're trying to create a CLIENT api-token, you will need `CREATE_API_TOKEN` and `CREATE_CLIENT_API_TOKEN` permissions. If the user performing the create call does not have these two permissions or the `ADMIN` permission, the creation will be rejected with a `403 - FORBIDDEN` status. ### Discussion points The test suite tests all operations using a token with operation_CLIENT_API_TOKEN permission and verifies that it fails trying to do any of the operations against FRONTEND and ADMIN tokens. During development the operation_FRONTEND_API_TOKEN and operation_ADMIN_API_TOKEN permission has also been tested in the same way. I wonder if it's worth it to re-add these tests in order to verify that the permission checker works for all operations, or if this is enough. Since we're running them using e2e tests, I've removed them for now, to avoid hogging too much processing time.
This commit is contained in:
parent
fa081e9014
commit
3acb116ab2
@ -3,10 +3,22 @@ import { Response } from 'express';
|
||||
import Controller from '../controller';
|
||||
import {
|
||||
ADMIN,
|
||||
CREATE_ADMIN_API_TOKEN,
|
||||
CREATE_API_TOKEN,
|
||||
CREATE_CLIENT_API_TOKEN,
|
||||
CREATE_FRONTEND_API_TOKEN,
|
||||
DELETE_ADMIN_API_TOKEN,
|
||||
DELETE_API_TOKEN,
|
||||
DELETE_CLIENT_API_TOKEN,
|
||||
DELETE_FRONTEND_API_TOKEN,
|
||||
READ_ADMIN_API_TOKEN,
|
||||
READ_API_TOKEN,
|
||||
READ_CLIENT_API_TOKEN,
|
||||
READ_FRONTEND_API_TOKEN,
|
||||
UPDATE_ADMIN_API_TOKEN,
|
||||
UPDATE_API_TOKEN,
|
||||
UPDATE_CLIENT_API_TOKEN,
|
||||
UPDATE_FRONTEND_API_TOKEN,
|
||||
} from '../../types/permissions';
|
||||
import { ApiTokenService } from '../../services/api-token-service';
|
||||
import { Logger } from '../../logger';
|
||||
@ -36,10 +48,85 @@ import { UpdateApiTokenSchema } from '../../openapi/spec/update-api-token-schema
|
||||
import { emptyResponse } from '../../openapi/util/standard-responses';
|
||||
import { ProxyService } from '../../services/proxy-service';
|
||||
import { extractUsername } from '../../util';
|
||||
import { OperationDeniedError } from '../../error';
|
||||
|
||||
interface TokenParam {
|
||||
token: string;
|
||||
}
|
||||
const tokenTypeToCreatePermission: (tokenType: ApiTokenType) => string = (
|
||||
tokenType,
|
||||
) => {
|
||||
switch (tokenType) {
|
||||
case ApiTokenType.ADMIN:
|
||||
return CREATE_ADMIN_API_TOKEN;
|
||||
case ApiTokenType.CLIENT:
|
||||
return CREATE_CLIENT_API_TOKEN;
|
||||
case ApiTokenType.FRONTEND:
|
||||
return CREATE_FRONTEND_API_TOKEN;
|
||||
}
|
||||
};
|
||||
|
||||
const permissionToTokenType: (
|
||||
permission: string,
|
||||
) => ApiTokenType | undefined = (permission) => {
|
||||
if (
|
||||
[
|
||||
CREATE_FRONTEND_API_TOKEN,
|
||||
READ_FRONTEND_API_TOKEN,
|
||||
DELETE_FRONTEND_API_TOKEN,
|
||||
UPDATE_FRONTEND_API_TOKEN,
|
||||
].includes(permission)
|
||||
) {
|
||||
return ApiTokenType.FRONTEND;
|
||||
} else if (
|
||||
[
|
||||
CREATE_CLIENT_API_TOKEN,
|
||||
READ_CLIENT_API_TOKEN,
|
||||
DELETE_CLIENT_API_TOKEN,
|
||||
UPDATE_CLIENT_API_TOKEN,
|
||||
].includes(permission)
|
||||
) {
|
||||
return ApiTokenType.CLIENT;
|
||||
} else if (
|
||||
[
|
||||
READ_ADMIN_API_TOKEN,
|
||||
CREATE_ADMIN_API_TOKEN,
|
||||
DELETE_ADMIN_API_TOKEN,
|
||||
UPDATE_ADMIN_API_TOKEN,
|
||||
].includes(permission)
|
||||
) {
|
||||
return ApiTokenType.ADMIN;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const tokenTypeToUpdatePermission: (tokenType: ApiTokenType) => string = (
|
||||
tokenType,
|
||||
) => {
|
||||
switch (tokenType) {
|
||||
case ApiTokenType.ADMIN:
|
||||
return UPDATE_ADMIN_API_TOKEN;
|
||||
case ApiTokenType.CLIENT:
|
||||
return UPDATE_CLIENT_API_TOKEN;
|
||||
case ApiTokenType.FRONTEND:
|
||||
return UPDATE_FRONTEND_API_TOKEN;
|
||||
}
|
||||
};
|
||||
|
||||
const tokenTypeToDeletePermission: (tokenType: ApiTokenType) => string = (
|
||||
tokenType,
|
||||
) => {
|
||||
switch (tokenType) {
|
||||
case ApiTokenType.ADMIN:
|
||||
return DELETE_ADMIN_API_TOKEN;
|
||||
case ApiTokenType.CLIENT:
|
||||
return DELETE_CLIENT_API_TOKEN;
|
||||
case ApiTokenType.FRONTEND:
|
||||
return DELETE_FRONTEND_API_TOKEN;
|
||||
}
|
||||
};
|
||||
|
||||
export class ApiTokenController extends Controller {
|
||||
private apiTokenService: ApiTokenService;
|
||||
|
||||
@ -160,17 +247,30 @@ export class ApiTokenController extends Controller {
|
||||
res: Response<ApiTokenSchema>,
|
||||
): Promise<any> {
|
||||
const createToken = await createApiToken.validateAsync(req.body);
|
||||
const token = await this.apiTokenService.createApiToken(
|
||||
createToken,
|
||||
extractUsername(req),
|
||||
const permissionRequired = tokenTypeToCreatePermission(
|
||||
createToken.type,
|
||||
);
|
||||
this.openApiService.respondWithValidation(
|
||||
201,
|
||||
res,
|
||||
apiTokenSchema.$id,
|
||||
serializeDates(token),
|
||||
{ location: `api-tokens` },
|
||||
const hasPermission = await this.accessService.hasPermission(
|
||||
req.user,
|
||||
permissionRequired,
|
||||
);
|
||||
if (hasPermission) {
|
||||
const token = await this.apiTokenService.createApiToken(
|
||||
createToken,
|
||||
extractUsername(req),
|
||||
);
|
||||
this.openApiService.respondWithValidation(
|
||||
201,
|
||||
res,
|
||||
apiTokenSchema.$id,
|
||||
serializeDates(token),
|
||||
{ location: `api-tokens` },
|
||||
);
|
||||
} else {
|
||||
throw new OperationDeniedError(
|
||||
`You don't have the necessary access [${permissionRequired}] to perform this operation`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async updateApiToken(
|
||||
@ -184,12 +284,33 @@ export class ApiTokenController extends Controller {
|
||||
this.logger.error(req.body);
|
||||
return res.status(400).send();
|
||||
}
|
||||
let tokenToUpdate;
|
||||
try {
|
||||
tokenToUpdate = await this.apiTokenService.getToken(token);
|
||||
} catch (error) {}
|
||||
if (!tokenToUpdate) {
|
||||
res.status(200).end();
|
||||
return;
|
||||
}
|
||||
const permissionRequired = tokenTypeToUpdatePermission(
|
||||
tokenToUpdate.type,
|
||||
);
|
||||
const hasPermission = await this.accessService.hasPermission(
|
||||
req.user,
|
||||
permissionRequired,
|
||||
);
|
||||
if (!hasPermission) {
|
||||
throw new OperationDeniedError(
|
||||
`You do not have the required access [${permissionRequired}] to perform this operation`,
|
||||
);
|
||||
}
|
||||
|
||||
await this.apiTokenService.updateExpiry(
|
||||
token,
|
||||
new Date(expiresAt),
|
||||
extractUsername(req),
|
||||
);
|
||||
|
||||
return res.status(200).end();
|
||||
}
|
||||
|
||||
@ -198,7 +319,26 @@ export class ApiTokenController extends Controller {
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { token } = req.params;
|
||||
|
||||
let tokenToUpdate;
|
||||
try {
|
||||
tokenToUpdate = await this.apiTokenService.getToken(token);
|
||||
} catch (error) {}
|
||||
if (!tokenToUpdate) {
|
||||
res.status(200).end();
|
||||
return;
|
||||
}
|
||||
const permissionRequired = tokenTypeToDeletePermission(
|
||||
tokenToUpdate.type,
|
||||
);
|
||||
let hasPermission = await this.accessService.hasPermission(
|
||||
req.user,
|
||||
permissionRequired,
|
||||
);
|
||||
if (!hasPermission) {
|
||||
throw new OperationDeniedError(
|
||||
`You do not have the required access [${permissionRequired}] to perform this operation`,
|
||||
);
|
||||
}
|
||||
await this.apiTokenService.delete(token, extractUsername(req));
|
||||
await this.proxyService.deleteClientForProxyToken(token);
|
||||
res.status(200).end();
|
||||
@ -210,11 +350,21 @@ export class ApiTokenController extends Controller {
|
||||
if (user.isAPI && user.permissions.includes(ADMIN)) {
|
||||
return allTokens;
|
||||
}
|
||||
|
||||
if (await this.accessService.hasPermission(user, UPDATE_API_TOKEN)) {
|
||||
return allTokens;
|
||||
}
|
||||
|
||||
return allTokens.filter((token) => token.type !== ApiTokenType.ADMIN);
|
||||
const userPermissions = await this.accessService.getPermissionsForUser(
|
||||
user,
|
||||
);
|
||||
let allowedTokenTypes = [
|
||||
READ_ADMIN_API_TOKEN,
|
||||
READ_CLIENT_API_TOKEN,
|
||||
READ_FRONTEND_API_TOKEN,
|
||||
]
|
||||
.filter((readPerm) =>
|
||||
userPermissions.some((p) => p.permission === readPerm),
|
||||
)
|
||||
.map(permissionToTokenType)
|
||||
.filter((t) => t);
|
||||
return allTokens.filter((token) =>
|
||||
allowedTokenTypes.includes(token.type),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -130,7 +130,6 @@ export class AccessService {
|
||||
|
||||
try {
|
||||
const userP = await this.getPermissionsForUser(user);
|
||||
|
||||
return userP
|
||||
.filter(
|
||||
(p) =>
|
||||
|
@ -88,6 +88,10 @@ export class ApiTokenService {
|
||||
}
|
||||
}
|
||||
|
||||
async getToken(secret: string): Promise<IApiToken> {
|
||||
return this.store.get(secret);
|
||||
}
|
||||
|
||||
async updateLastSeen(): Promise<void> {
|
||||
if (this.lastSeenSecrets.size > 0) {
|
||||
const toStore = [...this.lastSeenSecrets];
|
||||
|
@ -30,6 +30,22 @@ export const UPDATE_API_TOKEN = 'UPDATE_API_TOKEN';
|
||||
export const CREATE_API_TOKEN = 'CREATE_API_TOKEN';
|
||||
export const DELETE_API_TOKEN = 'DELETE_API_TOKEN';
|
||||
export const READ_API_TOKEN = 'READ_API_TOKEN';
|
||||
|
||||
export const UPDATE_ADMIN_API_TOKEN = 'UPDATE_ADMIN_API_TOKEN';
|
||||
export const CREATE_ADMIN_API_TOKEN = 'CREATE_ADMIN_API_TOKEN';
|
||||
export const DELETE_ADMIN_API_TOKEN = 'DELETE_ADMIN_API_TOKEN';
|
||||
export const READ_ADMIN_API_TOKEN = 'READ_ADMIN_API_TOKEN';
|
||||
|
||||
export const UPDATE_CLIENT_API_TOKEN = 'UPDATE_CLIENT_API_TOKEN';
|
||||
export const CREATE_CLIENT_API_TOKEN = 'CREATE_CLIENT_API_TOKEN';
|
||||
export const DELETE_CLIENT_API_TOKEN = 'DELETE_CLIENT_API_TOKEN';
|
||||
export const READ_CLIENT_API_TOKEN = 'READ_CLIENT_API_TOKEN';
|
||||
|
||||
export const UPDATE_FRONTEND_API_TOKEN = 'UPDATE_FRONTEND_API_TOKEN';
|
||||
export const CREATE_FRONTEND_API_TOKEN = 'CREATE_FRONTEND_API_TOKEN';
|
||||
export const DELETE_FRONTEND_API_TOKEN = 'DELETE_FRONTEND_API_TOKEN';
|
||||
export const READ_FRONTEND_API_TOKEN = 'READ_FRONTEND_API_TOKEN';
|
||||
|
||||
export const UPDATE_TAG_TYPE = 'UPDATE_TAG_TYPE';
|
||||
export const DELETE_TAG_TYPE = 'DELETE_TAG_TYPE';
|
||||
export const UPDATE_FEATURE_VARIANTS = 'UPDATE_FEATURE_VARIANTS';
|
||||
|
@ -0,0 +1,31 @@
|
||||
exports.up = function (db, cb) {
|
||||
db.runSql(
|
||||
`
|
||||
INSERT INTO permissions(permission, display_name, type) VALUES
|
||||
('CREATE_ADMIN_API_TOKEN', 'Allowed to create new ADMIN tokens', 'root'),
|
||||
('UPDATE_ADMIN_API_TOKEN', 'Allowed to update ADMIN tokens', 'root'),
|
||||
('DELETE_ADMIN_API_TOKEN', 'Allowed to delete ADMIN tokens', 'root'),
|
||||
('READ_ADMIN_API_TOKEN', 'Allowed to read ADMIN tokens', 'root'),
|
||||
('CREATE_CLIENT_API_TOKEN', 'Allowed to create new CLIENT tokens', 'root'),
|
||||
('UPDATE_CLIENT_API_TOKEN', 'Allowed to update CLIENT tokens', 'root'),
|
||||
('DELETE_CLIENT_API_TOKEN', 'Allowed to delete CLIENT tokens', 'root'),
|
||||
('READ_CLIENT_API_TOKEN', 'Allowed to read CLIENT tokens', 'root'),
|
||||
('CREATE_FRONTEND_API_TOKEN', 'Allowed to create new FRONTEND tokens', 'root'),
|
||||
('UPDATE_FRONTEND_API_TOKEN', 'Allowed to update FRONTEND tokens', 'root'),
|
||||
('DELETE_FRONTEND_API_TOKEN', 'Allowed to delete FRONTEND tokens', 'root'),
|
||||
('READ_FRONTEND_API_TOKEN', 'Allowed to read FRONTEND tokens', 'root');
|
||||
`,
|
||||
cb,
|
||||
);
|
||||
};
|
||||
|
||||
exports.down = function (db, cb) {
|
||||
db.runSql(
|
||||
`
|
||||
DELETE FROM permissions WHERE permission IN ('CREATE_ADMIN_API_TOKEN', 'UPDATE_ADMIN_API_TOKEN', 'DELETE_ADMIN_API_TOKEN', 'READ_ADMIN_API_TOKEN');
|
||||
DELETE FROM permissions WHERE permission IN ('CREATE_CLIENT_API_TOKEN', 'UPDATE_CLIENT_API_TOKEN', 'DELETE_CLIENT_API_TOKEN', 'READ_CLIENT_API_TOKEN');
|
||||
DELETE FROM permissions WHERE permission IN ('CREATE_FRONTEND_API_TOKEN', 'UPDATE_FRONTEND_API_TOKEN', 'DELETE_FRONTEND_API_TOKEN', 'READ_FRONTEND_API_TOKEN');
|
||||
`,
|
||||
cb,
|
||||
);
|
||||
};
|
@ -0,0 +1,27 @@
|
||||
exports.up = function (db, cb) {
|
||||
db.runSql(
|
||||
`
|
||||
CREATE OR REPLACE FUNCTION assign_unleash_permission_to_role(permission_name text, role_name text) returns void as
|
||||
$$
|
||||
declare role_id int;
|
||||
permission_id int;
|
||||
BEGIN
|
||||
role_id := (SELECT id FROM roles WHERE name = role_name);
|
||||
permission_id := (SELECT p.id FROM permissions p WHERE p.permission = permission_name);
|
||||
INSERT INTO role_permission(role_id, permission_id) VALUES (role_id, permission_id);
|
||||
END
|
||||
$$ language plpgsql;
|
||||
|
||||
SELECT assign_unleash_permission_to_role('READ_CLIENT_API_TOKEN', 'Editor');
|
||||
SELECT assign_unleash_permission_to_role('READ_FRONTEND_API_TOKEN', 'Editor');
|
||||
|
||||
`,
|
||||
cb,
|
||||
);
|
||||
};
|
||||
exports.down = function (db, cb) {
|
||||
db.runSql(
|
||||
`DROP FUNCTION IF EXISTS assign_unleash_permission_to_role(text, text)`,
|
||||
cb,
|
||||
);
|
||||
};
|
@ -3,6 +3,19 @@ import dbInit from '../../helpers/database-init';
|
||||
import getLogger from '../../../fixtures/no-logger';
|
||||
import { ApiTokenType } from '../../../../lib/types/models/api-token';
|
||||
import { RoleName } from '../../../../lib/types/model';
|
||||
import {
|
||||
CREATE_API_TOKEN,
|
||||
CREATE_CLIENT_API_TOKEN,
|
||||
DELETE_API_TOKEN,
|
||||
DELETE_CLIENT_API_TOKEN,
|
||||
READ_ADMIN_API_TOKEN,
|
||||
READ_API_TOKEN,
|
||||
READ_CLIENT_API_TOKEN,
|
||||
READ_FRONTEND_API_TOKEN,
|
||||
UPDATE_API_TOKEN,
|
||||
UPDATE_CLIENT_API_TOKEN,
|
||||
} from '../../../../lib/types';
|
||||
import { addDays } from 'date-fns';
|
||||
|
||||
let stores;
|
||||
let db;
|
||||
@ -22,8 +35,8 @@ afterEach(async () => {
|
||||
await stores.apiTokenStore.deleteAll();
|
||||
});
|
||||
|
||||
test('editor users should only get client tokens', async () => {
|
||||
expect.assertions(2);
|
||||
test('editor users should only get client or frontend tokens', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const preHook = (app, config, { userService, accessService }) => {
|
||||
app.use('/api/admin/', async (req, res, next) => {
|
||||
@ -45,6 +58,12 @@ test('editor users should only get client tokens', async () => {
|
||||
type: ApiTokenType.CLIENT,
|
||||
});
|
||||
|
||||
await stores.apiTokenStore.insert({
|
||||
username: 'frontend',
|
||||
secret: '12345',
|
||||
type: ApiTokenType.FRONTEND,
|
||||
});
|
||||
|
||||
await stores.apiTokenStore.insert({
|
||||
username: 'test',
|
||||
secret: 'sdfsdf2d',
|
||||
@ -56,8 +75,9 @@ test('editor users should only get client tokens', async () => {
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body.tokens.length).toBe(1);
|
||||
expect(res.body.tokens.length).toBe(2);
|
||||
expect(res.body.tokens[0].type).toBe(ApiTokenType.CLIENT);
|
||||
expect(res.body.tokens[1].type).toBe(ApiTokenType.FRONTEND);
|
||||
});
|
||||
|
||||
await destroy();
|
||||
@ -155,3 +175,759 @@ test('Token-admin should be allowed to create token', async () => {
|
||||
|
||||
await destroy();
|
||||
});
|
||||
|
||||
describe('Fine grained API token permissions', () => {
|
||||
describe('A role with access to CREATE_CLIENT_API_TOKEN', () => {
|
||||
test('should be allowed to create client tokens', async () => {
|
||||
const preHook = (app, config, { userService, accessService }) => {
|
||||
app.use('/api/admin/', async (req, res, next) => {
|
||||
const role = await accessService.getRootRole(
|
||||
RoleName.VIEWER,
|
||||
);
|
||||
const user = await userService.createUser({
|
||||
email: 'mylittlepony_viewer@example.com',
|
||||
rootRole: role.id,
|
||||
});
|
||||
req.user = user;
|
||||
const createClientApiTokenRole =
|
||||
await accessService.createRole({
|
||||
name: 'client_token_creator',
|
||||
description: 'Can create client tokens',
|
||||
permissions: [],
|
||||
type: 'root-custom',
|
||||
});
|
||||
await accessService.addPermissionToRole(
|
||||
role.id,
|
||||
CREATE_API_TOKEN,
|
||||
);
|
||||
await accessService.addPermissionToRole(
|
||||
role.id,
|
||||
CREATE_CLIENT_API_TOKEN,
|
||||
);
|
||||
await accessService.addUserToRole(
|
||||
user.id,
|
||||
createClientApiTokenRole.id,
|
||||
'default',
|
||||
);
|
||||
next();
|
||||
});
|
||||
};
|
||||
const { request, destroy } = await setupAppWithCustomAuth(
|
||||
stores,
|
||||
preHook,
|
||||
{
|
||||
experimental: {
|
||||
flags: {
|
||||
customRootRoles: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
await request
|
||||
.post('/api/admin/api-tokens')
|
||||
.send({
|
||||
username: 'default-client',
|
||||
type: 'client',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(201);
|
||||
await destroy();
|
||||
});
|
||||
test('should NOT be allowed to create frontend tokens', async () => {
|
||||
const preHook = (app, config, { userService, accessService }) => {
|
||||
app.use('/api/admin/', async (req, res, next) => {
|
||||
const role = await accessService.getRootRole(
|
||||
RoleName.VIEWER,
|
||||
);
|
||||
const user = await userService.createUser({
|
||||
email: 'mylittlepony_viewer_frontend@example.com',
|
||||
rootRole: role.id,
|
||||
});
|
||||
req.user = user;
|
||||
const createClientApiTokenRole =
|
||||
await accessService.createRole({
|
||||
name: 'client_token_creator_cannot_create_frontend',
|
||||
description: 'Can create client tokens',
|
||||
permissions: [],
|
||||
type: 'root-custom',
|
||||
});
|
||||
await accessService.addPermissionToRole(
|
||||
role.id,
|
||||
CREATE_API_TOKEN,
|
||||
);
|
||||
await accessService.addPermissionToRole(
|
||||
role.id,
|
||||
CREATE_CLIENT_API_TOKEN,
|
||||
);
|
||||
await accessService.addUserToRole(
|
||||
user.id,
|
||||
createClientApiTokenRole.id,
|
||||
'default',
|
||||
);
|
||||
next();
|
||||
});
|
||||
};
|
||||
const { request, destroy } = await setupAppWithCustomAuth(
|
||||
stores,
|
||||
preHook,
|
||||
{
|
||||
experimental: {
|
||||
flags: {
|
||||
customRootRoles: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
await request
|
||||
.post('/api/admin/api-tokens')
|
||||
.send({
|
||||
username: 'default-frontend',
|
||||
type: 'frontend',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(403);
|
||||
await destroy();
|
||||
});
|
||||
test('should NOT be allowed to create ADMIN tokens', async () => {
|
||||
const preHook = (app, config, { userService, accessService }) => {
|
||||
app.use('/api/admin/', async (req, res, next) => {
|
||||
const role = await accessService.getRootRole(
|
||||
RoleName.VIEWER,
|
||||
);
|
||||
const user = await userService.createUser({
|
||||
email: 'mylittlepony_admin@example.com',
|
||||
rootRole: role.id,
|
||||
});
|
||||
req.user = user;
|
||||
const createClientApiTokenRole =
|
||||
await accessService.createRole({
|
||||
name: 'client_token_creator_cannot_create_admin',
|
||||
description: 'Can create client tokens',
|
||||
permissions: [],
|
||||
type: 'root-custom',
|
||||
});
|
||||
await accessService.addPermissionToRole(
|
||||
role.id,
|
||||
CREATE_API_TOKEN,
|
||||
);
|
||||
await accessService.addPermissionToRole(
|
||||
role.id,
|
||||
CREATE_CLIENT_API_TOKEN,
|
||||
);
|
||||
await accessService.addUserToRole(
|
||||
user.id,
|
||||
createClientApiTokenRole.id,
|
||||
'default',
|
||||
);
|
||||
next();
|
||||
});
|
||||
};
|
||||
const { request, destroy } = await setupAppWithCustomAuth(
|
||||
stores,
|
||||
preHook,
|
||||
{
|
||||
experimental: {
|
||||
flags: {
|
||||
customRootRoles: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
await request
|
||||
.post('/api/admin/api-tokens')
|
||||
.send({
|
||||
username: 'default-admin',
|
||||
type: 'admin',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(403);
|
||||
await destroy();
|
||||
});
|
||||
});
|
||||
describe('Read operations', () => {
|
||||
test('READ_FRONTEND_API_TOKEN should be able to see FRONTEND tokens', async () => {
|
||||
const preHook = (app, config, { userService, accessService }) => {
|
||||
app.use('/api/admin/', async (req, res, next) => {
|
||||
const role = await accessService.getRootRole(
|
||||
RoleName.VIEWER,
|
||||
);
|
||||
const user = await userService.createUser({
|
||||
email: 'read_frontend_token@example.com',
|
||||
rootRole: role.id,
|
||||
});
|
||||
req.user = user;
|
||||
const readFrontendApiToken = await accessService.createRole(
|
||||
{
|
||||
name: 'frontend_token_reader',
|
||||
description: 'Can read frontend tokens',
|
||||
permissions: [],
|
||||
type: 'root-custom',
|
||||
},
|
||||
);
|
||||
await accessService.addPermissionToRole(
|
||||
readFrontendApiToken.id,
|
||||
READ_API_TOKEN,
|
||||
);
|
||||
await accessService.addPermissionToRole(
|
||||
readFrontendApiToken.id,
|
||||
READ_FRONTEND_API_TOKEN,
|
||||
);
|
||||
await accessService.addUserToRole(
|
||||
user.id,
|
||||
readFrontendApiToken.id,
|
||||
'default',
|
||||
);
|
||||
next();
|
||||
});
|
||||
};
|
||||
const { request, destroy } = await setupAppWithCustomAuth(
|
||||
stores,
|
||||
preHook,
|
||||
{
|
||||
experimental: {
|
||||
flags: {
|
||||
customRootRoles: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
await stores.apiTokenStore.insert({
|
||||
username: 'client',
|
||||
secret: 'client_secret',
|
||||
type: ApiTokenType.CLIENT,
|
||||
});
|
||||
|
||||
await stores.apiTokenStore.insert({
|
||||
username: 'admin',
|
||||
secret: 'sdfsdf2admin_secret',
|
||||
type: ApiTokenType.ADMIN,
|
||||
});
|
||||
await stores.apiTokenStore.insert({
|
||||
username: 'frontender',
|
||||
secret: 'sdfsdf2dfrontend_Secret',
|
||||
type: ApiTokenType.FRONTEND,
|
||||
});
|
||||
await request
|
||||
.get('/api/admin/api-tokens')
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(
|
||||
res.body.tokens.every(
|
||||
(t) => t.type === ApiTokenType.FRONTEND,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
await destroy();
|
||||
});
|
||||
test('READ_CLIENT_API_TOKEN should be able to see CLIENT tokens', async () => {
|
||||
const preHook = (app, config, { userService, accessService }) => {
|
||||
app.use('/api/admin/', async (req, res, next) => {
|
||||
const role = await accessService.getRootRole(
|
||||
RoleName.VIEWER,
|
||||
);
|
||||
const user = await userService.createUser({
|
||||
email: 'read_client_token@example.com',
|
||||
rootRole: role.id,
|
||||
});
|
||||
req.user = user;
|
||||
const readClientTokenRole = await accessService.createRole({
|
||||
name: 'client_token_reader',
|
||||
description: 'Can read client tokens',
|
||||
permissions: [],
|
||||
type: 'root-custom',
|
||||
});
|
||||
await accessService.addPermissionToRole(
|
||||
readClientTokenRole.id,
|
||||
READ_API_TOKEN,
|
||||
);
|
||||
await accessService.addPermissionToRole(
|
||||
readClientTokenRole.id,
|
||||
READ_CLIENT_API_TOKEN,
|
||||
);
|
||||
await accessService.addUserToRole(
|
||||
user.id,
|
||||
readClientTokenRole.id,
|
||||
'default',
|
||||
);
|
||||
next();
|
||||
});
|
||||
};
|
||||
const { request, destroy } = await setupAppWithCustomAuth(
|
||||
stores,
|
||||
preHook,
|
||||
{
|
||||
experimental: {
|
||||
flags: {
|
||||
customRootRoles: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
await stores.apiTokenStore.insert({
|
||||
username: 'client',
|
||||
secret: 'client_secret_1234',
|
||||
type: ApiTokenType.CLIENT,
|
||||
});
|
||||
|
||||
await stores.apiTokenStore.insert({
|
||||
username: 'admin',
|
||||
secret: 'admin_secret_1234',
|
||||
type: ApiTokenType.ADMIN,
|
||||
});
|
||||
await stores.apiTokenStore.insert({
|
||||
username: 'frontender',
|
||||
secret: 'frontend_secret_1234',
|
||||
type: ApiTokenType.FRONTEND,
|
||||
});
|
||||
await request
|
||||
.get('/api/admin/api-tokens')
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body.tokens).toHaveLength(1);
|
||||
expect(res.body.tokens[0].type).toBe(ApiTokenType.CLIENT);
|
||||
});
|
||||
await destroy();
|
||||
});
|
||||
test('READ_ADMIN_API_TOKEN should be able to see ADMIN tokens', async () => {
|
||||
const preHook = (app, config, { userService, accessService }) => {
|
||||
app.use('/api/admin/', async (req, res, next) => {
|
||||
const role = await accessService.getRootRole(
|
||||
RoleName.VIEWER,
|
||||
);
|
||||
const user = await userService.createUser({
|
||||
email: 'read_admin_token@example.com',
|
||||
rootRole: role.id,
|
||||
});
|
||||
req.user = user;
|
||||
const readAdminApiToken = await accessService.createRole({
|
||||
name: 'admin_token_reader',
|
||||
description: 'Can read admin tokens',
|
||||
permissions: [],
|
||||
type: 'root-custom',
|
||||
});
|
||||
await accessService.addPermissionToRole(
|
||||
readAdminApiToken.id,
|
||||
READ_API_TOKEN,
|
||||
);
|
||||
await accessService.addPermissionToRole(
|
||||
readAdminApiToken.id,
|
||||
READ_ADMIN_API_TOKEN,
|
||||
);
|
||||
await accessService.addUserToRole(
|
||||
user.id,
|
||||
readAdminApiToken.id,
|
||||
'default',
|
||||
);
|
||||
next();
|
||||
});
|
||||
};
|
||||
const { request, destroy } = await setupAppWithCustomAuth(
|
||||
stores,
|
||||
preHook,
|
||||
{
|
||||
experimental: {
|
||||
flags: {
|
||||
customRootRoles: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
await stores.apiTokenStore.insert({
|
||||
username: 'client',
|
||||
secret: 'client_secret_4321',
|
||||
type: ApiTokenType.CLIENT,
|
||||
});
|
||||
|
||||
await stores.apiTokenStore.insert({
|
||||
username: 'admin',
|
||||
secret: 'admin_secret_4321',
|
||||
type: ApiTokenType.ADMIN,
|
||||
});
|
||||
await stores.apiTokenStore.insert({
|
||||
username: 'frontender',
|
||||
secret: 'frontend_secret_4321',
|
||||
type: ApiTokenType.FRONTEND,
|
||||
});
|
||||
await request
|
||||
.get('/api/admin/api-tokens')
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body.tokens).toHaveLength(1);
|
||||
expect(res.body.tokens[0].type).toBe(ApiTokenType.ADMIN);
|
||||
});
|
||||
await destroy();
|
||||
});
|
||||
});
|
||||
describe('Update operations', () => {
|
||||
describe('UPDATE_CLIENT_API_TOKEN can', () => {
|
||||
test('UPDATE client_api token expiry', async () => {
|
||||
const preHook = (
|
||||
app,
|
||||
config,
|
||||
{ userService, accessService },
|
||||
) => {
|
||||
app.use('/api/admin/', async (req, res, next) => {
|
||||
const role = await accessService.getRootRole(
|
||||
RoleName.VIEWER,
|
||||
);
|
||||
const user = await userService.createUser({
|
||||
email: 'update_client_token@example.com',
|
||||
rootRole: role.id,
|
||||
});
|
||||
req.user = user;
|
||||
const updateClientApiExpiry =
|
||||
await accessService.createRole({
|
||||
name: 'update_client_token',
|
||||
description: 'Can update client tokens',
|
||||
permissions: [],
|
||||
type: 'root-custom',
|
||||
});
|
||||
await accessService.addPermissionToRole(
|
||||
updateClientApiExpiry.id,
|
||||
UPDATE_API_TOKEN,
|
||||
);
|
||||
await accessService.addPermissionToRole(
|
||||
updateClientApiExpiry.id,
|
||||
UPDATE_CLIENT_API_TOKEN,
|
||||
);
|
||||
await accessService.addUserToRole(
|
||||
user.id,
|
||||
updateClientApiExpiry.id,
|
||||
'default',
|
||||
);
|
||||
next();
|
||||
});
|
||||
};
|
||||
const { request, destroy } = await setupAppWithCustomAuth(
|
||||
stores,
|
||||
preHook,
|
||||
{
|
||||
experimental: {
|
||||
flags: {
|
||||
customRootRoles: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
const token = await stores.apiTokenStore.insert({
|
||||
username: 'cilent',
|
||||
secret: 'update_client_token',
|
||||
type: ApiTokenType.CLIENT,
|
||||
});
|
||||
await request
|
||||
.put(`/api/admin/api-tokens/${token.secret}`)
|
||||
.send({ expiresAt: addDays(new Date(), 14) })
|
||||
.expect(200);
|
||||
await destroy();
|
||||
});
|
||||
test('NOT UPDATE frontend_api token expiry', async () => {
|
||||
const preHook = (
|
||||
app,
|
||||
config,
|
||||
{ userService, accessService },
|
||||
) => {
|
||||
app.use('/api/admin/', async (req, res, next) => {
|
||||
const role = await accessService.getRootRole(
|
||||
RoleName.VIEWER,
|
||||
);
|
||||
const user = await userService.createUser({
|
||||
email: 'update_frontend_token@example.com',
|
||||
rootRole: role.id,
|
||||
});
|
||||
req.user = user;
|
||||
const updateClientApiExpiry =
|
||||
await accessService.createRole({
|
||||
name: 'update_client_token_not_frontend',
|
||||
description: 'Can not update frontend tokens',
|
||||
permissions: [],
|
||||
type: 'root-custom',
|
||||
});
|
||||
await accessService.addPermissionToRole(
|
||||
updateClientApiExpiry.id,
|
||||
UPDATE_API_TOKEN,
|
||||
);
|
||||
await accessService.addPermissionToRole(
|
||||
updateClientApiExpiry.id,
|
||||
UPDATE_CLIENT_API_TOKEN,
|
||||
);
|
||||
await accessService.addUserToRole(
|
||||
user.id,
|
||||
updateClientApiExpiry.id,
|
||||
'default',
|
||||
);
|
||||
next();
|
||||
});
|
||||
};
|
||||
const { request, destroy } = await setupAppWithCustomAuth(
|
||||
stores,
|
||||
preHook,
|
||||
{
|
||||
experimental: {
|
||||
flags: {
|
||||
customRootRoles: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
const token = await stores.apiTokenStore.insert({
|
||||
username: 'frontend',
|
||||
secret: 'update_frontend_token',
|
||||
type: ApiTokenType.FRONTEND,
|
||||
});
|
||||
await request
|
||||
.put(`/api/admin/api-tokens/${token.secret}`)
|
||||
.send({ expiresAt: addDays(new Date(), 14) })
|
||||
.expect(403);
|
||||
|
||||
await destroy();
|
||||
});
|
||||
test('NOT UPDATE admin_api token expiry', async () => {
|
||||
const preHook = (
|
||||
app,
|
||||
config,
|
||||
{ userService, accessService },
|
||||
) => {
|
||||
app.use('/api/admin/', async (req, res, next) => {
|
||||
const role = await accessService.getRootRole(
|
||||
RoleName.VIEWER,
|
||||
);
|
||||
const user = await userService.createUser({
|
||||
email: 'update_admin_token@example.com',
|
||||
rootRole: role.id,
|
||||
});
|
||||
req.user = user;
|
||||
const updateClientApiExpiry =
|
||||
await accessService.createRole({
|
||||
name: 'update_client_token_not_admin',
|
||||
description: 'Can not update admin tokens',
|
||||
permissions: [],
|
||||
type: 'root-custom',
|
||||
});
|
||||
await accessService.addPermissionToRole(
|
||||
updateClientApiExpiry.id,
|
||||
UPDATE_API_TOKEN,
|
||||
);
|
||||
await accessService.addPermissionToRole(
|
||||
updateClientApiExpiry.id,
|
||||
UPDATE_CLIENT_API_TOKEN,
|
||||
);
|
||||
await accessService.addUserToRole(
|
||||
user.id,
|
||||
updateClientApiExpiry.id,
|
||||
'default',
|
||||
);
|
||||
next();
|
||||
});
|
||||
};
|
||||
const { request, destroy } = await setupAppWithCustomAuth(
|
||||
stores,
|
||||
preHook,
|
||||
{
|
||||
experimental: {
|
||||
flags: {
|
||||
customRootRoles: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
const token = await stores.apiTokenStore.insert({
|
||||
username: 'admin',
|
||||
secret: 'update_admin_token',
|
||||
type: ApiTokenType.ADMIN,
|
||||
});
|
||||
await request
|
||||
.put(`/api/admin/api-tokens/${token.secret}`)
|
||||
.send({ expiresAt: addDays(new Date(), 14) })
|
||||
.expect(403);
|
||||
await destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('Delete operations', () => {
|
||||
describe('DELETE_CLIENT_API_TOKEN can', () => {
|
||||
test('DELETE client_api token', async () => {
|
||||
const preHook = (
|
||||
app,
|
||||
config,
|
||||
{ userService, accessService },
|
||||
) => {
|
||||
app.use('/api/admin/', async (req, res, next) => {
|
||||
const role = await accessService.getRootRole(
|
||||
RoleName.VIEWER,
|
||||
);
|
||||
const user = await userService.createUser({
|
||||
email: 'delete_client_token@example.com',
|
||||
rootRole: role.id,
|
||||
});
|
||||
req.user = user;
|
||||
const updateClientApiExpiry =
|
||||
await accessService.createRole({
|
||||
name: 'delete_client_token',
|
||||
description: 'Can delete client tokens',
|
||||
permissions: [],
|
||||
type: 'root-custom',
|
||||
});
|
||||
await accessService.addPermissionToRole(
|
||||
updateClientApiExpiry.id,
|
||||
DELETE_API_TOKEN,
|
||||
);
|
||||
await accessService.addPermissionToRole(
|
||||
updateClientApiExpiry.id,
|
||||
DELETE_CLIENT_API_TOKEN,
|
||||
);
|
||||
await accessService.addUserToRole(
|
||||
user.id,
|
||||
updateClientApiExpiry.id,
|
||||
'default',
|
||||
);
|
||||
next();
|
||||
});
|
||||
};
|
||||
const { request, destroy } = await setupAppWithCustomAuth(
|
||||
stores,
|
||||
preHook,
|
||||
{
|
||||
experimental: {
|
||||
flags: {
|
||||
customRootRoles: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
const token = await stores.apiTokenStore.insert({
|
||||
username: 'cilent',
|
||||
secret: 'delete_client_token',
|
||||
type: ApiTokenType.CLIENT,
|
||||
});
|
||||
await request
|
||||
.delete(`/api/admin/api-tokens/${token.secret}`)
|
||||
.send({ expiresAt: addDays(new Date(), 14) })
|
||||
.expect(200);
|
||||
await destroy();
|
||||
});
|
||||
test('NOT DELETE frontend_api token', async () => {
|
||||
const preHook = (
|
||||
app,
|
||||
config,
|
||||
{ userService, accessService },
|
||||
) => {
|
||||
app.use('/api/admin/', async (req, res, next) => {
|
||||
const role = await accessService.getRootRole(
|
||||
RoleName.VIEWER,
|
||||
);
|
||||
const user = await userService.createUser({
|
||||
email: 'delete_frontend_token@example.com',
|
||||
rootRole: role.id,
|
||||
});
|
||||
req.user = user;
|
||||
const updateClientApiExpiry =
|
||||
await accessService.createRole({
|
||||
name: 'delete_client_token_not_frontend',
|
||||
description: 'Can not delete frontend tokens',
|
||||
permissions: [],
|
||||
type: 'root-custom',
|
||||
});
|
||||
await accessService.addPermissionToRole(
|
||||
updateClientApiExpiry.id,
|
||||
DELETE_API_TOKEN,
|
||||
);
|
||||
await accessService.addPermissionToRole(
|
||||
updateClientApiExpiry.id,
|
||||
DELETE_CLIENT_API_TOKEN,
|
||||
);
|
||||
await accessService.addUserToRole(
|
||||
user.id,
|
||||
updateClientApiExpiry.id,
|
||||
'default',
|
||||
);
|
||||
next();
|
||||
});
|
||||
};
|
||||
const { request, destroy } = await setupAppWithCustomAuth(
|
||||
stores,
|
||||
preHook,
|
||||
{
|
||||
experimental: {
|
||||
flags: {
|
||||
customRootRoles: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
const token = await stores.apiTokenStore.insert({
|
||||
username: 'frontend',
|
||||
secret: 'delete_frontend_token',
|
||||
type: ApiTokenType.FRONTEND,
|
||||
});
|
||||
await request
|
||||
.delete(`/api/admin/api-tokens/${token.secret}`)
|
||||
.send({ expiresAt: addDays(new Date(), 14) })
|
||||
.expect(403);
|
||||
await destroy();
|
||||
});
|
||||
test('NOT DELETE admin_api token', async () => {
|
||||
const preHook = (
|
||||
app,
|
||||
config,
|
||||
{ userService, accessService },
|
||||
) => {
|
||||
app.use('/api/admin/', async (req, res, next) => {
|
||||
const role = await accessService.getRootRole(
|
||||
RoleName.VIEWER,
|
||||
);
|
||||
const user = await userService.createUser({
|
||||
email: 'delete_admin_token@example.com',
|
||||
rootRole: role.id,
|
||||
});
|
||||
req.user = user;
|
||||
const updateClientApiExpiry =
|
||||
await accessService.createRole({
|
||||
name: 'delete_client_token_not_admin',
|
||||
description: 'Can not delete admin tokens',
|
||||
permissions: [],
|
||||
type: 'root-custom',
|
||||
});
|
||||
await accessService.addPermissionToRole(
|
||||
updateClientApiExpiry.id,
|
||||
DELETE_API_TOKEN,
|
||||
);
|
||||
await accessService.addPermissionToRole(
|
||||
updateClientApiExpiry.id,
|
||||
DELETE_CLIENT_API_TOKEN,
|
||||
);
|
||||
await accessService.addUserToRole(
|
||||
user.id,
|
||||
updateClientApiExpiry.id,
|
||||
'default',
|
||||
);
|
||||
next();
|
||||
});
|
||||
};
|
||||
const { request, destroy } = await setupAppWithCustomAuth(
|
||||
stores,
|
||||
preHook,
|
||||
{
|
||||
experimental: {
|
||||
flags: {
|
||||
customRootRoles: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
const token = await stores.apiTokenStore.insert({
|
||||
username: 'admin',
|
||||
secret: 'delete_admin_token',
|
||||
type: ApiTokenType.ADMIN,
|
||||
});
|
||||
await request
|
||||
.delete(`/api/admin/api-tokens/${token.secret}`)
|
||||
.send({ expiresAt: addDays(new Date(), 14) })
|
||||
.expect(403);
|
||||
await destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -3,6 +3,7 @@ import dbInit from '../../helpers/database-init';
|
||||
import getLogger from '../../../fixtures/no-logger';
|
||||
import { ALL, ApiTokenType } from '../../../../lib/types/models/api-token';
|
||||
import { DEFAULT_ENV } from '../../../../lib/util';
|
||||
import { addDays } from 'date-fns';
|
||||
|
||||
let db;
|
||||
let app;
|
||||
@ -107,7 +108,7 @@ test('creates new admin token with expiry', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('update admin token with expiry', async () => {
|
||||
test('update client token with expiry', async () => {
|
||||
const tokenSecret = 'random-secret-update';
|
||||
|
||||
await db.stores.apiTokenStore.insert({
|
||||
@ -394,3 +395,17 @@ test('should create token for disabled environment', async () => {
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(201);
|
||||
});
|
||||
|
||||
test('updating expiry of non existing token should yield 200', async () => {
|
||||
return app.request
|
||||
.put('/api/admin/api-tokens/randomnonexistingsecret')
|
||||
.send({ expiresAt: addDays(new Date(), 14) })
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
test('Deleting non-existing token should yield 200', async () => {
|
||||
return app.request
|
||||
.delete('/api/admin/api-tokens/random-non-existing-token')
|
||||
.expect(200);
|
||||
});
|
||||
|
@ -230,9 +230,12 @@ export async function setupAppWithAuth(
|
||||
export async function setupAppWithCustomAuth(
|
||||
stores: IUnleashStores,
|
||||
preHook: Function,
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
customOptions?: any,
|
||||
): Promise<IUnleashTest> {
|
||||
return createApp(stores, IAuthType.CUSTOM, preHook);
|
||||
return createApp(stores, IAuthType.CUSTOM, preHook, customOptions);
|
||||
}
|
||||
|
||||
export async function setupAppWithBaseUrl(
|
||||
stores: IUnleashStores,
|
||||
): Promise<IUnleashTest> {
|
||||
|
Loading…
Reference in New Issue
Block a user