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:
parent
6a55964ce8
commit
2b1111044f
@ -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,
|
||||
|
@ -74,6 +74,7 @@ export type UiFlags = {
|
||||
enableLicense?: boolean;
|
||||
newStrategyConfigurationFeedback?: boolean;
|
||||
extendedUsageMetricsUI?: boolean;
|
||||
adminTokenKillSwitch?: boolean;
|
||||
};
|
||||
|
||||
export interface IVersionInfo {
|
||||
|
@ -73,6 +73,7 @@ exports[`should create default config 1`] = `
|
||||
"feedbackUriPath": undefined,
|
||||
"flagResolver": FlagResolver {
|
||||
"experiments": {
|
||||
"adminTokenKillSwitch": false,
|
||||
"anonymiseEventLog": false,
|
||||
"automatedActions": false,
|
||||
"caseInsensitiveInOperators": false,
|
||||
|
104
src/lib/routes/admin-api/api-token.test.ts
Normal file
104
src/lib/routes/admin-api/api-token.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
@ -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,
|
||||
|
@ -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 = {
|
||||
|
Loading…
Reference in New Issue
Block a user