mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
feat: start extracting project from session object (#6856)
Previously, we were extracting the project from the token, but now we will retrieve it from the session, which contains the full list of projects. This change also resolves an issue we encountered when the token was a multi-project token, formatted as []:dev:token. Previously, it was unable to display the exact list of projects. Now, it will show the exact project names.
This commit is contained in:
parent
8dbd680326
commit
f45593176c
@ -132,6 +132,7 @@ exports[`should create default config 1`] = `
|
|||||||
},
|
},
|
||||||
"migrationLock": true,
|
"migrationLock": true,
|
||||||
"outdatedSdksBanner": false,
|
"outdatedSdksBanner": false,
|
||||||
|
"parseProjectFromSession": false,
|
||||||
"personalAccessTokensKillSwitch": false,
|
"personalAccessTokensKillSwitch": false,
|
||||||
"projectListFilterMyProjects": false,
|
"projectListFilterMyProjects": false,
|
||||||
"projectOverviewRefactor": false,
|
"projectOverviewRefactor": false,
|
||||||
|
@ -10,6 +10,7 @@ import type { Logger, LogProvider } from '../logger';
|
|||||||
import type { Db } from './db';
|
import type { Db } from './db';
|
||||||
import type { IApplicationOverview } from '../features/metrics/instance/models';
|
import type { IApplicationOverview } from '../features/metrics/instance/models';
|
||||||
import { applySearchFilters } from '../features/feature-search/search-utils';
|
import { applySearchFilters } from '../features/feature-search/search-utils';
|
||||||
|
import type { IFlagResolver } from '../types';
|
||||||
|
|
||||||
const COLUMNS = [
|
const COLUMNS = [
|
||||||
'app_name',
|
'app_name',
|
||||||
@ -110,14 +111,6 @@ const remapRow = (input) => {
|
|||||||
return temp;
|
return temp;
|
||||||
};
|
};
|
||||||
|
|
||||||
const remapUsageRow = (input) => {
|
|
||||||
return {
|
|
||||||
app_name: input.appName,
|
|
||||||
project: input.project || '*',
|
|
||||||
environment: input.environment || '*',
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class ClientApplicationsStore
|
export default class ClientApplicationsStore
|
||||||
implements IClientApplicationsStore
|
implements IClientApplicationsStore
|
||||||
{
|
{
|
||||||
@ -125,24 +118,32 @@ export default class ClientApplicationsStore
|
|||||||
|
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
constructor(db: Db, eventBus: EventEmitter, getLogger: LogProvider) {
|
private flagResolver: IFlagResolver;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
db: Db,
|
||||||
|
eventBus: EventEmitter,
|
||||||
|
getLogger: LogProvider,
|
||||||
|
flagResolver: IFlagResolver,
|
||||||
|
) {
|
||||||
this.db = db;
|
this.db = db;
|
||||||
|
this.flagResolver = flagResolver;
|
||||||
this.logger = getLogger('client-applications-store.ts');
|
this.logger = getLogger('client-applications-store.ts');
|
||||||
}
|
}
|
||||||
|
|
||||||
async upsert(details: Partial<IClientApplication>): Promise<void> {
|
async upsert(details: Partial<IClientApplication>): Promise<void> {
|
||||||
const row = remapRow(details);
|
const row = remapRow(details);
|
||||||
await this.db(TABLE).insert(row).onConflict('app_name').merge();
|
await this.db(TABLE).insert(row).onConflict('app_name').merge();
|
||||||
const usageRow = remapUsageRow(details);
|
const usageRows = this.remapUsageRow(details);
|
||||||
await this.db(TABLE_USAGE)
|
await this.db(TABLE_USAGE)
|
||||||
.insert(usageRow)
|
.insert(usageRows)
|
||||||
.onConflict(['app_name', 'project', 'environment'])
|
.onConflict(['app_name', 'project', 'environment'])
|
||||||
.merge();
|
.merge();
|
||||||
}
|
}
|
||||||
|
|
||||||
async bulkUpsert(apps: Partial<IClientApplication>[]): Promise<void> {
|
async bulkUpsert(apps: Partial<IClientApplication>[]): Promise<void> {
|
||||||
const rows = apps.map(remapRow);
|
const rows = apps.map(remapRow);
|
||||||
const usageRows = apps.map(remapUsageRow);
|
const usageRows = apps.flatMap(this.remapUsageRow);
|
||||||
await this.db(TABLE).insert(rows).onConflict('app_name').merge();
|
await this.db(TABLE).insert(rows).onConflict('app_name').merge();
|
||||||
await this.db(TABLE_USAGE)
|
await this.db(TABLE_USAGE)
|
||||||
.insert(usageRows)
|
.insert(usageRows)
|
||||||
@ -420,4 +421,30 @@ export default class ClientApplicationsStore
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private remapUsageRow = (input) => {
|
||||||
|
if (this.flagResolver.isEnabled('parseProjectFromSession')) {
|
||||||
|
if (!input.projects || input.projects.length === 0) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
app_name: input.appName,
|
||||||
|
project: '*',
|
||||||
|
environment: input.environment || '*',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
return input.projects.map((project) => ({
|
||||||
|
app_name: input.appName,
|
||||||
|
project: project,
|
||||||
|
environment: input.environment || '*',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
app_name: input.appName,
|
||||||
|
project: input.project || '*',
|
||||||
|
environment: input.environment || '*',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -65,6 +65,7 @@ export const createStores = (
|
|||||||
db,
|
db,
|
||||||
eventBus,
|
eventBus,
|
||||||
getLogger,
|
getLogger,
|
||||||
|
config.flagResolver,
|
||||||
),
|
),
|
||||||
clientInstanceStore: new ClientInstanceStore(db, eventBus, getLogger),
|
clientInstanceStore: new ClientInstanceStore(db, eventBus, getLogger),
|
||||||
clientMetricsStoreV2: new ClientMetricsStoreV2(
|
clientMetricsStoreV2: new ClientMetricsStoreV2(
|
||||||
|
@ -28,6 +28,7 @@ export interface IApplication {
|
|||||||
instances?: IClientInstance[];
|
instances?: IClientInstance[];
|
||||||
seenToggles?: Record<string, any>;
|
seenToggles?: Record<string, any>;
|
||||||
project?: string;
|
project?: string;
|
||||||
|
projects?: string[];
|
||||||
environment?: string;
|
environment?: string;
|
||||||
links?: Record<string, string>;
|
links?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { Response } from 'express';
|
import type { Response } from 'express';
|
||||||
import Controller from '../../../routes/controller';
|
import Controller from '../../../routes/controller';
|
||||||
import type { IUnleashServices } from '../../../types';
|
import type { IFlagResolver, IUnleashServices } from '../../../types';
|
||||||
import type { IUnleashConfig } from '../../../types/option';
|
import type { IUnleashConfig } from '../../../types/option';
|
||||||
import type { Logger } from '../../../logger';
|
import type { Logger } from '../../../logger';
|
||||||
import type ClientInstanceService from './instance-service';
|
import type ClientInstanceService from './instance-service';
|
||||||
@ -24,6 +24,8 @@ export default class RegisterController extends Controller {
|
|||||||
|
|
||||||
openApiService: OpenApiService;
|
openApiService: OpenApiService;
|
||||||
|
|
||||||
|
flagResolver: IFlagResolver;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
{
|
{
|
||||||
clientInstanceService,
|
clientInstanceService,
|
||||||
@ -35,6 +37,7 @@ export default class RegisterController extends Controller {
|
|||||||
this.logger = config.getLogger('/api/client/register');
|
this.logger = config.getLogger('/api/client/register');
|
||||||
this.clientInstanceService = clientInstanceService;
|
this.clientInstanceService = clientInstanceService;
|
||||||
this.openApiService = openApiService;
|
this.openApiService = openApiService;
|
||||||
|
this.flagResolver = config.flagResolver;
|
||||||
|
|
||||||
this.route({
|
this.route({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
@ -62,7 +65,7 @@ export default class RegisterController extends Controller {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static resolveEnvironment(
|
private resolveEnvironment(
|
||||||
user: IUser | IApiUser,
|
user: IUser | IApiUser,
|
||||||
data: Partial<IClientApp>,
|
data: Partial<IClientApp>,
|
||||||
) {
|
) {
|
||||||
@ -76,7 +79,14 @@ export default class RegisterController extends Controller {
|
|||||||
return 'default';
|
return 'default';
|
||||||
}
|
}
|
||||||
|
|
||||||
private static extractProjectFromRequest(
|
private resolveProject(user: IUser | IApiUser) {
|
||||||
|
if (user instanceof ApiUser) {
|
||||||
|
return user.projects;
|
||||||
|
}
|
||||||
|
return ['default'];
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractProjectFromRequest(
|
||||||
req: IAuthRequest<unknown, void, ClientApplicationSchema>,
|
req: IAuthRequest<unknown, void, ClientApplicationSchema>,
|
||||||
) {
|
) {
|
||||||
const token = req.get('Authorisation') || req.headers.authorization;
|
const token = req.get('Authorisation') || req.headers.authorization;
|
||||||
@ -91,8 +101,13 @@ export default class RegisterController extends Controller {
|
|||||||
res: Response<void>,
|
res: Response<void>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { body: data, ip: clientIp, user } = req;
|
const { body: data, ip: clientIp, user } = req;
|
||||||
data.environment = RegisterController.resolveEnvironment(user, data);
|
data.environment = this.resolveEnvironment(user, data);
|
||||||
data.project = RegisterController.extractProjectFromRequest(req);
|
if (this.flagResolver.isEnabled('parseProjectFromSession')) {
|
||||||
|
data.projects = this.resolveProject(user);
|
||||||
|
} else {
|
||||||
|
data.project = this.extractProjectFromRequest(req);
|
||||||
|
}
|
||||||
|
|
||||||
await this.clientInstanceService.registerClient(data, clientIp);
|
await this.clientInstanceService.registerClient(data, clientIp);
|
||||||
res.header('X-Unleash-Version', version).status(202).end();
|
res.header('X-Unleash-Version', version).status(202).end();
|
||||||
}
|
}
|
||||||
|
@ -92,4 +92,5 @@ export const clientRegisterSchema = joi
|
|||||||
interval: joi.number().required(),
|
interval: joi.number().required(),
|
||||||
environment: joi.string().optional(),
|
environment: joi.string().optional(),
|
||||||
project: joi.string().optional(),
|
project: joi.string().optional(),
|
||||||
|
projects: joi.array().optional().items(joi.string()),
|
||||||
});
|
});
|
||||||
|
@ -58,7 +58,8 @@ export type IFlagKey =
|
|||||||
| 'bearerTokenMiddleware'
|
| 'bearerTokenMiddleware'
|
||||||
| 'projectOverviewRefactorFeedback'
|
| 'projectOverviewRefactorFeedback'
|
||||||
| 'featureLifecycle'
|
| 'featureLifecycle'
|
||||||
| 'projectListFilterMyProjects';
|
| 'projectListFilterMyProjects'
|
||||||
|
| 'parseProjectFromSession';
|
||||||
|
|
||||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||||
|
|
||||||
@ -287,6 +288,10 @@ const flags: IFlags = {
|
|||||||
process.env.UNLEASH_EXPERIMENTAL_PROJECTS_LIST_MY_PROJECTS,
|
process.env.UNLEASH_EXPERIMENTAL_PROJECTS_LIST_MY_PROJECTS,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
parseProjectFromSession: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_EXPERIMENTAL_PARSE_PROJECT_FROM_SESSION,
|
||||||
|
false,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||||
|
@ -55,6 +55,7 @@ process.nextTick(async () => {
|
|||||||
projectOverviewRefactorFeedback: true,
|
projectOverviewRefactorFeedback: true,
|
||||||
featureLifecycle: true,
|
featureLifecycle: true,
|
||||||
projectListFilterMyProjects: true,
|
projectListFilterMyProjects: true,
|
||||||
|
parseProjectFromSession: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
authentication: {
|
authentication: {
|
||||||
|
@ -4,13 +4,31 @@ import {
|
|||||||
setupAppWithCustomConfig,
|
setupAppWithCustomConfig,
|
||||||
} from '../../helpers/test-helper';
|
} from '../../helpers/test-helper';
|
||||||
import getLogger from '../../../fixtures/no-logger';
|
import getLogger from '../../../fixtures/no-logger';
|
||||||
|
import { ApiTokenType } from '../../../../lib/types/models/api-token';
|
||||||
|
|
||||||
let app: IUnleashTest;
|
let app: IUnleashTest;
|
||||||
let db: ITestDb;
|
let db: ITestDb;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('metrics_serial', getLogger, {});
|
db = await dbInit('metrics_serial', getLogger, {
|
||||||
app = await setupAppWithCustomConfig(db.stores, {}, db.rawDatabase);
|
experimental: {
|
||||||
|
flags: {
|
||||||
|
parseProjectFromSession: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
app = await setupAppWithCustomConfig(
|
||||||
|
db.stores,
|
||||||
|
{
|
||||||
|
experimental: {
|
||||||
|
flags: {
|
||||||
|
strictSchemaValidation: true,
|
||||||
|
parseProjectFromSession: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
db.rawDatabase,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@ -52,7 +70,7 @@ beforeEach(async () => {
|
|||||||
appName: 'usage-app',
|
appName: 'usage-app',
|
||||||
strategies: ['default'],
|
strategies: ['default'],
|
||||||
description: 'Some desc',
|
description: 'Some desc',
|
||||||
project: 'default',
|
projects: ['default'],
|
||||||
environment: 'dev',
|
environment: 'dev',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -123,6 +141,64 @@ test('should get list of application usage', async () => {
|
|||||||
);
|
);
|
||||||
expect(application).toMatchObject({
|
expect(application).toMatchObject({
|
||||||
appName: 'usage-app',
|
appName: 'usage-app',
|
||||||
usage: [{ project: 'default', environments: ['dev'] }],
|
usage: [
|
||||||
|
{
|
||||||
|
project: 'default',
|
||||||
|
environments: ['dev'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should save multiple projects from token', async () => {
|
||||||
|
await db.reset();
|
||||||
|
await db.stores.projectStore.create({
|
||||||
|
id: 'mainProject',
|
||||||
|
name: 'mainProject',
|
||||||
|
});
|
||||||
|
|
||||||
|
const multiProjectToken =
|
||||||
|
await app.services.apiTokenService.createApiTokenWithProjects({
|
||||||
|
type: ApiTokenType.CLIENT,
|
||||||
|
projects: ['default', 'mainProject'],
|
||||||
|
environment: 'default',
|
||||||
|
tokenName: 'tester',
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.request
|
||||||
|
.post('/api/client/register')
|
||||||
|
.set('Authorization', multiProjectToken.secret)
|
||||||
|
.send({
|
||||||
|
appName: 'multi-project-app',
|
||||||
|
instanceId: 'instance-1',
|
||||||
|
strategies: ['default'],
|
||||||
|
started: Date.now(),
|
||||||
|
interval: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.services.clientInstanceService.bulkAdd();
|
||||||
|
|
||||||
|
const { body } = await app.request
|
||||||
|
.get('/api/admin/metrics/applications')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
applications: [
|
||||||
|
{
|
||||||
|
appName: 'multi-project-app',
|
||||||
|
usage: [
|
||||||
|
{
|
||||||
|
environments: ['default'],
|
||||||
|
project: 'default',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
environments: ['default'],
|
||||||
|
project: 'mainProject',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
total: 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user