mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01:00
Feat/api key scoping (#941)
Co-authored-by: Christopher Kolstad <chriswk@getunleash.ai>
This commit is contained in:
parent
24b057ab6d
commit
c4b697b57d
@ -4,15 +4,17 @@ import metricsHelper from '../util/metrics-helper';
|
||||
import { DB_TIME } from '../metric-events';
|
||||
import { Logger, LogProvider } from '../logger';
|
||||
import NotFoundError from '../error/notfound-error';
|
||||
import { IApiTokenStore } from '../types/stores/api-token-store';
|
||||
import {
|
||||
ApiTokenType,
|
||||
IApiToken,
|
||||
IApiTokenCreate,
|
||||
IApiTokenStore,
|
||||
} from '../types/stores/api-token-store';
|
||||
} from '../types/models/api-token';
|
||||
|
||||
const TABLE = 'api_tokens';
|
||||
|
||||
const ALL = '*';
|
||||
|
||||
interface ITokenTable {
|
||||
id: number;
|
||||
secret: string;
|
||||
@ -21,12 +23,17 @@ interface ITokenTable {
|
||||
expires_at?: Date;
|
||||
created_at: Date;
|
||||
seen_at?: Date;
|
||||
environment: string;
|
||||
project: string;
|
||||
}
|
||||
|
||||
const toRow = (newToken: IApiTokenCreate) => ({
|
||||
username: newToken.username,
|
||||
secret: newToken.secret,
|
||||
type: newToken.type,
|
||||
project: newToken.project === ALL ? undefined : newToken.project,
|
||||
environment:
|
||||
newToken.environment === ALL ? undefined : newToken.environment,
|
||||
expires_at: newToken.expiresAt,
|
||||
});
|
||||
|
||||
@ -34,6 +41,8 @@ const toToken = (row: ITokenTable): IApiToken => ({
|
||||
secret: row.secret,
|
||||
username: row.username,
|
||||
type: row.type,
|
||||
environment: row.environment ? row.environment : ALL,
|
||||
project: row.project ? row.project : ALL,
|
||||
expiresAt: row.expires_at,
|
||||
createdAt: row.created_at,
|
||||
});
|
||||
|
@ -71,7 +71,12 @@ export class ClientMetricsStore
|
||||
}
|
||||
}
|
||||
|
||||
// Insert new client metrics
|
||||
/**
|
||||
* Insert client metrics. In the future we will isolate "appName" and "environment"
|
||||
* in separate columns in the database to make it easier to query the data.
|
||||
*
|
||||
* @param metrics sent from the client SDK.
|
||||
*/
|
||||
async insert(metrics: IClientMetric): Promise<void> {
|
||||
const stopTimer = this.startTimer('insert');
|
||||
|
||||
|
@ -3,6 +3,7 @@ import getLogger from '../../test/fixtures/no-logger';
|
||||
import { CLIENT } from '../types/permissions';
|
||||
import { createTestConfig } from '../../test/config/test-config';
|
||||
import ApiUser from '../types/api-user';
|
||||
import { ALL, ApiTokenType } from '../types/models/api-token';
|
||||
|
||||
let config: any;
|
||||
|
||||
@ -55,10 +56,13 @@ test('should not add user if unknown token', async () => {
|
||||
expect(req.user).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should add user if unknown token', async () => {
|
||||
test('should add user if known token', async () => {
|
||||
const apiUser = new ApiUser({
|
||||
username: 'default',
|
||||
permissions: [CLIENT],
|
||||
project: ALL,
|
||||
environment: ALL,
|
||||
type: ApiTokenType.CLIENT,
|
||||
});
|
||||
const apiTokenService = {
|
||||
getUserForToken: jest.fn().mockReturnValue(apiUser),
|
||||
@ -71,6 +75,7 @@ test('should add user if unknown token', async () => {
|
||||
const req = {
|
||||
header: jest.fn().mockReturnValue('some-known-token'),
|
||||
user: undefined,
|
||||
path: '/api/client',
|
||||
};
|
||||
|
||||
await func(req, undefined, cb);
|
||||
@ -80,10 +85,47 @@ test('should add user if unknown token', async () => {
|
||||
expect(req.user).toBe(apiUser);
|
||||
});
|
||||
|
||||
test('should not add user if not /api/client', async () => {
|
||||
const apiUser = new ApiUser({
|
||||
username: 'default',
|
||||
permissions: [CLIENT],
|
||||
project: ALL,
|
||||
environment: ALL,
|
||||
type: ApiTokenType.CLIENT,
|
||||
});
|
||||
const apiTokenService = {
|
||||
getUserForToken: jest.fn().mockReturnValue(apiUser),
|
||||
};
|
||||
|
||||
const func = apiTokenMiddleware(config, { apiTokenService });
|
||||
|
||||
const cb = jest.fn();
|
||||
|
||||
const res = {
|
||||
sendStatus: jest.fn(),
|
||||
};
|
||||
|
||||
const req = {
|
||||
header: jest.fn().mockReturnValue('some-known-token'),
|
||||
user: undefined,
|
||||
path: '/api/admin',
|
||||
};
|
||||
|
||||
await func(req, res, cb);
|
||||
|
||||
expect(cb).not.toHaveBeenCalled();
|
||||
expect(req.header).toHaveBeenCalled();
|
||||
expect(req.user).toBeUndefined();
|
||||
expect(res.sendStatus).toHaveBeenCalledWith(403);
|
||||
});
|
||||
|
||||
test('should not add user if disabled', async () => {
|
||||
const apiUser = new ApiUser({
|
||||
username: 'default',
|
||||
permissions: [CLIENT],
|
||||
project: ALL,
|
||||
environment: ALL,
|
||||
type: ApiTokenType.CLIENT,
|
||||
});
|
||||
const apiTokenService = {
|
||||
getUserForToken: jest.fn().mockReturnValue(apiUser),
|
||||
@ -136,6 +178,7 @@ test('should call next if apiTokenService throws', async () => {
|
||||
});
|
||||
|
||||
test('should call next if apiTokenService throws x2', async () => {
|
||||
jest.spyOn(global.console, 'error').mockImplementation(() => jest.fn());
|
||||
const apiTokenService = {
|
||||
getUserForToken: () => {
|
||||
throw new Error('hi there, i am stupid');
|
||||
|
@ -1,6 +1,11 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { ApiTokenType } from '../types/models/api-token';
|
||||
import { IUnleashConfig } from '../types/option';
|
||||
|
||||
const isClientApi = ({ path }) => {
|
||||
return path && path.startsWith('/api/client');
|
||||
};
|
||||
|
||||
const apiAccessMiddleware = (
|
||||
{
|
||||
getLogger,
|
||||
@ -9,14 +14,14 @@ const apiAccessMiddleware = (
|
||||
{ apiTokenService }: any,
|
||||
): any => {
|
||||
const logger = getLogger('/middleware/api-token.ts');
|
||||
logger.info('Enabling api-token middleware');
|
||||
logger.debug('Enabling api-token middleware');
|
||||
|
||||
if (!authentication.enableApiToken) {
|
||||
return (req, res, next) => next();
|
||||
}
|
||||
|
||||
return (req, res, next) => {
|
||||
if (req.apiUser) {
|
||||
if (req.user) {
|
||||
return next();
|
||||
}
|
||||
|
||||
@ -24,6 +29,9 @@ const apiAccessMiddleware = (
|
||||
const apiToken = req.header('authorization');
|
||||
const apiUser = apiTokenService.getUserForToken(apiToken);
|
||||
if (apiUser) {
|
||||
if (apiUser.type === ApiTokenType.CLIENT && !isClientApi(req)) {
|
||||
return res.sendStatus(403);
|
||||
}
|
||||
req.user = apiUser;
|
||||
}
|
||||
} catch (error) {
|
||||
|
@ -30,7 +30,7 @@ function demoAuthentication(
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(`${basePath}/api/admin/`, (req, res, next) => {
|
||||
app.use(`${basePath}/api`, (req, res, next) => {
|
||||
// @ts-ignore
|
||||
if (req.user) {
|
||||
return next();
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Application, NextFunction, Request, Response } from 'express';
|
||||
import { Application, NextFunction, Response } from 'express';
|
||||
import { IAuthRequest } from '../routes/unleash-types';
|
||||
import AuthenticationRequired from '../types/authentication-required';
|
||||
|
||||
function ossAuthHook(app: Application, baseUriPath: string): void {
|
||||
@ -11,14 +12,11 @@ function ossAuthHook(app: Application, baseUriPath: string): void {
|
||||
|
||||
app.use(
|
||||
`${baseUriPath}/api`,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
// @ts-ignore
|
||||
async (req: IAuthRequest, res: Response, next: NextFunction) => {
|
||||
if (req.session && req.session.user) {
|
||||
// @ts-ignore
|
||||
req.user = req.session.user;
|
||||
return next();
|
||||
}
|
||||
// @ts-ignore
|
||||
if (req.user) {
|
||||
return next();
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import { createTestConfig } from '../../test/config/test-config';
|
||||
import ApiUser from '../types/api-user';
|
||||
import { IFeatureToggleStore } from '../types/stores/feature-toggle-store';
|
||||
import FakeFeatureToggleStore from '../../test/fixtures/fake-feature-toggle-store';
|
||||
import { ApiTokenType } from '../types/models/api-token';
|
||||
|
||||
let config: IUnleashConfig;
|
||||
let featureToggleStore: IFeatureToggleStore;
|
||||
@ -46,6 +47,9 @@ test('should give api-user ADMIN permission', async () => {
|
||||
user: new ApiUser({
|
||||
username: 'api',
|
||||
permissions: [perms.ADMIN],
|
||||
project: '*',
|
||||
environment: '*',
|
||||
type: ApiTokenType.ADMIN,
|
||||
}),
|
||||
};
|
||||
|
||||
@ -68,6 +72,9 @@ test('should not give api-user ADMIN permission', async () => {
|
||||
user: new ApiUser({
|
||||
username: 'api',
|
||||
permissions: [perms.CLIENT],
|
||||
project: '*',
|
||||
environment: '*',
|
||||
type: ApiTokenType.CLIENT,
|
||||
}),
|
||||
};
|
||||
|
||||
|
@ -22,8 +22,8 @@ const rbacMiddleware = (
|
||||
{ featureToggleStore }: Pick<IUnleashStores, 'featureToggleStore'>,
|
||||
accessService: PermissionChecker,
|
||||
): any => {
|
||||
const logger = config.getLogger('/middleware/rbac-middleware.js');
|
||||
logger.info('Enabling RBAC');
|
||||
const logger = config.getLogger('/middleware/rbac-middleware.ts');
|
||||
logger.debug('Enabling RBAC middleware');
|
||||
|
||||
return (req, res, next) => {
|
||||
req.checkRbac = async (permission: string) => {
|
||||
|
@ -13,7 +13,8 @@ import { AccessService } from '../../services/access-service';
|
||||
import { IAuthRequest } from '../unleash-types';
|
||||
import User from '../../types/user';
|
||||
import { IUnleashConfig } from '../../types/option';
|
||||
import { ApiTokenType } from '../../types/stores/api-token-store';
|
||||
import { ApiTokenType } from '../../types/models/api-token';
|
||||
import { createApiToken } from '../../schema/api-token-schema';
|
||||
|
||||
interface IServices {
|
||||
apiTokenService: ApiTokenService;
|
||||
@ -64,41 +65,16 @@ class ApiTokenController extends Controller {
|
||||
}
|
||||
|
||||
async createApiToken(req: IAuthRequest, res: Response): Promise<any> {
|
||||
const { username, type, expiresAt } = req.body;
|
||||
|
||||
if (!username || !type) {
|
||||
this.logger.error(req.body);
|
||||
return res.status(400).send();
|
||||
}
|
||||
|
||||
const tokenType =
|
||||
type.toLowerCase() === 'admin'
|
||||
? ApiTokenType.ADMIN
|
||||
: ApiTokenType.CLIENT;
|
||||
|
||||
try {
|
||||
const token = await this.apiTokenService.creteApiToken({
|
||||
type: tokenType,
|
||||
username,
|
||||
expiresAt,
|
||||
});
|
||||
return res.status(201).json(token);
|
||||
} catch (error) {
|
||||
this.logger.error('error creating api-token', error);
|
||||
return res.status(500);
|
||||
}
|
||||
const createToken = await createApiToken.validateAsync(req.body);
|
||||
const token = await this.apiTokenService.createApiToken(createToken);
|
||||
return res.status(201).json(token);
|
||||
}
|
||||
|
||||
async deleteApiToken(req: IAuthRequest, res: Response): Promise<void> {
|
||||
const { token } = req.params;
|
||||
|
||||
try {
|
||||
await this.apiTokenService.delete(token);
|
||||
res.status(200).end();
|
||||
} catch (error) {
|
||||
this.logger.error('error creating api-token', error);
|
||||
res.status(500);
|
||||
}
|
||||
await this.apiTokenService.delete(token);
|
||||
res.status(200).end();
|
||||
}
|
||||
|
||||
async updateApiToken(req: IAuthRequest, res: Response): Promise<any> {
|
||||
@ -110,13 +86,8 @@ class ApiTokenController extends Controller {
|
||||
return res.status(400).send();
|
||||
}
|
||||
|
||||
try {
|
||||
await this.apiTokenService.updateExpiry(token, expiresAt);
|
||||
return res.status(200).end();
|
||||
} catch (error) {
|
||||
this.logger.error('error creating api-token', error);
|
||||
return res.status(500);
|
||||
}
|
||||
await this.apiTokenService.updateExpiry(token, expiresAt);
|
||||
return res.status(200).end();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import memoizee from 'memoizee';
|
||||
import { Request, Response } from 'express';
|
||||
import { Response } from 'express';
|
||||
import Controller from '../controller';
|
||||
import { IUnleashServices } from '../../types/services';
|
||||
import { IUnleashConfig } from '../../types/option';
|
||||
@ -8,9 +8,17 @@ import { Logger } from '../../logger';
|
||||
import { querySchema } from '../../schema/feature-schema';
|
||||
import { IFeatureToggleQuery } from '../../types/model';
|
||||
import NotFoundError from '../../error/notfound-error';
|
||||
import { IAuthRequest } from '../unleash-types';
|
||||
import ApiUser from '../../types/api-user';
|
||||
import { ALL } from '../../types/models/api-token';
|
||||
|
||||
const version = 2;
|
||||
|
||||
interface QueryOverride {
|
||||
project?: string[];
|
||||
environment?: string;
|
||||
}
|
||||
|
||||
export default class FeatureController extends Controller {
|
||||
private readonly logger: Logger;
|
||||
|
||||
@ -50,20 +58,34 @@ export default class FeatureController extends Controller {
|
||||
}
|
||||
}
|
||||
|
||||
async getAll(req: Request, res: Response): Promise<void> {
|
||||
const query = await this.prepQuery(req.query);
|
||||
let features;
|
||||
if (this.cache) {
|
||||
features = await this.cachedFeatures(query);
|
||||
} else {
|
||||
features = await this.featureToggleServiceV2.getClientFeatures(
|
||||
query,
|
||||
);
|
||||
private async resolveQuery(
|
||||
req: IAuthRequest,
|
||||
): Promise<IFeatureToggleQuery> {
|
||||
const { user, query } = req;
|
||||
|
||||
const override: QueryOverride = {};
|
||||
if (user instanceof ApiUser) {
|
||||
if (user.project !== ALL) {
|
||||
override.project = [user.project];
|
||||
}
|
||||
if (user.environment !== ALL) {
|
||||
override.environment = user.environment;
|
||||
}
|
||||
}
|
||||
res.json({ version, features });
|
||||
|
||||
const q = { ...query, ...override };
|
||||
return this.prepQuery(q);
|
||||
}
|
||||
|
||||
async prepQuery({
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
private paramToArray(param: any) {
|
||||
if (!param) {
|
||||
return param;
|
||||
}
|
||||
return Array.isArray(param) ? param : [param];
|
||||
}
|
||||
|
||||
private async prepQuery({
|
||||
tag,
|
||||
project,
|
||||
namePrefix,
|
||||
@ -86,29 +108,25 @@ export default class FeatureController extends Controller {
|
||||
return query;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
paramToArray(param: any) {
|
||||
if (!param) {
|
||||
return param;
|
||||
async getAll(req: IAuthRequest, res: Response): Promise<void> {
|
||||
const featureQuery = await this.resolveQuery(req);
|
||||
let features;
|
||||
if (this.cache) {
|
||||
features = await this.cachedFeatures(featureQuery);
|
||||
} else {
|
||||
features = await this.featureToggleServiceV2.getClientFeatures(
|
||||
featureQuery,
|
||||
);
|
||||
}
|
||||
return Array.isArray(param) ? param : [param];
|
||||
res.json({ version, features, query: featureQuery });
|
||||
}
|
||||
|
||||
async getFeatureToggle(
|
||||
req: Request<
|
||||
{ featureName: string },
|
||||
any,
|
||||
any,
|
||||
{ environment?: string }
|
||||
>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
async getFeatureToggle(req: IAuthRequest, res: Response): Promise<void> {
|
||||
const name = req.params.featureName;
|
||||
const { environment } = req.query;
|
||||
const toggles = await this.featureToggleServiceV2.getClientFeatures({
|
||||
namePrefix: name,
|
||||
environment,
|
||||
});
|
||||
const featureQuery = await this.resolveQuery(req);
|
||||
const q = { ...featureQuery, namePrefix: name };
|
||||
const toggles = await this.featureToggleServiceV2.getClientFeatures(q);
|
||||
|
||||
const toggle = toggles.find((t) => t.name === name);
|
||||
if (!toggle) {
|
||||
throw new NotFoundError(`Could not find feature toggle ${name}`);
|
||||
@ -116,5 +134,3 @@ export default class FeatureController extends Controller {
|
||||
res.json(toggle).end();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FeatureController;
|
||||
|
@ -43,7 +43,6 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
test('should validate client metrics', () => {
|
||||
expect.assertions(0);
|
||||
return request
|
||||
.post('/api/client/metrics')
|
||||
.send({ random: 'blush' })
|
||||
@ -51,7 +50,6 @@ test('should validate client metrics', () => {
|
||||
});
|
||||
|
||||
test('should accept empty client metrics', () => {
|
||||
expect.assertions(0);
|
||||
return request
|
||||
.post('/api/client/metrics')
|
||||
.send({
|
||||
@ -67,7 +65,6 @@ test('should accept empty client metrics', () => {
|
||||
});
|
||||
|
||||
test('should accept client metrics with yes/no', () => {
|
||||
expect.assertions(0);
|
||||
return request
|
||||
.post('/api/client/metrics')
|
||||
.send({
|
||||
@ -88,7 +85,6 @@ test('should accept client metrics with yes/no', () => {
|
||||
});
|
||||
|
||||
test('should accept client metrics with variants', () => {
|
||||
expect.assertions(0);
|
||||
return request
|
||||
.post('/api/client/metrics')
|
||||
.send({
|
||||
@ -113,7 +109,6 @@ test('should accept client metrics with variants', () => {
|
||||
});
|
||||
|
||||
test('should accept client metrics without yes/no', () => {
|
||||
expect.assertions(0);
|
||||
return request
|
||||
.post('/api/client/metrics')
|
||||
.send({
|
||||
@ -133,7 +128,7 @@ test('should accept client metrics without yes/no', () => {
|
||||
.expect(202);
|
||||
});
|
||||
|
||||
test('shema allow empty strings', () => {
|
||||
test('schema allow empty strings', () => {
|
||||
const data = {
|
||||
appName: 'java-test',
|
||||
instanceId: 'instance y',
|
||||
|
@ -1,9 +1,12 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { Response } from 'express';
|
||||
import Controller from '../controller';
|
||||
import { IUnleashServices } from '../../types';
|
||||
import { IUnleashConfig } from '../../types/option';
|
||||
import ClientMetricsService from '../../services/client-metrics';
|
||||
import { Logger } from '../../logger';
|
||||
import { IAuthRequest } from '../unleash-types';
|
||||
import ApiUser from '../../types/api-user';
|
||||
import { ALL } from '../../types/models/api-token';
|
||||
|
||||
export default class ClientMetricsController extends Controller {
|
||||
logger: Logger;
|
||||
@ -23,13 +26,13 @@ export default class ClientMetricsController extends Controller {
|
||||
this.post('/', this.registerMetrics);
|
||||
}
|
||||
|
||||
async registerMetrics(
|
||||
req: Request<any, any, any, any>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const data = req.body;
|
||||
const clientIp = req.ip;
|
||||
|
||||
async registerMetrics(req: IAuthRequest, res: Response): Promise<void> {
|
||||
const { body: data, ip: clientIp, user } = req;
|
||||
if (user instanceof ApiUser) {
|
||||
if (user.environment !== ALL) {
|
||||
data.environment = user.environment;
|
||||
}
|
||||
}
|
||||
await this.metrics.registerClientMetrics(data, clientIp);
|
||||
return res.status(202).end();
|
||||
}
|
||||
|
17
src/lib/schema/api-token-schema.ts
Normal file
17
src/lib/schema/api-token-schema.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import joi from 'joi';
|
||||
import { ALL, ApiTokenType } from '../types/models/api-token';
|
||||
|
||||
export const createApiToken = joi
|
||||
.object()
|
||||
.keys({
|
||||
username: joi.string().required(),
|
||||
type: joi
|
||||
.string()
|
||||
.lowercase()
|
||||
.required()
|
||||
.valid(ApiTokenType.ADMIN, ApiTokenType.CLIENT),
|
||||
expiresAt: joi.date().optional(),
|
||||
project: joi.string().optional().default(ALL),
|
||||
environment: joi.string().optional().default(ALL),
|
||||
})
|
||||
.options({ stripUnknown: true, allowUnknown: false, abortEarly: false });
|
@ -115,9 +115,9 @@ async function start(opts: IUnleashOptions = {}): Promise<IUnleash> {
|
||||
if (config.db.disableMigration) {
|
||||
logger.info('DB migration: disabled');
|
||||
} else {
|
||||
logger.info('DB migration: start');
|
||||
logger.debug('DB migration: start');
|
||||
await migrateDb(config);
|
||||
logger.info('DB migration: end');
|
||||
logger.debug('DB migration: end');
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Failed to migrate db', err);
|
||||
|
@ -5,19 +5,17 @@ import { IUnleashStores } from '../types/stores';
|
||||
import { IUnleashConfig } from '../types/option';
|
||||
import ApiUser from '../types/api-user';
|
||||
import {
|
||||
ALL,
|
||||
ApiTokenType,
|
||||
IApiToken,
|
||||
IApiTokenStore,
|
||||
} from '../types/stores/api-token-store';
|
||||
IApiTokenCreate,
|
||||
} from '../types/models/api-token';
|
||||
import { IApiTokenStore } from '../types/stores/api-token-store';
|
||||
import { FOREIGN_KEY_VIOLATION } from '../error/db-error';
|
||||
import BadDataError from '../error/bad-data-error';
|
||||
|
||||
const ONE_MINUTE = 60_000;
|
||||
|
||||
interface CreateTokenRequest {
|
||||
username: string;
|
||||
type: ApiTokenType;
|
||||
expiresAt?: Date;
|
||||
}
|
||||
|
||||
export class ApiTokenService {
|
||||
private store: IApiTokenStore;
|
||||
|
||||
@ -66,6 +64,9 @@ export class ApiTokenService {
|
||||
return new ApiUser({
|
||||
username: token.username,
|
||||
permissions,
|
||||
project: token.project,
|
||||
environment: token.environment,
|
||||
type: token.type,
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
@ -82,16 +83,49 @@ export class ApiTokenService {
|
||||
return this.store.delete(secret);
|
||||
}
|
||||
|
||||
public async creteApiToken(
|
||||
creteTokenRequest: CreateTokenRequest,
|
||||
): Promise<IApiToken> {
|
||||
const secret = this.generateSecretKey();
|
||||
const createNewToken = { ...creteTokenRequest, secret };
|
||||
return this.store.insert(createNewToken);
|
||||
private validateAdminToken({ type, project, environment }) {
|
||||
if (type === ApiTokenType.ADMIN && project !== ALL) {
|
||||
throw new BadDataError(
|
||||
'Admin token cannot be scoped to single project',
|
||||
);
|
||||
}
|
||||
|
||||
if (type === ApiTokenType.ADMIN && environment !== ALL) {
|
||||
throw new BadDataError(
|
||||
'Admin token cannot be scoped to single environment',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private generateSecretKey() {
|
||||
return crypto.randomBytes(32).toString('hex');
|
||||
public async createApiToken(
|
||||
newToken: Omit<IApiTokenCreate, 'secret'>,
|
||||
): Promise<IApiToken> {
|
||||
this.validateAdminToken(newToken);
|
||||
|
||||
const secret = this.generateSecretKey(newToken);
|
||||
const createNewToken = { ...newToken, secret };
|
||||
|
||||
try {
|
||||
const token = await this.store.insert(createNewToken);
|
||||
this.activeTokens.push(token);
|
||||
return token;
|
||||
} catch (error) {
|
||||
if (error.code === FOREIGN_KEY_VIOLATION) {
|
||||
let { message } = error;
|
||||
if (error.constraint === 'api_tokens_project_fkey') {
|
||||
message = `Project=${newToken.project} does not exist`;
|
||||
} else if (error.constraint === 'api_tokens_environment_fkey') {
|
||||
message = `Environment=${newToken.environment} does not exist`;
|
||||
}
|
||||
throw new BadDataError(message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private generateSecretKey({ project, environment }) {
|
||||
const randomStr = crypto.randomBytes(28).toString('hex');
|
||||
return `${project}:${environment}.${randomStr}`;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
|
@ -13,6 +13,7 @@ export const clientMetricsSchema = joi
|
||||
.object()
|
||||
.options({ stripUnknown: true })
|
||||
.keys({
|
||||
environment: joi.string().optional(),
|
||||
appName: joi.string().required(),
|
||||
instanceId: joi.string().required(),
|
||||
bucket: joi
|
||||
|
@ -1,23 +1,42 @@
|
||||
import { ApiTokenType } from './models/api-token';
|
||||
import { CLIENT } from './permissions';
|
||||
|
||||
interface IApiUserData {
|
||||
username: string;
|
||||
permissions?: string[];
|
||||
project: string;
|
||||
environment: string;
|
||||
type: ApiTokenType;
|
||||
}
|
||||
|
||||
export default class ApiUser {
|
||||
isAPI: boolean = true;
|
||||
readonly isAPI: boolean = true;
|
||||
|
||||
username: string;
|
||||
readonly username: string;
|
||||
|
||||
permissions: string[];
|
||||
readonly permissions: string[];
|
||||
|
||||
constructor({ username, permissions = [CLIENT] }: IApiUserData) {
|
||||
readonly project: string;
|
||||
|
||||
readonly environment: string;
|
||||
|
||||
readonly type: ApiTokenType;
|
||||
|
||||
constructor({
|
||||
username,
|
||||
permissions = [CLIENT],
|
||||
project,
|
||||
environment,
|
||||
type,
|
||||
}: IApiUserData) {
|
||||
if (!username) {
|
||||
throw new TypeError('username is required');
|
||||
}
|
||||
this.username = username;
|
||||
this.permissions = permissions;
|
||||
this.project = project;
|
||||
this.environment = environment;
|
||||
this.type = type;
|
||||
}
|
||||
}
|
||||
|
||||
|
22
src/lib/types/models/api-token.ts
Normal file
22
src/lib/types/models/api-token.ts
Normal file
@ -0,0 +1,22 @@
|
||||
export const ALL = '*';
|
||||
|
||||
export enum ApiTokenType {
|
||||
CLIENT = 'client',
|
||||
ADMIN = 'admin',
|
||||
}
|
||||
|
||||
export interface IApiTokenCreate {
|
||||
secret: string;
|
||||
username: string;
|
||||
type: ApiTokenType;
|
||||
environment: string;
|
||||
project: string;
|
||||
expiresAt?: Date;
|
||||
}
|
||||
|
||||
export interface IApiToken extends IApiTokenCreate {
|
||||
createdAt: Date;
|
||||
seenAt?: Date;
|
||||
environment: string;
|
||||
project: string;
|
||||
}
|
@ -1,22 +1,6 @@
|
||||
import { IApiToken, IApiTokenCreate } from '../models/api-token';
|
||||
import { Store } from './store';
|
||||
|
||||
export enum ApiTokenType {
|
||||
CLIENT = 'client',
|
||||
ADMIN = 'admin',
|
||||
}
|
||||
|
||||
export interface IApiTokenCreate {
|
||||
secret: string;
|
||||
username: string;
|
||||
type: ApiTokenType;
|
||||
expiresAt?: Date;
|
||||
}
|
||||
|
||||
export interface IApiToken extends IApiTokenCreate {
|
||||
createdAt: Date;
|
||||
seenAt?: Date;
|
||||
}
|
||||
|
||||
export interface IApiTokenStore extends Store<IApiToken, string> {
|
||||
getAllActive(): Promise<IApiToken[]>;
|
||||
insert(newToken: IApiTokenCreate): Promise<IApiToken>;
|
||||
|
@ -14,7 +14,7 @@ function registerGracefulShutdown(unleash: IUnleash, logger: Logger): void {
|
||||
}
|
||||
};
|
||||
|
||||
logger.info('Registering graceful shutdown');
|
||||
logger.debug('Registering graceful shutdown');
|
||||
|
||||
process.on('SIGINT', unleashCloser('SIGINT'));
|
||||
process.on('SIGHUP', unleashCloser('SIGHUP'));
|
||||
|
19
src/migrations/20210913103159-api-keys-scoping.js
Normal file
19
src/migrations/20210913103159-api-keys-scoping.js
Normal file
@ -0,0 +1,19 @@
|
||||
exports.up = function (db, cb) {
|
||||
db.runSql(
|
||||
`
|
||||
ALTER TABLE api_tokens ADD COLUMN project VARCHAR REFERENCES PROJECTS(id) ON DELETE CASCADE;;
|
||||
ALTER TABLE api_tokens ADD COLUMN environment VARCHAR REFERENCES environments(name) ON DELETE CASCADE;;
|
||||
`,
|
||||
cb,
|
||||
);
|
||||
};
|
||||
|
||||
exports.down = function (db, cb) {
|
||||
db.runSql(
|
||||
`
|
||||
ALTER TABLE api_tokens DROP COLUMN project;
|
||||
ALTER TABLE api_tokens DROP COLUMN environment;
|
||||
`,
|
||||
cb,
|
||||
);
|
||||
};
|
@ -1,7 +1,7 @@
|
||||
import { setupAppWithCustomAuth } from '../../helpers/test-helper';
|
||||
import dbInit from '../../helpers/database-init';
|
||||
import getLogger from '../../../fixtures/no-logger';
|
||||
import { ApiTokenType } from '../../../../lib/types/stores/api-token-store';
|
||||
import { ApiTokenType } from '../../../../lib/types/models/api-token';
|
||||
import { RoleName } from '../../../../lib/types/model';
|
||||
|
||||
let stores;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { setupApp } from '../../helpers/test-helper';
|
||||
import dbInit from '../../helpers/database-init';
|
||||
import getLogger from '../../../fixtures/no-logger';
|
||||
import { ApiTokenType } from '../../../../lib/types/stores/api-token-store';
|
||||
import { ALL, ApiTokenType } from '../../../../lib/types/models/api-token';
|
||||
|
||||
let db;
|
||||
let app;
|
||||
@ -70,6 +70,25 @@ test('creates new admin token', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('creates new ADMIN token should fix casing', async () => {
|
||||
expect.assertions(5);
|
||||
return app.request
|
||||
.post('/api/admin/api-tokens')
|
||||
.send({
|
||||
username: 'default-admin',
|
||||
type: 'ADMIN',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(201)
|
||||
.expect((res) => {
|
||||
expect(res.body.username).toBe('default-admin');
|
||||
expect(res.body.type).toBe('admin');
|
||||
expect(res.body.createdAt).toBeTruthy();
|
||||
expect(res.body.expiresAt).toBeFalsy();
|
||||
expect(res.body.secret.length > 16).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('creates new admin token with expiry', async () => {
|
||||
expect.assertions(1);
|
||||
const expiresAt = new Date();
|
||||
@ -170,3 +189,142 @@ test('removes api token', async () => {
|
||||
expect(res.body.tokens.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('creates new client token: project & environment defaults to "*"', async () => {
|
||||
return app.request
|
||||
.post('/api/admin/api-tokens')
|
||||
.send({
|
||||
username: 'default-client',
|
||||
type: 'client',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(201)
|
||||
.expect((res) => {
|
||||
expect(res.body.type).toBe('client');
|
||||
expect(res.body.secret.length > 16).toBe(true);
|
||||
expect(res.body.environment).toBe(ALL);
|
||||
expect(res.body.project).toBe(ALL);
|
||||
});
|
||||
});
|
||||
|
||||
test('creates new client token with project & environment set', async () => {
|
||||
return app.request
|
||||
.post('/api/admin/api-tokens')
|
||||
.send({
|
||||
username: 'default-client',
|
||||
type: 'client',
|
||||
project: 'default',
|
||||
environment: ':global:',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(201)
|
||||
.expect((res) => {
|
||||
expect(res.body.type).toBe('client');
|
||||
expect(res.body.secret.length > 16).toBe(true);
|
||||
expect(res.body.environment).toBe(':global:');
|
||||
expect(res.body.project).toBe('default');
|
||||
});
|
||||
});
|
||||
|
||||
test('should prefix default token with "*:*."', async () => {
|
||||
return app.request
|
||||
.post('/api/admin/api-tokens')
|
||||
.send({
|
||||
username: 'default-client',
|
||||
type: 'client',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(201)
|
||||
.expect((res) => {
|
||||
expect(res.body.secret).toMatch(/\*:\*\..*/);
|
||||
});
|
||||
});
|
||||
|
||||
test('should prefix token with "project:environment."', async () => {
|
||||
return app.request
|
||||
.post('/api/admin/api-tokens')
|
||||
.send({
|
||||
username: 'default-client',
|
||||
type: 'client',
|
||||
project: 'default',
|
||||
environment: ':global:',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(201)
|
||||
.expect((res) => {
|
||||
expect(res.body.secret).toMatch(/default::global:\..*/);
|
||||
});
|
||||
});
|
||||
|
||||
test('should not create token for invalid projectId', async () => {
|
||||
return app.request
|
||||
.post('/api/admin/api-tokens')
|
||||
.send({
|
||||
username: 'default-client',
|
||||
type: 'client',
|
||||
project: 'bogus-project-something',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(400)
|
||||
.expect((res) => {
|
||||
expect(res.body.details[0].message).toMatch(
|
||||
/bogus-project-something/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should not create token for invalid environment', async () => {
|
||||
return app.request
|
||||
.post('/api/admin/api-tokens')
|
||||
.send({
|
||||
username: 'default-client',
|
||||
type: 'client',
|
||||
environment: 'bogus-environment-something',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(400)
|
||||
.expect((res) => {
|
||||
expect(res.body.details[0].message).toMatch(
|
||||
/bogus-environment-something/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should not create token for invalid project & environment', async () => {
|
||||
return app.request
|
||||
.post('/api/admin/api-tokens')
|
||||
.send({
|
||||
username: 'default-admin',
|
||||
type: 'admin',
|
||||
project: 'bogus-project-something',
|
||||
environment: 'bogus-environment-something',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
test('admin token only supports ALL projects', async () => {
|
||||
return app.request
|
||||
.post('/api/admin/api-tokens')
|
||||
.send({
|
||||
username: 'default-admin',
|
||||
type: 'admin',
|
||||
project: 'default',
|
||||
environment: '*',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
test('admin token only supports ALL environments', async () => {
|
||||
return app.request
|
||||
.post('/api/admin/api-tokens')
|
||||
.send({
|
||||
username: 'default-admin',
|
||||
type: 'admin',
|
||||
project: '*',
|
||||
environment: ':global:',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(400);
|
||||
});
|
||||
|
199
src/test/e2e/api/client/feature.token.access.e2e.test.ts
Normal file
199
src/test/e2e/api/client/feature.token.access.e2e.test.ts
Normal file
@ -0,0 +1,199 @@
|
||||
import { IUnleashTest, setupAppWithAuth } from '../../helpers/test-helper';
|
||||
import dbInit, { ITestDb } from '../../helpers/database-init';
|
||||
import getLogger from '../../../fixtures/no-logger';
|
||||
import { ApiTokenService } from '../../../../lib/services/api-token-service';
|
||||
import { ApiTokenType } from '../../../../lib/types/models/api-token';
|
||||
|
||||
let app: IUnleashTest;
|
||||
let db: ITestDb;
|
||||
|
||||
let apiTokenService: ApiTokenService;
|
||||
|
||||
const environment = 'testing';
|
||||
const project = 'default';
|
||||
const project2 = 'some';
|
||||
const username = 'test';
|
||||
const feature1 = 'f1.token.access';
|
||||
const feature2 = 'f2.token.access';
|
||||
const feature3 = 'f3.p2.token.access';
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('feature_api_api_access_client', getLogger);
|
||||
app = await setupAppWithAuth(db.stores);
|
||||
apiTokenService = app.services.apiTokenService;
|
||||
|
||||
const { featureToggleServiceV2, environmentService } = app.services;
|
||||
const { environmentStore, projectStore } = db.stores;
|
||||
|
||||
await environmentStore.create({
|
||||
name: environment,
|
||||
displayName: '',
|
||||
type: 'test',
|
||||
});
|
||||
|
||||
await projectStore.create({
|
||||
id: project2,
|
||||
name: 'Test Project 2',
|
||||
description: '',
|
||||
});
|
||||
|
||||
await environmentService.addEnvironmentToProject(environment, project);
|
||||
await environmentService.addEnvironmentToProject(environment, project2);
|
||||
|
||||
await featureToggleServiceV2.createFeatureToggle(
|
||||
project,
|
||||
{
|
||||
name: feature1,
|
||||
description: 'the #1 feature',
|
||||
},
|
||||
username,
|
||||
);
|
||||
|
||||
await featureToggleServiceV2.createStrategy(
|
||||
{
|
||||
name: 'default',
|
||||
constraints: [],
|
||||
parameters: {},
|
||||
},
|
||||
project,
|
||||
feature1,
|
||||
);
|
||||
await featureToggleServiceV2.createStrategy(
|
||||
{
|
||||
name: 'custom-testing',
|
||||
constraints: [],
|
||||
parameters: {},
|
||||
},
|
||||
project,
|
||||
feature1,
|
||||
environment,
|
||||
);
|
||||
|
||||
// create feature 2
|
||||
await featureToggleServiceV2.createFeatureToggle(
|
||||
project,
|
||||
{
|
||||
name: feature2,
|
||||
},
|
||||
username,
|
||||
);
|
||||
await featureToggleServiceV2.createStrategy(
|
||||
{
|
||||
name: 'default',
|
||||
constraints: [],
|
||||
parameters: {},
|
||||
},
|
||||
project,
|
||||
feature2,
|
||||
environment,
|
||||
);
|
||||
|
||||
// create feature 3
|
||||
await featureToggleServiceV2.createFeatureToggle(
|
||||
project2,
|
||||
{
|
||||
name: feature3,
|
||||
},
|
||||
username,
|
||||
);
|
||||
await featureToggleServiceV2.createStrategy(
|
||||
{
|
||||
name: 'default',
|
||||
constraints: [],
|
||||
parameters: {},
|
||||
},
|
||||
project2,
|
||||
feature3,
|
||||
environment,
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.destroy();
|
||||
await db.destroy();
|
||||
});
|
||||
|
||||
test('returns feature toggle with :global: config', async () => {
|
||||
const token = await apiTokenService.createApiToken({
|
||||
type: ApiTokenType.CLIENT,
|
||||
username,
|
||||
environment: ':global:',
|
||||
project,
|
||||
});
|
||||
await app.request
|
||||
.get('/api/client/features')
|
||||
.set('Authorization', token.secret)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
const { features } = res.body;
|
||||
const f1 = features.find((f) => f.name === feature1);
|
||||
const f2 = features.find((f) => f.name === feature2);
|
||||
expect(features).toHaveLength(2);
|
||||
expect(f1.strategies).toHaveLength(1);
|
||||
expect(f2.strategies).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('returns feature toggle with :global: config', async () => {
|
||||
const token = await apiTokenService.createApiToken({
|
||||
type: ApiTokenType.CLIENT,
|
||||
username,
|
||||
environment,
|
||||
project,
|
||||
});
|
||||
await app.request
|
||||
.get('/api/client/features')
|
||||
.set('Authorization', token.secret)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
const { features, query } = res.body;
|
||||
const f1 = features.find((f) => f.name === feature1);
|
||||
const f2 = features.find((f) => f.name === feature2);
|
||||
|
||||
expect(features).toHaveLength(2);
|
||||
expect(f1.strategies).toHaveLength(2);
|
||||
expect(f2.strategies).toHaveLength(1);
|
||||
expect(query.project[0]).toBe(project);
|
||||
expect(query.environment).toBe(environment);
|
||||
});
|
||||
});
|
||||
|
||||
test('returns feature toggle for project2', async () => {
|
||||
const token = await apiTokenService.createApiToken({
|
||||
type: ApiTokenType.CLIENT,
|
||||
username,
|
||||
environment,
|
||||
project: project2,
|
||||
});
|
||||
await app.request
|
||||
.get('/api/client/features')
|
||||
.set('Authorization', token.secret)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
const { features } = res.body;
|
||||
const f3 = features.find((f) => f.name === feature3);
|
||||
expect(features).toHaveLength(1);
|
||||
expect(f3.strategies).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('returns feature toggle for all projects', async () => {
|
||||
const token = await apiTokenService.createApiToken({
|
||||
type: ApiTokenType.CLIENT,
|
||||
username,
|
||||
environment,
|
||||
project: '*',
|
||||
});
|
||||
await app.request
|
||||
.get('/api/client/features')
|
||||
.set('Authorization', token.secret)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
const { features } = res.body;
|
||||
expect(features).toHaveLength(3);
|
||||
});
|
||||
});
|
45
src/test/e2e/api/client/metrics.e2e.access.e2e.test.ts
Normal file
45
src/test/e2e/api/client/metrics.e2e.access.e2e.test.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { setupAppWithAuth } from '../../helpers/test-helper';
|
||||
import metricsExample from '../../../examples/client-metrics.json';
|
||||
import dbInit from '../../helpers/database-init';
|
||||
import getLogger from '../../../fixtures/no-logger';
|
||||
import { ApiTokenType } from '../../../../lib/types/models/api-token';
|
||||
|
||||
let app;
|
||||
let db;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('metrics_api_client', getLogger);
|
||||
app = await setupAppWithAuth(db.stores);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.destroy();
|
||||
await db.destroy();
|
||||
});
|
||||
|
||||
test('should enrich metrics with environment from api-token', async () => {
|
||||
const { apiTokenService } = app.services;
|
||||
const { environmentStore, clientMetricsStore } = db.stores;
|
||||
|
||||
await environmentStore.create({
|
||||
name: 'some',
|
||||
displayName: '',
|
||||
type: 'test',
|
||||
});
|
||||
|
||||
const token = await apiTokenService.createApiToken({
|
||||
type: ApiTokenType.CLIENT,
|
||||
username: 'test',
|
||||
environment: 'some',
|
||||
project: '*',
|
||||
});
|
||||
|
||||
await app.request
|
||||
.post('/api/client/metrics')
|
||||
.set('Authorization', token.secret)
|
||||
.send(metricsExample)
|
||||
.expect(202);
|
||||
|
||||
const all = await clientMetricsStore.getAll();
|
||||
expect(all[0].metrics.environment).toBe('some');
|
||||
});
|
@ -2,10 +2,7 @@ import dbInit from '../helpers/database-init';
|
||||
import getLogger from '../../fixtures/no-logger';
|
||||
import { ApiTokenService } from '../../../lib/services/api-token-service';
|
||||
import { createTestConfig } from '../../config/test-config';
|
||||
import {
|
||||
ApiTokenType,
|
||||
IApiToken,
|
||||
} from '../../../lib/types/stores/api-token-store';
|
||||
import { ApiTokenType, IApiToken } from '../../../lib/types/models/api-token';
|
||||
|
||||
let db;
|
||||
let stores;
|
||||
@ -42,9 +39,11 @@ test('should have empty list of tokens', async () => {
|
||||
});
|
||||
|
||||
test('should create client token', async () => {
|
||||
const token = await apiTokenService.creteApiToken({
|
||||
const token = await apiTokenService.createApiToken({
|
||||
username: 'default-client',
|
||||
type: ApiTokenType.CLIENT,
|
||||
project: '*',
|
||||
environment: '*',
|
||||
});
|
||||
const allTokens = await apiTokenService.getAllTokens();
|
||||
|
||||
@ -56,9 +55,11 @@ test('should create client token', async () => {
|
||||
});
|
||||
|
||||
test('should create admin token', async () => {
|
||||
const token = await apiTokenService.creteApiToken({
|
||||
const token = await apiTokenService.createApiToken({
|
||||
username: 'admin',
|
||||
type: ApiTokenType.ADMIN,
|
||||
project: '*',
|
||||
environment: '*',
|
||||
});
|
||||
|
||||
expect(token.secret.length > 32).toBe(true);
|
||||
@ -67,10 +68,12 @@ test('should create admin token', async () => {
|
||||
|
||||
test('should set expiry of token', async () => {
|
||||
const time = new Date('2022-01-01');
|
||||
await apiTokenService.creteApiToken({
|
||||
await apiTokenService.createApiToken({
|
||||
username: 'default-client',
|
||||
type: ApiTokenType.CLIENT,
|
||||
expiresAt: time,
|
||||
project: '*',
|
||||
environment: '*',
|
||||
});
|
||||
|
||||
const [token] = await apiTokenService.getAllTokens();
|
||||
@ -82,10 +85,12 @@ test('should update expiry of token', async () => {
|
||||
const time = new Date('2022-01-01');
|
||||
const newTime = new Date('2023-01-01');
|
||||
|
||||
const token = await apiTokenService.creteApiToken({
|
||||
const token = await apiTokenService.createApiToken({
|
||||
username: 'default-client',
|
||||
type: ApiTokenType.CLIENT,
|
||||
expiresAt: time,
|
||||
project: '*',
|
||||
environment: '*',
|
||||
});
|
||||
|
||||
await apiTokenService.updateExpiry(token.secret, newTime);
|
||||
@ -99,16 +104,20 @@ test('should only return valid tokens', async () => {
|
||||
const today = new Date();
|
||||
const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000);
|
||||
|
||||
await apiTokenService.creteApiToken({
|
||||
await apiTokenService.createApiToken({
|
||||
username: 'default-expired',
|
||||
type: ApiTokenType.CLIENT,
|
||||
expiresAt: new Date('2021-01-01'),
|
||||
project: '*',
|
||||
environment: '*',
|
||||
});
|
||||
|
||||
const activeToken = await apiTokenService.creteApiToken({
|
||||
const activeToken = await apiTokenService.createApiToken({
|
||||
username: 'default-valid',
|
||||
type: ApiTokenType.CLIENT,
|
||||
expiresAt: tomorrow,
|
||||
project: '*',
|
||||
environment: '*',
|
||||
});
|
||||
|
||||
const tokens = await apiTokenService.getAllActiveTokens();
|
||||
|
8
src/test/fixtures/fake-api-token-store.ts
vendored
8
src/test/fixtures/fake-api-token-store.ts
vendored
@ -1,8 +1,6 @@
|
||||
import {
|
||||
IApiToken,
|
||||
IApiTokenCreate,
|
||||
IApiTokenStore,
|
||||
} from '../../lib/types/stores/api-token-store';
|
||||
import { IApiTokenStore } from '../../lib/types/stores/api-token-store';
|
||||
import { IApiToken, IApiTokenCreate } from '../../lib/types/models/api-token';
|
||||
|
||||
import NotFoundError from '../../lib/error/notfound-error';
|
||||
|
||||
export default class FakeApiTokenStore implements IApiTokenStore {
|
||||
|
Loading…
Reference in New Issue
Block a user