1
0
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:
Jaanus Sellin 2024-04-16 15:47:45 +03:00 committed by GitHub
parent 8dbd680326
commit f45593176c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 150 additions and 22 deletions

View File

@ -132,6 +132,7 @@ exports[`should create default config 1`] = `
},
"migrationLock": true,
"outdatedSdksBanner": false,
"parseProjectFromSession": false,
"personalAccessTokensKillSwitch": false,
"projectListFilterMyProjects": false,
"projectOverviewRefactor": false,

View File

@ -10,6 +10,7 @@ import type { Logger, LogProvider } from '../logger';
import type { Db } from './db';
import type { IApplicationOverview } from '../features/metrics/instance/models';
import { applySearchFilters } from '../features/feature-search/search-utils';
import type { IFlagResolver } from '../types';
const COLUMNS = [
'app_name',
@ -110,14 +111,6 @@ const remapRow = (input) => {
return temp;
};
const remapUsageRow = (input) => {
return {
app_name: input.appName,
project: input.project || '*',
environment: input.environment || '*',
};
};
export default class ClientApplicationsStore
implements IClientApplicationsStore
{
@ -125,24 +118,32 @@ export default class ClientApplicationsStore
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.flagResolver = flagResolver;
this.logger = getLogger('client-applications-store.ts');
}
async upsert(details: Partial<IClientApplication>): Promise<void> {
const row = remapRow(details);
await this.db(TABLE).insert(row).onConflict('app_name').merge();
const usageRow = remapUsageRow(details);
const usageRows = this.remapUsageRow(details);
await this.db(TABLE_USAGE)
.insert(usageRow)
.insert(usageRows)
.onConflict(['app_name', 'project', 'environment'])
.merge();
}
async bulkUpsert(apps: Partial<IClientApplication>[]): Promise<void> {
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_USAGE)
.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 || '*',
};
}
};
}

View File

@ -65,6 +65,7 @@ export const createStores = (
db,
eventBus,
getLogger,
config.flagResolver,
),
clientInstanceStore: new ClientInstanceStore(db, eventBus, getLogger),
clientMetricsStoreV2: new ClientMetricsStoreV2(

View File

@ -28,6 +28,7 @@ export interface IApplication {
instances?: IClientInstance[];
seenToggles?: Record<string, any>;
project?: string;
projects?: string[];
environment?: string;
links?: Record<string, string>;
}

View File

@ -1,6 +1,6 @@
import type { Response } from 'express';
import Controller from '../../../routes/controller';
import type { IUnleashServices } from '../../../types';
import type { IFlagResolver, IUnleashServices } from '../../../types';
import type { IUnleashConfig } from '../../../types/option';
import type { Logger } from '../../../logger';
import type ClientInstanceService from './instance-service';
@ -24,6 +24,8 @@ export default class RegisterController extends Controller {
openApiService: OpenApiService;
flagResolver: IFlagResolver;
constructor(
{
clientInstanceService,
@ -35,6 +37,7 @@ export default class RegisterController extends Controller {
this.logger = config.getLogger('/api/client/register');
this.clientInstanceService = clientInstanceService;
this.openApiService = openApiService;
this.flagResolver = config.flagResolver;
this.route({
method: 'post',
@ -62,7 +65,7 @@ export default class RegisterController extends Controller {
});
}
private static resolveEnvironment(
private resolveEnvironment(
user: IUser | IApiUser,
data: Partial<IClientApp>,
) {
@ -76,7 +79,14 @@ export default class RegisterController extends Controller {
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>,
) {
const token = req.get('Authorisation') || req.headers.authorization;
@ -91,8 +101,13 @@ export default class RegisterController extends Controller {
res: Response<void>,
): Promise<void> {
const { body: data, ip: clientIp, user } = req;
data.environment = RegisterController.resolveEnvironment(user, data);
data.project = RegisterController.extractProjectFromRequest(req);
data.environment = this.resolveEnvironment(user, data);
if (this.flagResolver.isEnabled('parseProjectFromSession')) {
data.projects = this.resolveProject(user);
} else {
data.project = this.extractProjectFromRequest(req);
}
await this.clientInstanceService.registerClient(data, clientIp);
res.header('X-Unleash-Version', version).status(202).end();
}

View File

@ -92,4 +92,5 @@ export const clientRegisterSchema = joi
interval: joi.number().required(),
environment: joi.string().optional(),
project: joi.string().optional(),
projects: joi.array().optional().items(joi.string()),
});

View File

@ -58,7 +58,8 @@ export type IFlagKey =
| 'bearerTokenMiddleware'
| 'projectOverviewRefactorFeedback'
| 'featureLifecycle'
| 'projectListFilterMyProjects';
| 'projectListFilterMyProjects'
| 'parseProjectFromSession';
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
@ -287,6 +288,10 @@ const flags: IFlags = {
process.env.UNLEASH_EXPERIMENTAL_PROJECTS_LIST_MY_PROJECTS,
false,
),
parseProjectFromSession: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_PARSE_PROJECT_FROM_SESSION,
false,
),
};
export const defaultExperimentalOptions: IExperimentalOptions = {

View File

@ -55,6 +55,7 @@ process.nextTick(async () => {
projectOverviewRefactorFeedback: true,
featureLifecycle: true,
projectListFilterMyProjects: true,
parseProjectFromSession: true,
},
},
authentication: {

View File

@ -4,13 +4,31 @@ import {
setupAppWithCustomConfig,
} from '../../helpers/test-helper';
import getLogger from '../../../fixtures/no-logger';
import { ApiTokenType } from '../../../../lib/types/models/api-token';
let app: IUnleashTest;
let db: ITestDb;
beforeAll(async () => {
db = await dbInit('metrics_serial', getLogger, {});
app = await setupAppWithCustomConfig(db.stores, {}, db.rawDatabase);
db = await dbInit('metrics_serial', getLogger, {
experimental: {
flags: {
parseProjectFromSession: true,
},
},
});
app = await setupAppWithCustomConfig(
db.stores,
{
experimental: {
flags: {
strictSchemaValidation: true,
parseProjectFromSession: true,
},
},
},
db.rawDatabase,
);
});
beforeEach(async () => {
@ -52,7 +70,7 @@ beforeEach(async () => {
appName: 'usage-app',
strategies: ['default'],
description: 'Some desc',
project: 'default',
projects: ['default'],
environment: 'dev',
});
});
@ -123,6 +141,64 @@ test('should get list of application usage', async () => {
);
expect(application).toMatchObject({
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,
});
});