1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-20 00:08:02 +01:00

feat: added killswitch for admin tokens (#5905)

Since we've now added PAT's we really do recommend switching to those,
or for enterprises, we recommend using service accounts.

Admin tokens have an obvious disadvantage in that they're not connected
to any user, so actions performed by them are harder to audit.

This PR adds a killswitch for turning it off, in preparation for
deprecating them and ultimately removing them in the future.
This commit is contained in:
Christopher Kolstad 2024-01-17 10:27:36 +01:00 committed by GitHub
parent 6a55964ce8
commit 2b1111044f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 127 additions and 3 deletions

View File

@ -11,11 +11,13 @@ import {
} from '@server/types/permissions';
import { useHasRootAccess } from 'hooks/useHasAccess';
import { SelectOption } from './TokenTypeSelector/TokenTypeSelector';
import { useUiFlag } from '../../../../hooks/useUiFlag';
export type ApiTokenFormErrorType = 'username' | 'projects';
export const useApiTokenForm = (project?: string) => {
const { environments } = useEnvironments();
const { uiConfig } = useUiConfig();
const adminTokenKillSwitch = useUiFlag('adminTokenKillSwitch');
const initialEnvironment = environments?.find((e) => e.enabled)?.name;
const hasCreateTokenPermission = useHasRootAccess(CREATE_CLIENT_API_TOKEN);
@ -40,7 +42,7 @@ export const useApiTokenForm = (project?: string) => {
CREATE_PROJECT_API_TOKEN,
project,
);
if (!project) {
if (!project && !adminTokenKillSwitch) {
apiTokenTypes.push({
key: TokenType.ADMIN,
label: TokenType.ADMIN,

View File

@ -74,6 +74,7 @@ export type UiFlags = {
enableLicense?: boolean;
newStrategyConfigurationFeedback?: boolean;
extendedUsageMetricsUI?: boolean;
adminTokenKillSwitch?: boolean;
};
export interface IVersionInfo {

View File

@ -73,6 +73,7 @@ exports[`should create default config 1`] = `
"feedbackUriPath": undefined,
"flagResolver": FlagResolver {
"experiments": {
"adminTokenKillSwitch": false,
"anonymiseEventLog": false,
"automatedActions": false,
"caseInsensitiveInOperators": false,

View File

@ -0,0 +1,104 @@
import permissions from '../../../test/fixtures/permissions';
import { createTestConfig } from '../../../test/config/test-config';
import createStores from '../../../test/fixtures/store';
import { createServices } from '../../services';
import getApp from '../../app';
import supertest from 'supertest';
import { addDays } from 'date-fns';
async function getSetup(adminTokenKillSwitchEnabled: boolean) {
const base = `/random${Math.round(Math.random() * 1000)}`;
const perms = permissions();
const config = createTestConfig({
preHook: perms.hook,
server: { baseUriPath: base },
experimental: {
flags: {
adminTokenKillSwitch: adminTokenKillSwitchEnabled,
},
},
//@ts-ignore - Just testing, so only need the isEnabled call here
});
const stores = createStores();
await stores.environmentStore.create({
name: 'development',
type: 'development',
enabled: true,
});
const services = createServices(stores, config);
const app = await getApp(config, stores, services);
return {
base,
request: supertest(app),
};
}
describe('Admin token killswitch', () => {
test('If killswitch is off we can still create admin tokens', async () => {
const setup = await getSetup(false);
return setup.request
.post(`${setup.base}/api/admin/api-tokens`)
.set('Content-Type', 'application/json')
.send({
expiresAt: addDays(new Date(), 60),
type: 'ADMIN',
tokenName: 'Non killswitched',
})
.expect(201)
.expect((res) => {
expect(res.body.secret).toBeTruthy();
});
});
test('If killswitch is on we will get an operation denied if we try to create an admin token', async () => {
const setup = await getSetup(true);
return setup.request
.post(`${setup.base}/api/admin/api-tokens`)
.set('Content-Type', 'application/json')
.send({
expiresAt: addDays(new Date(), 60),
type: 'ADMIN',
tokenName: 'Killswitched',
})
.expect(403)
.expect((res) => {
expect(res.body.message).toBe(
'Admin tokens are disabled in this instance. Use a Service account or a PAT to access admin operations instead',
);
});
});
test('If killswitch is on we can still create a client token', async () => {
const setup = await getSetup(true);
return setup.request
.post(`${setup.base}/api/admin/api-tokens`)
.set('Content-Type', 'application/json')
.send({
expiresAt: addDays(new Date(), 60),
type: 'CLIENT',
environment: 'development',
projects: ['*'],
tokenName: 'Client',
})
.expect(201)
.expect((res) => {
expect(res.body.secret).toBeTruthy();
});
});
test('If killswitch is on we can still create a frontend token', async () => {
const setup = await getSetup(true);
return setup.request
.post(`${setup.base}/api/admin/api-tokens`)
.set('Content-Type', 'application/json')
.send({
expiresAt: addDays(new Date(), 60),
type: 'FRONTEND',
environment: 'development',
projects: ['*'],
tokenName: 'Frontend',
})
.expect(201)
.expect((res) => {
expect(res.body.secret).toBeTruthy();
});
});
});

View File

@ -21,7 +21,7 @@ import { IUnleashConfig } from '../../types/option';
import { ApiTokenType, IApiToken } from '../../types/models/api-token';
import { createApiToken } from '../../schema/api-token-schema';
import { OpenApiService } from '../../services/openapi-service';
import { IUnleashServices } from '../../types';
import { IFlagResolver, IUnleashServices } from '../../types';
import { createRequestSchema } from '../../openapi/util/create-request-schema';
import {
createResponseSchema,
@ -127,6 +127,8 @@ export class ApiTokenController extends Controller {
private logger: Logger;
private flagResolver: IFlagResolver;
constructor(
config: IUnleashConfig,
{
@ -147,6 +149,7 @@ export class ApiTokenController extends Controller {
this.accessService = accessService;
this.proxyService = proxyService;
this.openApiService = openApiService;
this.flagResolver = config.flagResolver;
this.logger = config.getLogger('api-token-controller.js');
this.route({
@ -304,6 +307,14 @@ export class ApiTokenController extends Controller {
const permissionRequired = tokenTypeToCreatePermission(
createToken.type,
);
if (
createToken.type.toUpperCase() === 'ADMIN' &&
this.flagResolver.isEnabled('adminTokenKillSwitch')
) {
throw new OperationDeniedError(
`Admin tokens are disabled in this instance. Use a Service account or a PAT to access admin operations instead`,
);
}
const hasPermission = await this.accessService.hasPermission(
req.user,
permissionRequired,

View File

@ -42,7 +42,8 @@ export type IFlagKey =
| 'newStrategyConfigurationFeedback'
| 'edgeBulkMetricsKillSwitch'
| 'extendedUsageMetrics'
| 'extendedUsageMetricsUI';
| 'extendedUsageMetricsUI'
| 'adminTokenKillSwitch';
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
@ -193,6 +194,10 @@ const flags: IFlags = {
process.env.UNLEASH_EXPERIMENTAL_EXTENDED_USAGE_METRICS_UI,
false,
),
adminTokenKillSwitch: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_ADMIN_TOKEN_KILL_SWITCH,
false,
),
};
export const defaultExperimentalOptions: IExperimentalOptions = {