1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

feat: private projects in application overview (#6775)

This commit is contained in:
Mateusz Kwasniewski 2024-04-04 14:56:21 +02:00 committed by GitHub
parent f29ecaf3c0
commit 0422e5b5b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 173 additions and 3 deletions

View File

@ -3,10 +3,11 @@ import type { IClientApp } from '../../../types/model';
import FakeEventStore from '../../../../test/fixtures/fake-event-store';
import { createTestConfig } from '../../../../test/config/test-config';
import { FakePrivateProjectChecker } from '../../private-project/fakePrivateProjectChecker';
import type { IUnleashConfig } from '../../../types';
import type { IClientApplicationsStore, IUnleashConfig } from '../../../types';
import FakeClientMetricsStoreV2 from '../client-metrics/fake-client-metrics-store-v2';
import FakeStrategiesStore from '../../../../test/fixtures/fake-strategies-store';
import FakeFeatureToggleStore from '../../feature-toggle/fakes/fake-feature-toggle-store';
import type { IApplicationOverview } from './models';
let config: IUnleashConfig;
beforeAll(() => {
@ -189,3 +190,68 @@ test('No registrations during a time period will not call stores', async () => {
expect(appStoreSpy).toHaveBeenCalledTimes(0);
expect(bulkSpy).toHaveBeenCalledTimes(0);
});
test('filter out private projects from overview', async () => {
const clientApplicationsStore = {
async getApplicationOverview(
appName: string,
): Promise<IApplicationOverview> {
return {
environments: [
{
name: 'development',
instanceCount: 1,
sdks: ['unleash-client-node:3.5.1'],
lastSeen: new Date(),
issues: {
missingFeatures: [],
outdatedSdks: [],
},
},
],
projects: ['privateProject', 'publicProject'],
issues: {
missingStrategies: [],
},
featureCount: 0,
};
},
} as IClientApplicationsStore;
const privateProjectsChecker = {
async filterUserAccessibleProjects(
userId: number,
projects: string[],
): Promise<string[]> {
return projects.filter((project) => !project.includes('private'));
},
} as FakePrivateProjectChecker;
const clientInstanceService = new ClientInstanceService(
{ clientApplicationsStore } as any,
config,
privateProjectsChecker,
);
const overview = await clientInstanceService.getApplicationOverview(
'appName',
123,
);
expect(overview).toMatchObject({
environments: [
{
name: 'development',
instanceCount: 1,
sdks: ['unleash-client-node:3.5.1'],
issues: {
missingFeatures: [],
outdatedSdks: ['unleash-client-node:3.5.1'],
},
},
],
projects: ['publicProject'],
issues: {
missingStrategies: [],
},
featureCount: 0,
});
});

View File

@ -220,9 +220,16 @@ export default class ClientInstanceService {
async getApplicationOverview(
appName: string,
userId: number,
): Promise<IApplicationOverview> {
const result =
await this.clientApplicationsStore.getApplicationOverview(appName);
const accessibleProjects =
await this.privateProjectChecker.filterUserAccessibleProjects(
userId,
result.projects,
);
result.projects = accessibleProjects;
result.environments.forEach((environment) => {
environment.issues.outdatedSdks = findOutdatedSDKs(
environment.sdks,

View File

@ -2,6 +2,12 @@ import type { IPrivateProjectChecker } from './privateProjectCheckerType';
import { ALL_PROJECT_ACCESS, type ProjectAccess } from './privateProjectStore';
export class FakePrivateProjectChecker implements IPrivateProjectChecker {
async filterUserAccessibleProjects(
userId: number,
projects: string[],
): Promise<string[]> {
return projects;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getUserAccessibleProjects(userId: number): Promise<ProjectAccess> {
return ALL_PROJECT_ACCESS;

View File

@ -0,0 +1,68 @@
import { PrivateProjectChecker } from './privateProjectChecker';
import type { IPrivateProjectStore } from './privateProjectStoreType';
test('filter user accessible projects', async () => {
const checker = new PrivateProjectChecker(
{
privateProjectStore: {
async getUserAccessibleProjects() {
return {
mode: 'limited',
projects: ['projectA', 'projectB'],
};
},
} as IPrivateProjectStore,
},
{ isEnterprise: true },
);
const projects = await checker.filterUserAccessibleProjects(123, [
'projectA',
'projectC',
]);
expect(projects).toEqual(['projectA']);
});
test('do not filter for non enterprise', async () => {
const checker = new PrivateProjectChecker(
{
privateProjectStore: {
async getUserAccessibleProjects() {
return {
mode: 'limited',
projects: ['projectA', 'projectB'],
};
},
} as IPrivateProjectStore,
},
{ isEnterprise: false },
);
const projects = await checker.filterUserAccessibleProjects(123, [
'projectA',
'projectC',
]);
expect(projects).toEqual(['projectA', 'projectC']);
});
test('do not filter for all mode', async () => {
const checker = new PrivateProjectChecker(
{
privateProjectStore: {
async getUserAccessibleProjects() {
return { mode: 'all' };
},
} as IPrivateProjectStore,
},
{ isEnterprise: false },
);
const projects = await checker.filterUserAccessibleProjects(123, [
'projectA',
'projectC',
]);
expect(projects).toEqual(['projectA', 'projectC']);
});

View File

@ -22,6 +22,21 @@ export class PrivateProjectChecker implements IPrivateProjectChecker {
: Promise.resolve(ALL_PROJECT_ACCESS);
}
async filterUserAccessibleProjects(
userId: number,
projects: string[],
): Promise<string[]> {
if (!this.isEnterprise) {
return projects;
}
const accessibleProjects =
await this.privateProjectStore.getUserAccessibleProjects(userId);
if (accessibleProjects.mode === 'all') return projects;
return projects.filter((project) =>
accessibleProjects.projects.includes(project),
);
}
async hasAccessToProject(
userId: number,
projectId: string,

View File

@ -2,5 +2,9 @@ import type { ProjectAccess } from './privateProjectStore';
export interface IPrivateProjectChecker {
getUserAccessibleProjects(userId: number): Promise<ProjectAccess>;
filterUserAccessibleProjects(
userId: number,
projects: string[],
): Promise<string[]>;
hasAccessToProject(userId: number, projectId: string): Promise<boolean>;
}

View File

@ -281,12 +281,16 @@ class MetricsController extends Controller {
}
async getApplicationOverview(
req: Request<{ appName: string }>,
req: IAuthRequest<{ appName: string }>,
res: Response<ApplicationOverviewSchema>,
): Promise<void> {
const { appName } = req.params;
const { user } = req;
const overview =
await this.clientInstanceService.getApplicationOverview(appName);
await this.clientInstanceService.getApplicationOverview(
appName,
extractUserIdFromUser(user),
);
this.openApiService.respondWithValidation(
200,