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>(
strategies.map(({ projectId }) => projectId!).filter(Boolean)
);
const availableProjects = projects.filter(
({ id }) =>
!projectsUsed.size ||

View File

@ -5,6 +5,7 @@ import { IFeatureStrategy } from 'interfaces/strategy';
import { Link } from 'react-router-dom';
import { formatStrategyName } from 'utils/strategyNames';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
const StyledUl = styled('ul')(({ theme }) => ({
listStyle: 'none',
@ -37,7 +38,6 @@ export const SegmentProjectAlert = ({
},
});
};
const projectList = (
<StyledUl>
{Array.from(projectsUsed).map(projectId => (
@ -78,11 +78,19 @@ export const SegmentProjectAlert = ({
</StyledUl>
);
if (projectsUsed.length > 1) {
if (projectsUsed.length > 0) {
return (
<StyledAlert severity="info">
You can't specify a project for this segment because it is used
in multiple projects:
<ConditionallyRender
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}
</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 FakeStrategiesStore from '../../../test/fixtures/fake-strategies-store';
import EventStore from '../../db/event-store';
import {
createFakePrivateProjectChecker,
createPrivateProjectChecker,
} from '../private-project/createPrivateProjectChecker';
export const createFakeExportImportTogglesService = (
config: IUnleashConfig,
): ExportImportService => {
const { getLogger } = config;
const { getLogger, flagResolver } = config;
const importTogglesStore = {} as ImportTogglesStore;
const featureToggleStore = new FakeFeatureToggleStore();
const tagStore = new FakeTagStore();
@ -57,6 +61,7 @@ export const createFakeExportImportTogglesService = (
const featureEnvironmentStore = new FakeFeatureEnvironmentStore();
const accessService = createFakeAccessService(config);
const featureToggleService = createFakeFeatureToggleService(config);
const privateProjectChecker = createFakePrivateProjectChecker();
const featureTagService = new FeatureTagService(
{
@ -74,7 +79,8 @@ export const createFakeExportImportTogglesService = (
contextFieldStore,
featureStrategiesStore,
},
{ getLogger },
{ getLogger, flagResolver },
privateProjectChecker,
);
const strategyService = new StrategyService(
{ strategyStore, eventStore },
@ -152,6 +158,7 @@ export const createExportImportTogglesService = (
const eventStore = new EventStore(db, getLogger);
const accessService = createAccessService(db, config);
const featureToggleService = createFeatureToggleService(db, config);
const privateProjectChecker = createPrivateProjectChecker(db, config);
const featureTagService = new FeatureTagService(
{
@ -169,7 +176,8 @@ export const createExportImportTogglesService = (
contextFieldStore,
featureStrategiesStore,
},
{ getLogger },
{ getLogger, flagResolver },
privateProjectChecker,
);
const strategyService = new StrategyService(
{ strategyStore, eventStore },

View File

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

View File

@ -347,7 +347,8 @@ export class SegmentsController extends Controller {
res: Response<SegmentStrategiesSchema>,
): Promise<void> {
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.
const segmentStrategies = strategies.map((strategy) => ({
@ -368,7 +369,8 @@ export class SegmentsController extends Controller {
res: Response,
): Promise<void> {
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) {
res.status(409).send();

View File

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

View File

@ -13,7 +13,7 @@ export interface ISegmentService {
get(id: number): Promise<ISegment>;
getStrategies(id: number): Promise<IFeatureStrategy[]>;
getStrategies(id: number, userId: number): Promise<IFeatureStrategy[]>;
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 { IUnleashConfig } from '../types/option';
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 NameExistsError = require('../error/name-exists-error');
@ -30,6 +32,10 @@ class ContextService {
private logger: Logger;
private flagResolver: IFlagResolver;
private privateProjectChecker: IPrivateProjectChecker;
constructor(
{
projectStore,
@ -43,10 +49,16 @@ class ContextService {
| 'contextFieldStore'
| 'featureStrategiesStore'
>,
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
{
getLogger,
flagResolver,
}: Pick<IUnleashConfig, 'getLogger' | 'flagResolver'>,
privateProjectChecker: IPrivateProjectChecker,
) {
this.privateProjectChecker = privateProjectChecker;
this.projectStore = projectStore;
this.eventStore = eventStore;
this.flagResolver = flagResolver;
this.contextFieldStore = contextFieldStore;
this.featureStrategiesStore = featureStrategiesStore;
this.logger = getLogger('services/context-service.js');
@ -62,9 +74,31 @@ class ContextService {
async getStrategiesByContextField(
name: string,
userId: number,
): Promise<ContextFieldStrategiesSchema> {
const strategies =
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 {
strategies: strategies.map((strategy) => ({
id: strategy.id,

View File

@ -169,7 +169,15 @@ export const createServices = (
config,
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 eventService = new EventService(stores, config);
const featureTypeService = new FeatureTypeService(stores, config);
@ -201,11 +209,9 @@ export const createServices = (
stores,
changeRequestAccessReadModel,
config,
privateProjectChecker,
);
const privateProjectChecker = db
? createPrivateProjectChecker(db, config)
: createFakePrivateProjectChecker();
const clientInstanceService = new ClientInstanceService(
stores,
config,

View File

@ -22,6 +22,7 @@ import BadDataError from '../error/bad-data-error';
import { ISegmentService } from '../segments/segment-service-interface';
import { PermissionError } from '../error';
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 {
private logger: Logger;
@ -38,6 +39,8 @@ export class SegmentService implements ISegmentService {
private flagResolver: IFlagResolver;
private privateProjectChecker: IPrivateProjectChecker;
constructor(
{
segmentStore,
@ -49,11 +52,13 @@ export class SegmentService implements ISegmentService {
>,
changeRequestAccessReadModel: IChangeRequestAccessReadModel,
config: IUnleashConfig,
privateProjectChecker: IPrivateProjectChecker,
) {
this.segmentStore = segmentStore;
this.featureStrategiesStore = featureStrategiesStore;
this.eventStore = eventStore;
this.changeRequestAccessReadModel = changeRequestAccessReadModel;
this.privateProjectChecker = privateProjectChecker;
this.logger = config.getLogger('services/segment-service.ts');
this.flagResolver = config.flagResolver;
this.config = config;
@ -81,8 +86,26 @@ export class SegmentService implements ISegmentService {
}
// Used by unleash-enterprise.
async getStrategies(id: number): Promise<IFeatureStrategy[]> {
return this.featureStrategiesStore.getStrategiesBySegment(id);
async getStrategies(
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(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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