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

refactor: migrate permissions service from another repo (#3163)

This commit is contained in:
Mateusz Kwasniewski 2023-02-20 16:06:10 +01:00 committed by GitHub
parent ec8de3dead
commit bdba449a6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 281 additions and 168 deletions

View File

@ -17,29 +17,20 @@ import { IContextFieldStore, IUnleashStores } from '../types/stores';
import { ISegmentStore } from '../types/stores/segment-store';
import { ExportQuerySchema } from '../openapi/spec/export-query-schema';
import {
CREATE_CONTEXT_FIELD,
CREATE_FEATURE,
CREATE_FEATURE_STRATEGY,
DELETE_FEATURE_STRATEGY,
FEATURES_EXPORTED,
FEATURES_IMPORTED,
IFlagResolver,
IUnleashServices,
UPDATE_FEATURE,
UPDATE_FEATURE_ENVIRONMENT_VARIANTS,
UPDATE_TAG_TYPE,
WithRequired,
} from '../types';
import {
ExportResultSchema,
FeatureStrategySchema,
ImportTogglesValidateItemSchema,
ImportTogglesValidateSchema,
} from '../openapi';
import { ImportTogglesSchema } from '../openapi/spec/import-toggles-schema';
import User from '../types/user';
import { IContextFieldDto } from '../types/stores/context-field-store';
import { BadDataError, InvalidOperationError } from '../error';
import { BadDataError } from '../error';
import { extractUsernameFromUser } from '../util';
import {
AccessService,
@ -51,6 +42,8 @@ import {
} from '../services';
import { isValidField } from './import-context-validation';
import { IImportTogglesStore } from './import-toggles-store-type';
import { ImportPermissionsService } from './import-permissions-service';
import { ImportValidationMessages } from './import-validation-messages';
export default class ExportImportService {
private logger: Logger;
@ -87,6 +80,8 @@ export default class ExportImportService {
private featureTagService: FeatureTagService;
private importPermissionsService: ImportPermissionsService;
constructor(
stores: Pick<
IUnleashStores,
@ -137,6 +132,12 @@ export default class ExportImportService {
this.accessService = accessService;
this.tagTypeService = tagTypeService;
this.featureTagService = featureTagService;
this.importPermissionsService = new ImportPermissionsService(
this.importTogglesStore,
this.accessService,
this.tagTypeService,
this.contextService,
);
this.logger = getLogger('services/state-service.js');
}
@ -157,20 +158,29 @@ export default class ExportImportService {
this.getUnsupportedContextFields(dto),
this.getArchivedFeatures(dto),
this.getOtherProjectFeatures(dto),
this.getMissingPermissions(dto, user),
this.importPermissionsService.getMissingPermissions(
dto,
user,
'regular',
),
]);
const errors = this.compileErrors(
const errors = ImportValidationMessages.compileErrors(
dto.project,
unsupportedStrategies,
otherProjectFeatures,
unsupportedContextFields,
[],
otherProjectFeatures,
false,
);
const warnings = this.compileWarnings(
const warnings = ImportValidationMessages.compileWarnings(
usedCustomStrategies,
archivedFeatures,
);
const permissions = this.compilePermissionErrors(missingPermissions);
const permissions =
ImportValidationMessages.compilePermissionErrors(
missingPermissions,
);
return {
errors,
@ -185,7 +195,11 @@ export default class ExportImportService {
await Promise.all([
this.verifyStrategies(cleanedDto),
this.verifyContextFields(cleanedDto),
this.verifyPermissions(dto, user),
this.importPermissionsService.verifyPermissions(
dto,
user,
'regular',
),
this.verifyFeatures(dto),
]);
await this.createToggles(cleanedDto, user);
@ -359,15 +373,6 @@ export default class ExportImportService {
}
}
private async verifyPermissions(dto: ImportTogglesSchema, user: User) {
const missingPermissions = await this.getMissingPermissions(dto, user);
if (missingPermissions.length > 0) {
throw new InvalidOperationError(
'You are missing permissions to import',
);
}
}
private async verifyFeatures(dto: ImportTogglesSchema) {
const otherProjectFeatures = await this.getOtherProjectFeatures(dto);
if (otherProjectFeatures.length > 0) {
@ -444,75 +449,6 @@ export default class ExportImportService {
}
}
private compileErrors(
projectName: string,
strategies: FeatureStrategySchema[],
otherProjectFeatures: string[],
contextFields?: IContextFieldDto[],
) {
const errors: ImportTogglesValidateItemSchema[] = [];
if (strategies.length > 0) {
errors.push({
message:
'We detected the following custom strategy in the import file that needs to be created first:',
affectedItems: strategies.map((strategy) => strategy.name),
});
}
if (Array.isArray(contextFields) && contextFields.length > 0) {
errors.push({
message:
'We detected the following context fields that do not have matching legal values with the imported ones:',
affectedItems: contextFields.map(
(contextField) => contextField.name,
),
});
}
if (otherProjectFeatures.length > 0) {
errors.push({
message: `You cannot import a features that already exist in other projects. You already have the following features defined outside of project ${projectName}:`,
affectedItems: otherProjectFeatures,
});
}
return errors;
}
private compileWarnings(
usedCustomStrategies: string[],
archivedFeatures: string[],
) {
const warnings: ImportTogglesValidateItemSchema[] = [];
if (usedCustomStrategies.length > 0) {
warnings.push({
message:
'The following strategy types will be used in import. Please make sure the strategy type parameters are configured as in source environment:',
affectedItems: usedCustomStrategies,
});
}
if (archivedFeatures.length > 0) {
warnings.push({
message:
'The following features will not be imported as they are currently archived. To import them, please unarchive them first:',
affectedItems: archivedFeatures,
});
}
return warnings;
}
private compilePermissionErrors(missingPermissions: string[]) {
const errors: ImportTogglesValidateItemSchema[] = [];
if (missingPermissions.length > 0) {
errors.push({
message:
'We detected you are missing the following permissions:',
affectedItems: missingPermissions,
});
}
return errors;
}
private async getUnsupportedStrategies(
dto: ImportTogglesSchema,
): Promise<FeatureStrategySchema[]> {
@ -572,80 +508,6 @@ export default class ExportImportService {
);
}
private async getMissingPermissions(
dto: ImportTogglesSchema,
user: User,
): Promise<string[]> {
const [
newTagTypes,
newContextFields,
strategiesExistForFeatures,
featureEnvsWithVariants,
existingFeatures,
] = await Promise.all([
this.getNewTagTypes(dto),
this.getNewContextFields(dto),
this.importTogglesStore.strategiesExistForFeatures(
dto.data.features.map((feature) => feature.name),
dto.environment,
),
dto.data.featureEnvironments?.filter(
(featureEnvironment) =>
Array.isArray(featureEnvironment.variants) &&
featureEnvironment.variants.length > 0,
) || Promise.resolve([]),
this.importTogglesStore.getExistingFeatures(
dto.data.features.map((feature) => feature.name),
),
]);
const permissions = [UPDATE_FEATURE];
if (newTagTypes.length > 0) {
permissions.push(UPDATE_TAG_TYPE);
}
if (Array.isArray(newContextFields) && newContextFields.length > 0) {
permissions.push(CREATE_CONTEXT_FIELD);
}
if (strategiesExistForFeatures) {
permissions.push(DELETE_FEATURE_STRATEGY);
}
if (dto.data.featureStrategies.length > 0) {
permissions.push(CREATE_FEATURE_STRATEGY);
}
if (featureEnvsWithVariants.length > 0) {
permissions.push(UPDATE_FEATURE_ENVIRONMENT_VARIANTS);
}
if (existingFeatures.length < dto.data.features.length) {
permissions.push(CREATE_FEATURE);
}
const displayPermissions =
await this.importTogglesStore.getDisplayPermissions(permissions);
const results = await Promise.all(
displayPermissions.map((permission) =>
this.accessService
.hasPermission(
user,
permission.name,
dto.project,
dto.environment,
)
.then(
(hasPermission) => [permission, hasPermission] as const,
),
),
);
return results
.filter(([, hasAccess]) => !hasAccess)
.map(([permission]) => permission.displayName);
}
private async getNewTagTypes(dto: ImportTogglesSchema) {
const existingTagTypes = (await this.tagTypeService.getAll()).map(
(tagType) => tagType.name,

View File

@ -0,0 +1,157 @@
import { IImportTogglesStore } from './import-toggles-store-type';
import { AccessService, ContextService, TagTypeService } from '../services';
import { ContextFieldSchema, ImportTogglesSchema } from '../openapi';
import { ITagType } from '../types/stores/tag-type-store';
import User from '../types/user';
import {
CREATE_CONTEXT_FIELD,
CREATE_FEATURE,
CREATE_FEATURE_STRATEGY,
DELETE_FEATURE_STRATEGY,
UPDATE_FEATURE,
UPDATE_FEATURE_ENVIRONMENT_VARIANTS,
UPDATE_TAG_TYPE,
} from '../types';
import { InvalidOperationError } from '../error';
type Mode = 'regular' | 'change_request';
export class ImportPermissionsService {
private importTogglesStore: IImportTogglesStore;
private accessService: AccessService;
private tagTypeService: TagTypeService;
private contextService: ContextService;
private async getNewTagTypes(
dto: ImportTogglesSchema,
): Promise<ITagType[]> {
const existingTagTypes = (await this.tagTypeService.getAll()).map(
(tagType) => tagType.name,
);
const newTagTypes = dto.data.tagTypes?.filter(
(tagType) => !existingTagTypes.includes(tagType.name),
);
return [
...new Map(newTagTypes.map((item) => [item.name, item])).values(),
];
}
private async getNewContextFields(
dto: ImportTogglesSchema,
): Promise<ContextFieldSchema[]> {
const availableContextFields = await this.contextService.getAll();
return dto.data.contextFields?.filter(
(contextField) =>
!availableContextFields.some(
(availableField) =>
availableField.name === contextField.name,
),
);
}
constructor(
importTogglesStore: IImportTogglesStore,
accessService: AccessService,
tagTypeService: TagTypeService,
contextService: ContextService,
) {
this.importTogglesStore = importTogglesStore;
this.accessService = accessService;
this.tagTypeService = tagTypeService;
this.contextService = contextService;
}
async getMissingPermissions(
dto: ImportTogglesSchema,
user: User,
mode: Mode,
): Promise<string[]> {
const [
newTagTypes,
newContextFields,
strategiesExistForFeatures,
featureEnvsWithVariants,
existingFeatures,
] = await Promise.all([
this.getNewTagTypes(dto),
this.getNewContextFields(dto),
this.importTogglesStore.strategiesExistForFeatures(
dto.data.features.map((feature) => feature.name),
dto.environment,
),
dto.data.featureEnvironments?.filter(
(featureEnvironment) =>
Array.isArray(featureEnvironment.variants) &&
featureEnvironment.variants.length > 0,
) || Promise.resolve([]),
this.importTogglesStore.getExistingFeatures(
dto.data.features.map((feature) => feature.name),
),
]);
const permissions = [UPDATE_FEATURE];
if (newTagTypes.length > 0) {
permissions.push(UPDATE_TAG_TYPE);
}
if (Array.isArray(newContextFields) && newContextFields.length > 0) {
permissions.push(CREATE_CONTEXT_FIELD);
}
if (strategiesExistForFeatures && mode === 'regular') {
permissions.push(DELETE_FEATURE_STRATEGY);
}
if (dto.data.featureStrategies.length > 0 && mode === 'regular') {
permissions.push(CREATE_FEATURE_STRATEGY);
}
if (featureEnvsWithVariants.length > 0 && mode === 'regular') {
permissions.push(UPDATE_FEATURE_ENVIRONMENT_VARIANTS);
}
if (existingFeatures.length < dto.data.features.length) {
permissions.push(CREATE_FEATURE);
}
const displayPermissions =
await this.importTogglesStore.getDisplayPermissions(permissions);
const results = await Promise.all(
displayPermissions.map((permission) =>
this.accessService
.hasPermission(
user,
permission.name,
dto.project,
dto.environment,
)
.then(
(hasPermission) => [permission, hasPermission] as const,
),
),
);
return results
.filter(([, hasAccess]) => !hasAccess)
.map(([permission]) => permission.displayName);
}
async verifyPermissions(
dto: ImportTogglesSchema,
user: User,
mode: Mode,
): Promise<void> {
const missingPermissions = await this.getMissingPermissions(
dto,
user,
mode,
);
if (missingPermissions.length > 0) {
throw new InvalidOperationError(
'You are missing permissions to import',
);
}
}
}

View File

@ -0,0 +1,94 @@
import {
FeatureStrategySchema,
ImportTogglesValidateItemSchema,
} from '../openapi';
import { IContextFieldDto } from '../types/stores/context-field-store';
export class ImportValidationMessages {
static compilePermissionErrors(
missingPermissions: string[],
): ImportTogglesValidateItemSchema[] {
const errors: ImportTogglesValidateItemSchema[] = [];
if (missingPermissions.length > 0) {
errors.push({
message:
'We detected you are missing the following permissions:',
affectedItems: missingPermissions,
});
}
return errors;
}
static compileErrors(
projectName: string,
strategies: FeatureStrategySchema[],
contextFields: IContextFieldDto[],
segments: string[],
otherProjectFeatures: string[],
changeRequestExists: boolean,
): ImportTogglesValidateItemSchema[] {
const errors: ImportTogglesValidateItemSchema[] = [];
if (strategies.length > 0) {
errors.push({
message:
'We detected the following custom strategy in the import file that needs to be created first:',
affectedItems: strategies.map((strategy) => strategy.name),
});
}
if (contextFields.length > 0) {
errors.push({
message:
'We detected the following context fields that do not have matching legal values with the imported ones:',
affectedItems: contextFields.map(
(contextField) => contextField.name,
),
});
}
if (segments.length > 0) {
errors.push({
message:
'We detected the following segments in the import file that need to be created first:',
affectedItems: segments,
});
}
if (changeRequestExists) {
errors.push({
message:
'Before importing any data, please resolve your pending change request in this project and environment as it is preventing you from importing at this time',
affectedItems: [],
});
}
if (otherProjectFeatures.length > 0) {
errors.push({
message: `You cannot import a features that already exist in other projects. You already have the following features defined outside of project ${projectName}:`,
affectedItems: otherProjectFeatures,
});
}
return errors;
}
static compileWarnings(
usedCustomStrategies: string[],
archivedFeatures: string[],
): ImportTogglesValidateItemSchema[] {
const warnings: ImportTogglesValidateItemSchema[] = [];
if (usedCustomStrategies.length > 0) {
warnings.push({
message:
'The following strategy types will be used in import. Please make sure the strategy type parameters are configured as in source environment:',
affectedItems: usedCustomStrategies,
});
}
if (archivedFeatures.length > 0) {
warnings.push({
message:
'The following features will not be imported as they are currently archived. To import them, please unarchive them first:',
affectedItems: archivedFeatures,
});
}
return warnings;
}
}