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 { 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,
|
||||
});
|
||||
});
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
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,
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user