1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-20 00:08:02 +01:00

feat: context/segment usage private (#4826)

This commit is contained in:
Jaanus Sellin 2023-09-25 15:50:44 +03:00 committed by GitHub
parent fd9aeec0fb
commit 2bf995e731
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 150 additions and 33 deletions

View File

@ -77,7 +77,6 @@ export const SegmentFormStepOne: React.FC<ISegmentFormPartOneProps> = ({
const projectsUsed = new Set<string>( const projectsUsed = new Set<string>(
strategies.map(({ projectId }) => projectId!).filter(Boolean) strategies.map(({ projectId }) => projectId!).filter(Boolean)
); );
const availableProjects = projects.filter( const availableProjects = projects.filter(
({ id }) => ({ id }) =>
!projectsUsed.size || !projectsUsed.size ||

View File

@ -5,6 +5,7 @@ import { IFeatureStrategy } from 'interfaces/strategy';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { formatStrategyName } from 'utils/strategyNames'; import { formatStrategyName } from 'utils/strategyNames';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
const StyledUl = styled('ul')(({ theme }) => ({ const StyledUl = styled('ul')(({ theme }) => ({
listStyle: 'none', listStyle: 'none',
@ -37,7 +38,6 @@ export const SegmentProjectAlert = ({
}, },
}); });
}; };
const projectList = ( const projectList = (
<StyledUl> <StyledUl>
{Array.from(projectsUsed).map(projectId => ( {Array.from(projectsUsed).map(projectId => (
@ -78,11 +78,19 @@ export const SegmentProjectAlert = ({
</StyledUl> </StyledUl>
); );
if (projectsUsed.length > 1) { if (projectsUsed.length > 0) {
return ( return (
<StyledAlert severity="info"> <StyledAlert severity="info">
You can't specify a project for this segment because it is used <ConditionallyRender
in multiple projects: condition={projectsUsed.length > 1}
show={
<span>
You can't specify a project for this segment because
it is used in multiple projects:
</span>
}
elseShow={<span>Usage of this segment:</span>}
/>
{projectList} {projectList}
</StyledAlert> </StyledAlert>
); );

View File

@ -38,11 +38,15 @@ import FakeFeatureStrategiesStore from '../../../test/fixtures/fake-feature-stra
import FakeFeatureEnvironmentStore from '../../../test/fixtures/fake-feature-environment-store'; import FakeFeatureEnvironmentStore from '../../../test/fixtures/fake-feature-environment-store';
import FakeStrategiesStore from '../../../test/fixtures/fake-strategies-store'; import FakeStrategiesStore from '../../../test/fixtures/fake-strategies-store';
import EventStore from '../../db/event-store'; import EventStore from '../../db/event-store';
import {
createFakePrivateProjectChecker,
createPrivateProjectChecker,
} from '../private-project/createPrivateProjectChecker';
export const createFakeExportImportTogglesService = ( export const createFakeExportImportTogglesService = (
config: IUnleashConfig, config: IUnleashConfig,
): ExportImportService => { ): ExportImportService => {
const { getLogger } = config; const { getLogger, flagResolver } = config;
const importTogglesStore = {} as ImportTogglesStore; const importTogglesStore = {} as ImportTogglesStore;
const featureToggleStore = new FakeFeatureToggleStore(); const featureToggleStore = new FakeFeatureToggleStore();
const tagStore = new FakeTagStore(); const tagStore = new FakeTagStore();
@ -57,6 +61,7 @@ export const createFakeExportImportTogglesService = (
const featureEnvironmentStore = new FakeFeatureEnvironmentStore(); const featureEnvironmentStore = new FakeFeatureEnvironmentStore();
const accessService = createFakeAccessService(config); const accessService = createFakeAccessService(config);
const featureToggleService = createFakeFeatureToggleService(config); const featureToggleService = createFakeFeatureToggleService(config);
const privateProjectChecker = createFakePrivateProjectChecker();
const featureTagService = new FeatureTagService( const featureTagService = new FeatureTagService(
{ {
@ -74,7 +79,8 @@ export const createFakeExportImportTogglesService = (
contextFieldStore, contextFieldStore,
featureStrategiesStore, featureStrategiesStore,
}, },
{ getLogger }, { getLogger, flagResolver },
privateProjectChecker,
); );
const strategyService = new StrategyService( const strategyService = new StrategyService(
{ strategyStore, eventStore }, { strategyStore, eventStore },
@ -152,6 +158,7 @@ export const createExportImportTogglesService = (
const eventStore = new EventStore(db, getLogger); const eventStore = new EventStore(db, getLogger);
const accessService = createAccessService(db, config); const accessService = createAccessService(db, config);
const featureToggleService = createFeatureToggleService(db, config); const featureToggleService = createFeatureToggleService(db, config);
const privateProjectChecker = createPrivateProjectChecker(db, config);
const featureTagService = new FeatureTagService( const featureTagService = new FeatureTagService(
{ {
@ -169,7 +176,8 @@ export const createExportImportTogglesService = (
contextFieldStore, contextFieldStore,
featureStrategiesStore, featureStrategiesStore,
}, },
{ getLogger }, { getLogger, flagResolver },
privateProjectChecker,
); );
const strategyService = new StrategyService( const strategyService = new StrategyService(
{ strategyStore, eventStore }, { strategyStore, eventStore },

View File

@ -11,6 +11,10 @@ import {
createChangeRequestAccessReadModel, createChangeRequestAccessReadModel,
createFakeChangeRequestAccessService, createFakeChangeRequestAccessService,
} from '../change-request-access-service/createChangeRequestAccessReadModel'; } from '../change-request-access-service/createChangeRequestAccessReadModel';
import {
createFakePrivateProjectChecker,
createPrivateProjectChecker,
} from '../private-project/createPrivateProjectChecker';
export const createSegmentService = ( export const createSegmentService = (
db: Db, db: Db,
@ -34,11 +38,13 @@ export const createSegmentService = (
db, db,
config, config,
); );
const privateProjectChecker = createPrivateProjectChecker(db, config);
return new SegmentService( return new SegmentService(
{ segmentStore, featureStrategiesStore, eventStore }, { segmentStore, featureStrategiesStore, eventStore },
changeRequestAccessReadModel, changeRequestAccessReadModel,
config, config,
privateProjectChecker,
); );
}; };
@ -50,9 +56,12 @@ export const createFakeSegmentService = (
const featureStrategiesStore = new FakeFeatureStrategiesStore(); const featureStrategiesStore = new FakeFeatureStrategiesStore();
const changeRequestAccessReadModel = createFakeChangeRequestAccessService(); const changeRequestAccessReadModel = createFakeChangeRequestAccessService();
const privateProjectChecker = createFakePrivateProjectChecker();
return new SegmentService( return new SegmentService(
{ segmentStore, featureStrategiesStore, eventStore }, { segmentStore, featureStrategiesStore, eventStore },
changeRequestAccessReadModel, changeRequestAccessReadModel,
config, config,
privateProjectChecker,
); );
}; };

View File

@ -347,7 +347,8 @@ export class SegmentsController extends Controller {
res: Response<SegmentStrategiesSchema>, res: Response<SegmentStrategiesSchema>,
): Promise<void> { ): Promise<void> {
const { id } = req.params; const { id } = req.params;
const strategies = await this.segmentService.getStrategies(id); const { user } = req;
const strategies = await this.segmentService.getStrategies(id, user.id);
// Remove unnecessary IFeatureStrategy fields from the response. // Remove unnecessary IFeatureStrategy fields from the response.
const segmentStrategies = strategies.map((strategy) => ({ const segmentStrategies = strategies.map((strategy) => ({
@ -368,7 +369,8 @@ export class SegmentsController extends Controller {
res: Response, res: Response,
): Promise<void> { ): Promise<void> {
const id = Number(req.params.id); const id = Number(req.params.id);
const strategies = await this.segmentService.getStrategies(id); const { user } = req;
const strategies = await this.segmentService.getStrategies(id, user.id);
if (strategies.length > 0) { if (strategies.length > 0) {
res.status(409).send(); res.status(409).send();

View File

@ -301,8 +301,12 @@ export class ContextController extends Controller {
res: Response<ContextFieldStrategiesSchema>, res: Response<ContextFieldStrategiesSchema>,
): Promise<void> { ): Promise<void> {
const { contextField } = req.params; const { contextField } = req.params;
const { user } = req;
const contextFields = const contextFields =
await this.contextService.getStrategiesByContextField(contextField); await this.contextService.getStrategiesByContextField(
contextField,
user.id,
);
this.openApiService.respondWithValidation( this.openApiService.respondWithValidation(
200, 200,

View File

@ -13,7 +13,7 @@ export interface ISegmentService {
get(id: number): Promise<ISegment>; get(id: number): Promise<ISegment>;
getStrategies(id: number): Promise<IFeatureStrategy[]>; getStrategies(id: number, userId: number): Promise<IFeatureStrategy[]>;
validateName(name: string): Promise<void>; validateName(name: string): Promise<void>;

View File

@ -9,6 +9,8 @@ import { IProjectStore } from '../types/stores/project-store';
import { IFeatureStrategiesStore, IUnleashStores } from '../types/stores'; import { IFeatureStrategiesStore, IUnleashStores } from '../types/stores';
import { IUnleashConfig } from '../types/option'; import { IUnleashConfig } from '../types/option';
import { ContextFieldStrategiesSchema } from '../openapi/spec/context-field-strategies-schema'; import { ContextFieldStrategiesSchema } from '../openapi/spec/context-field-strategies-schema';
import { IFeatureStrategy, IFlagResolver } from '../types';
import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType';
const { contextSchema, nameSchema } = require('./context-schema'); const { contextSchema, nameSchema } = require('./context-schema');
const NameExistsError = require('../error/name-exists-error'); const NameExistsError = require('../error/name-exists-error');
@ -30,6 +32,10 @@ class ContextService {
private logger: Logger; private logger: Logger;
private flagResolver: IFlagResolver;
private privateProjectChecker: IPrivateProjectChecker;
constructor( constructor(
{ {
projectStore, projectStore,
@ -43,10 +49,16 @@ class ContextService {
| 'contextFieldStore' | 'contextFieldStore'
| 'featureStrategiesStore' | 'featureStrategiesStore'
>, >,
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>, {
getLogger,
flagResolver,
}: Pick<IUnleashConfig, 'getLogger' | 'flagResolver'>,
privateProjectChecker: IPrivateProjectChecker,
) { ) {
this.privateProjectChecker = privateProjectChecker;
this.projectStore = projectStore; this.projectStore = projectStore;
this.eventStore = eventStore; this.eventStore = eventStore;
this.flagResolver = flagResolver;
this.contextFieldStore = contextFieldStore; this.contextFieldStore = contextFieldStore;
this.featureStrategiesStore = featureStrategiesStore; this.featureStrategiesStore = featureStrategiesStore;
this.logger = getLogger('services/context-service.js'); this.logger = getLogger('services/context-service.js');
@ -62,9 +74,31 @@ class ContextService {
async getStrategiesByContextField( async getStrategiesByContextField(
name: string, name: string,
userId: number,
): Promise<ContextFieldStrategiesSchema> { ): Promise<ContextFieldStrategiesSchema> {
const strategies = const strategies =
await this.featureStrategiesStore.getStrategiesByContextField(name); await this.featureStrategiesStore.getStrategiesByContextField(name);
if (this.flagResolver.isEnabled('privateProjects')) {
const accessibleProjects =
await this.privateProjectChecker.getUserAccessibleProjects(
userId,
);
if (accessibleProjects.mode === 'all') {
return this.mapStrategies(strategies);
} else {
return this.mapStrategies(
strategies.filter((strategy) =>
accessibleProjects.projects.includes(
strategy.projectId,
),
),
);
}
}
return this.mapStrategies(strategies);
}
private mapStrategies(strategies: IFeatureStrategy[]) {
return { return {
strategies: strategies.map((strategy) => ({ strategies: strategies.map((strategy) => ({
id: strategy.id, id: strategy.id,

View File

@ -169,7 +169,15 @@ export const createServices = (
config, config,
lastSeenService, lastSeenService,
); );
const contextService = new ContextService(stores, config); const privateProjectChecker = db
? createPrivateProjectChecker(db, config)
: createFakePrivateProjectChecker();
const contextService = new ContextService(
stores,
config,
privateProjectChecker,
);
const emailService = new EmailService(config.email, config.getLogger); const emailService = new EmailService(config.email, config.getLogger);
const eventService = new EventService(stores, config); const eventService = new EventService(stores, config);
const featureTypeService = new FeatureTypeService(stores, config); const featureTypeService = new FeatureTypeService(stores, config);
@ -201,11 +209,9 @@ export const createServices = (
stores, stores,
changeRequestAccessReadModel, changeRequestAccessReadModel,
config, config,
privateProjectChecker,
); );
const privateProjectChecker = db
? createPrivateProjectChecker(db, config)
: createFakePrivateProjectChecker();
const clientInstanceService = new ClientInstanceService( const clientInstanceService = new ClientInstanceService(
stores, stores,
config, config,

View File

@ -22,6 +22,7 @@ import BadDataError from '../error/bad-data-error';
import { ISegmentService } from '../segments/segment-service-interface'; import { ISegmentService } from '../segments/segment-service-interface';
import { PermissionError } from '../error'; import { PermissionError } from '../error';
import { IChangeRequestAccessReadModel } from '../features/change-request-access-service/change-request-access-read-model'; import { IChangeRequestAccessReadModel } from '../features/change-request-access-service/change-request-access-read-model';
import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType';
export class SegmentService implements ISegmentService { export class SegmentService implements ISegmentService {
private logger: Logger; private logger: Logger;
@ -38,6 +39,8 @@ export class SegmentService implements ISegmentService {
private flagResolver: IFlagResolver; private flagResolver: IFlagResolver;
private privateProjectChecker: IPrivateProjectChecker;
constructor( constructor(
{ {
segmentStore, segmentStore,
@ -49,11 +52,13 @@ export class SegmentService implements ISegmentService {
>, >,
changeRequestAccessReadModel: IChangeRequestAccessReadModel, changeRequestAccessReadModel: IChangeRequestAccessReadModel,
config: IUnleashConfig, config: IUnleashConfig,
privateProjectChecker: IPrivateProjectChecker,
) { ) {
this.segmentStore = segmentStore; this.segmentStore = segmentStore;
this.featureStrategiesStore = featureStrategiesStore; this.featureStrategiesStore = featureStrategiesStore;
this.eventStore = eventStore; this.eventStore = eventStore;
this.changeRequestAccessReadModel = changeRequestAccessReadModel; this.changeRequestAccessReadModel = changeRequestAccessReadModel;
this.privateProjectChecker = privateProjectChecker;
this.logger = config.getLogger('services/segment-service.ts'); this.logger = config.getLogger('services/segment-service.ts');
this.flagResolver = config.flagResolver; this.flagResolver = config.flagResolver;
this.config = config; this.config = config;
@ -81,8 +86,26 @@ export class SegmentService implements ISegmentService {
} }
// Used by unleash-enterprise. // Used by unleash-enterprise.
async getStrategies(id: number): Promise<IFeatureStrategy[]> { async getStrategies(
return this.featureStrategiesStore.getStrategiesBySegment(id); id: number,
userId: number,
): Promise<IFeatureStrategy[]> {
const strategies =
await this.featureStrategiesStore.getStrategiesBySegment(id);
if (this.flagResolver.isEnabled('privateProjects')) {
const accessibleProjects =
await this.privateProjectChecker.getUserAccessibleProjects(
userId,
);
if (accessibleProjects.mode === 'all') {
return strategies;
} else {
return strategies.filter((strategy) =>
accessibleProjects.projects.includes(strategy.projectId),
);
}
}
return strategies;
} }
async create( async create(

View File

@ -253,7 +253,12 @@ beforeAll(async () => {
featureToggleService = new FeatureToggleService( featureToggleService = new FeatureToggleService(
stores, stores,
config, config,
new SegmentService(stores, changeRequestAccessReadModel, config), new SegmentService(
stores,
changeRequestAccessReadModel,
config,
privateProjectChecker,
),
accessService, accessService,
changeRequestAccessReadModel, changeRequestAccessReadModel,
privateProjectChecker, privateProjectChecker,

View File

@ -39,7 +39,12 @@ beforeAll(async () => {
const featureToggleService = new FeatureToggleService( const featureToggleService = new FeatureToggleService(
stores, stores,
config, config,
new SegmentService(stores, changeRequestAccessReadModel, config), new SegmentService(
stores,
changeRequestAccessReadModel,
config,
privateProjectChecker,
),
accessService, accessService,
changeRequestAccessReadModel, changeRequestAccessReadModel,
privateProjectChecker, privateProjectChecker,

View File

@ -54,15 +54,17 @@ beforeAll(async () => {
db.rawDatabase, db.rawDatabase,
accessService, accessService,
); );
segmentService = new SegmentService(
stores,
changeRequestAccessReadModel,
config,
);
const privateProjectChecker = createPrivateProjectChecker( const privateProjectChecker = createPrivateProjectChecker(
db.rawDatabase, db.rawDatabase,
config, config,
); );
segmentService = new SegmentService(
stores,
changeRequestAccessReadModel,
config,
privateProjectChecker,
);
service = new FeatureToggleService( service = new FeatureToggleService(
stores, stores,
config, config,

View File

@ -43,15 +43,17 @@ beforeAll(async () => {
db.rawDatabase, db.rawDatabase,
accessService, accessService,
); );
segmentService = new SegmentService(
stores,
changeRequestAccessReadModel,
config,
);
const privateProjectChecker = createPrivateProjectChecker( const privateProjectChecker = createPrivateProjectChecker(
db.rawDatabase, db.rawDatabase,
config, config,
); );
segmentService = new SegmentService(
stores,
changeRequestAccessReadModel,
config,
privateProjectChecker,
);
featureToggleService = new FeatureToggleService( featureToggleService = new FeatureToggleService(
stores, stores,
config, config,

View File

@ -44,7 +44,12 @@ beforeAll(async () => {
featureToggleService = new FeatureToggleService( featureToggleService = new FeatureToggleService(
stores, stores,
config, config,
new SegmentService(stores, changeRequestAccessReadModel, config), new SegmentService(
stores,
changeRequestAccessReadModel,
config,
privateProjectChecker,
),
accessService, accessService,
changeRequestAccessReadModel, changeRequestAccessReadModel,
privateProjectChecker, privateProjectChecker,

View File

@ -65,7 +65,12 @@ beforeAll(async () => {
featureToggleService = new FeatureToggleService( featureToggleService = new FeatureToggleService(
stores, stores,
config, config,
new SegmentService(stores, changeRequestAccessReadModel, config), new SegmentService(
stores,
changeRequestAccessReadModel,
config,
privateProjectChecker,
),
accessService, accessService,
changeRequestAccessReadModel, changeRequestAccessReadModel,
privateProjectChecker, privateProjectChecker,