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:
parent
f29ecaf3c0
commit
0422e5b5b0
@ -3,10 +3,11 @@ import type { IClientApp } from '../../../types/model';
|
|||||||
import FakeEventStore from '../../../../test/fixtures/fake-event-store';
|
import FakeEventStore from '../../../../test/fixtures/fake-event-store';
|
||||||
import { createTestConfig } from '../../../../test/config/test-config';
|
import { createTestConfig } from '../../../../test/config/test-config';
|
||||||
import { FakePrivateProjectChecker } from '../../private-project/fakePrivateProjectChecker';
|
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 FakeClientMetricsStoreV2 from '../client-metrics/fake-client-metrics-store-v2';
|
||||||
import FakeStrategiesStore from '../../../../test/fixtures/fake-strategies-store';
|
import FakeStrategiesStore from '../../../../test/fixtures/fake-strategies-store';
|
||||||
import FakeFeatureToggleStore from '../../feature-toggle/fakes/fake-feature-toggle-store';
|
import FakeFeatureToggleStore from '../../feature-toggle/fakes/fake-feature-toggle-store';
|
||||||
|
import type { IApplicationOverview } from './models';
|
||||||
|
|
||||||
let config: IUnleashConfig;
|
let config: IUnleashConfig;
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
@ -189,3 +190,68 @@ test('No registrations during a time period will not call stores', async () => {
|
|||||||
expect(appStoreSpy).toHaveBeenCalledTimes(0);
|
expect(appStoreSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(bulkSpy).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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -220,9 +220,16 @@ export default class ClientInstanceService {
|
|||||||
|
|
||||||
async getApplicationOverview(
|
async getApplicationOverview(
|
||||||
appName: string,
|
appName: string,
|
||||||
|
userId: number,
|
||||||
): Promise<IApplicationOverview> {
|
): Promise<IApplicationOverview> {
|
||||||
const result =
|
const result =
|
||||||
await this.clientApplicationsStore.getApplicationOverview(appName);
|
await this.clientApplicationsStore.getApplicationOverview(appName);
|
||||||
|
const accessibleProjects =
|
||||||
|
await this.privateProjectChecker.filterUserAccessibleProjects(
|
||||||
|
userId,
|
||||||
|
result.projects,
|
||||||
|
);
|
||||||
|
result.projects = accessibleProjects;
|
||||||
result.environments.forEach((environment) => {
|
result.environments.forEach((environment) => {
|
||||||
environment.issues.outdatedSdks = findOutdatedSDKs(
|
environment.issues.outdatedSdks = findOutdatedSDKs(
|
||||||
environment.sdks,
|
environment.sdks,
|
||||||
|
@ -2,6 +2,12 @@ import type { IPrivateProjectChecker } from './privateProjectCheckerType';
|
|||||||
import { ALL_PROJECT_ACCESS, type ProjectAccess } from './privateProjectStore';
|
import { ALL_PROJECT_ACCESS, type ProjectAccess } from './privateProjectStore';
|
||||||
|
|
||||||
export class FakePrivateProjectChecker implements IPrivateProjectChecker {
|
export class FakePrivateProjectChecker implements IPrivateProjectChecker {
|
||||||
|
async filterUserAccessibleProjects(
|
||||||
|
userId: number,
|
||||||
|
projects: string[],
|
||||||
|
): Promise<string[]> {
|
||||||
|
return projects;
|
||||||
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
async getUserAccessibleProjects(userId: number): Promise<ProjectAccess> {
|
async getUserAccessibleProjects(userId: number): Promise<ProjectAccess> {
|
||||||
return ALL_PROJECT_ACCESS;
|
return ALL_PROJECT_ACCESS;
|
||||||
|
@ -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']);
|
||||||
|
});
|
@ -22,6 +22,21 @@ export class PrivateProjectChecker implements IPrivateProjectChecker {
|
|||||||
: Promise.resolve(ALL_PROJECT_ACCESS);
|
: 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(
|
async hasAccessToProject(
|
||||||
userId: number,
|
userId: number,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
|
@ -2,5 +2,9 @@ import type { ProjectAccess } from './privateProjectStore';
|
|||||||
|
|
||||||
export interface IPrivateProjectChecker {
|
export interface IPrivateProjectChecker {
|
||||||
getUserAccessibleProjects(userId: number): Promise<ProjectAccess>;
|
getUserAccessibleProjects(userId: number): Promise<ProjectAccess>;
|
||||||
|
filterUserAccessibleProjects(
|
||||||
|
userId: number,
|
||||||
|
projects: string[],
|
||||||
|
): Promise<string[]>;
|
||||||
hasAccessToProject(userId: number, projectId: string): Promise<boolean>;
|
hasAccessToProject(userId: number, projectId: string): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
@ -281,12 +281,16 @@ class MetricsController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getApplicationOverview(
|
async getApplicationOverview(
|
||||||
req: Request<{ appName: string }>,
|
req: IAuthRequest<{ appName: string }>,
|
||||||
res: Response<ApplicationOverviewSchema>,
|
res: Response<ApplicationOverviewSchema>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { appName } = req.params;
|
const { appName } = req.params;
|
||||||
|
const { user } = req;
|
||||||
const overview =
|
const overview =
|
||||||
await this.clientInstanceService.getApplicationOverview(appName);
|
await this.clientInstanceService.getApplicationOverview(
|
||||||
|
appName,
|
||||||
|
extractUserIdFromUser(user),
|
||||||
|
);
|
||||||
|
|
||||||
this.openApiService.respondWithValidation(
|
this.openApiService.respondWithValidation(
|
||||||
200,
|
200,
|
||||||
|
Loading…
Reference in New Issue
Block a user