Skip to content

Commit

Permalink
refactor: read model for change request access checking (#3380)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew committed Mar 24, 2023
1 parent 9cdd870 commit ab91322
Show file tree
Hide file tree
Showing 18 changed files with 184 additions and 47 deletions.
15 changes: 0 additions & 15 deletions src/lib/db/access-store.ts
Expand Up @@ -475,19 +475,4 @@ export class AccessStore implements IAccessStore {
[destinationEnvironment, sourceEnvironment],
);
}

async isChangeRequestsEnabled(
project: string,
environment: string,
): Promise<boolean> {
const result = await this.db.raw(
`SELECT EXISTS(SELECT 1
FROM ${T.CHANGE_REQUEST_SETTINGS}
WHERE environment = ?
and project = ?) AS present`,
[environment, project],
);
const { present } = result.rows[0];
return present;
}
}
@@ -0,0 +1,13 @@
import User from '../../types/user';

export interface IChangeRequestAccessReadModel {
canBypassChangeRequest(
project: string,
environment: string,
user?: User,
): Promise<boolean>;
isChangeRequestsEnabled(
project: string,
environment: string,
): Promise<boolean>;
}
@@ -0,0 +1,19 @@
import { Db, IUnleashConfig } from 'lib/server-impl';
import { ChangeRequestAccessReadModel } from './sql-change-request-access-read-model';
import { createAccessService } from '../access/createAccessService';
import { FakeChangeRequestAccessReadModel } from './fake-change-request-access-read-model';
import { IChangeRequestAccessReadModel } from './change-request-access-read-model';

export const createChangeRequestAccessReadModel = (
db: Db,
config: IUnleashConfig,
): IChangeRequestAccessReadModel => {
const accessService = createAccessService(db, config);

return new ChangeRequestAccessReadModel(db, accessService);
};

export const createFakeChangeRequestAccessService =
(): IChangeRequestAccessReadModel => {
return new FakeChangeRequestAccessReadModel();
};
@@ -0,0 +1,22 @@
import { IChangeRequestAccessReadModel } from './change-request-access-read-model';

export class FakeChangeRequestAccessReadModel
implements IChangeRequestAccessReadModel
{
private canBypass: boolean;

private isChangeRequestEnabled: boolean;

constructor(canBypass = true, isChangeRequestEnabled = true) {
this.canBypass = canBypass;
this.isChangeRequestEnabled = isChangeRequestEnabled;
}

public async canBypassChangeRequest(): Promise<boolean> {
return this.canBypass;
}

public async isChangeRequestsEnabled(): Promise<boolean> {
return this.isChangeRequestEnabled;
}
}
@@ -0,0 +1,52 @@
import { SKIP_CHANGE_REQUEST } from '../../types';
import { Db } from '../../db/db';
import { AccessService } from '../../services';
import User from '../../types/user';
import { IChangeRequestAccessReadModel } from './change-request-access-read-model';

export class ChangeRequestAccessReadModel
implements IChangeRequestAccessReadModel
{
private db: Db;

private accessService: AccessService;

constructor(db: Db, accessService: AccessService) {
this.db = db;
this.accessService = accessService;
}

public async canBypassChangeRequest(
project: string,
environment: string,
user?: User,
): Promise<boolean> {
const [canSkipChangeRequest, changeRequestEnabled] = await Promise.all([
user
? this.accessService.hasPermission(
user,
SKIP_CHANGE_REQUEST,
project,
environment,
)
: Promise.resolve(false),
this.isChangeRequestsEnabled(project, environment),
]);
return !(changeRequestEnabled && !canSkipChangeRequest);
}

public async isChangeRequestsEnabled(
project: string,
environment: string,
): Promise<boolean> {
const result = await this.db.raw(
`SELECT EXISTS(SELECT 1
FROM change_request_settings
WHERE environment = ?
and project = ?) AS present`,
[environment, project],
);
const { present } = result.rows[0];
return present;
}
}
11 changes: 11 additions & 0 deletions src/lib/features/feature-toggle/createFeatureToggleService.ts
Expand Up @@ -34,6 +34,10 @@ import FakeAccessStore from '../../../test/fixtures/fake-access-store';
import FakeRoleStore from '../../../test/fixtures/fake-role-store';
import FakeEnvironmentStore from '../../../test/fixtures/fake-environment-store';
import EventStore from '../../db/event-store';
import {
createChangeRequestAccessReadModel,
createFakeChangeRequestAccessService,
} from '../change-request-access-service/createChangeRequestAccessReadModel';

export const createFeatureToggleService = (
db: Db,
Expand Down Expand Up @@ -85,6 +89,10 @@ export const createFeatureToggleService = (
{ segmentStore, featureStrategiesStore, eventStore },
config,
);
const changeRequestAccessReadMode = createChangeRequestAccessReadModel(
db,
config,
);
const featureToggleService = new FeatureToggleService(
{
featureStrategiesStore,
Expand All @@ -99,6 +107,7 @@ export const createFeatureToggleService = (
{ getLogger, flagResolver },
segmentService,
accessService,
changeRequestAccessReadMode,
);
return featureToggleService;
};
Expand Down Expand Up @@ -134,6 +143,7 @@ export const createFakeFeatureToggleService = (
{ segmentStore, featureStrategiesStore, eventStore },
config,
);
const changeRequestAccessReadMode = createFakeChangeRequestAccessService();
const featureToggleService = new FeatureToggleService(
{
featureStrategiesStore,
Expand All @@ -148,6 +158,7 @@ export const createFakeFeatureToggleService = (
{ getLogger, flagResolver },
segmentService,
accessService,
changeRequestAccessReadMode,
);
return featureToggleService;
};
1 change: 1 addition & 0 deletions src/lib/features/index.ts
Expand Up @@ -2,3 +2,4 @@ export * from './access/createAccessService';
export * from './export-import-toggles/createExportImportService';
export * from './feature-toggle/createFeatureToggleService';
export * from './project/createProjectService';
export * from './change-request-access-service/createChangeRequestAccessReadModel';
7 changes: 0 additions & 7 deletions src/lib/services/access-service.ts
Expand Up @@ -550,11 +550,4 @@ export class AccessService {
await this.validateRoleIsUnique(role.name, existingId);
return cleanedRole;
}

async isChangeRequestsEnabled(
project: string,
environment: string,
): Promise<boolean> {
return this.store.isChangeRequestsEnabled(project, environment);
}
}
24 changes: 12 additions & 12 deletions src/lib/services/feature-toggle-service.ts
Expand Up @@ -83,6 +83,7 @@ import NoAccessError from '../error/no-access-error';
import { IFeatureProjectUserParams } from '../routes/admin-api/project/project-features';
import { unique } from '../util/unique';
import { ISegmentService } from 'lib/segments/segment-service-interface';
import { IChangeRequestAccessReadModel } from '../features/change-request-access-service/change-request-access-read-model';

interface IFeatureContext {
featureName: string;
Expand Down Expand Up @@ -130,6 +131,8 @@ class FeatureToggleService {

private flagResolver: IFlagResolver;

private changeRequestAccessReadModel: IChangeRequestAccessReadModel;

constructor(
{
featureStrategiesStore,
Expand Down Expand Up @@ -157,6 +160,7 @@ class FeatureToggleService {
}: Pick<IUnleashConfig, 'getLogger' | 'flagResolver'>,
segmentService: ISegmentService,
accessService: AccessService,
changeRequestAccessReadModel: IChangeRequestAccessReadModel,
) {
this.logger = getLogger('services/feature-toggle-service.ts');
this.featureStrategiesStore = featureStrategiesStore;
Expand All @@ -170,6 +174,7 @@ class FeatureToggleService {
this.segmentService = segmentService;
this.accessService = accessService;
this.flagResolver = flagResolver;
this.changeRequestAccessReadModel = changeRequestAccessReadModel;
}

async validateFeaturesContext(
Expand Down Expand Up @@ -1734,18 +1739,13 @@ class FeatureToggleService {
environment: string,
user?: User,
) {
const [canSkipChangeRequest, changeRequestEnabled] = await Promise.all([
user
? this.accessService.hasPermission(
user,
SKIP_CHANGE_REQUEST,
project,
environment,
)
: Promise.resolve(false),
this.accessService.isChangeRequestsEnabled(project, environment),
]);
if (changeRequestEnabled && !canSkipChangeRequest) {
const canBypass =
await this.changeRequestAccessReadModel.canBypassChangeRequest(
project,
environment,
user,
);
if (!canBypass) {
throw new NoAccessError(SKIP_CHANGE_REQUEST);
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/lib/services/index.ts
Expand Up @@ -52,6 +52,10 @@ import {
createFakeExportImportTogglesService,
} from '../features/export-import-toggles/createExportImportService';
import { Db } from '../db/db';
import {
createChangeRequestAccessReadModel,
createFakeChangeRequestAccessService,
} from '../features/change-request-access-service/createChangeRequestAccessReadModel';

// TODO: will be moved to scheduler feature directory
export const scheduleServices = (
Expand Down Expand Up @@ -149,11 +153,15 @@ export const createServices = (
const healthService = new HealthService(stores, config);
const userFeedbackService = new UserFeedbackService(stores, config);
const segmentService = new SegmentService(stores, config);
const changeRequestAccessReadModel = db
? createChangeRequestAccessReadModel(db, config)
: createFakeChangeRequestAccessService();
const featureToggleServiceV2 = new FeatureToggleService(
stores,
config,
segmentService,
accessService,
changeRequestAccessReadModel,
);
const environmentService = new EnvironmentService(stores, config);
const featureTagService = new FeatureTagService(stores, config);
Expand Down
5 changes: 0 additions & 5 deletions src/lib/types/stores/access-store.ts
Expand Up @@ -138,9 +138,4 @@ export interface IAccessStore extends Store<IRole, number> {
sourceEnvironment: string,
destinationEnvironment: string,
): Promise<void>;

isChangeRequestsEnabled(
project: string,
environment: string,
): Promise<boolean>;
}
6 changes: 6 additions & 0 deletions src/test/e2e/services/access-service.e2e.test.ts
Expand Up @@ -15,6 +15,7 @@ import { ALL_PROJECTS } from '../../../lib/util/constants';
import { SegmentService } from '../../../lib/services/segment-service';
import { GroupService } from '../../../lib/services/group-service';
import { FavoritesService } from '../../../lib/services';
import { ChangeRequestAccessReadModel } from '../../../lib/features/change-request-access-service/sql-change-request-access-read-model';

let db: ITestDb;
let stores: IUnleashStores;
Expand Down Expand Up @@ -216,11 +217,16 @@ beforeAll(async () => {
editorRole = roles.find((r) => r.name === RoleName.EDITOR);
adminRole = roles.find((r) => r.name === RoleName.ADMIN);
readRole = roles.find((r) => r.name === RoleName.VIEWER);
const changeRequestAccessReadModel = new ChangeRequestAccessReadModel(
db.rawDatabase,
accessService,
);
featureToggleService = new FeatureToggleService(
stores,
config,
new SegmentService(stores, config),
accessService,
changeRequestAccessReadModel,
);
favoritesService = new FavoritesService(stores, config);
projectService = new ProjectService(
Expand Down
6 changes: 6 additions & 0 deletions src/test/e2e/services/api-token-service.e2e.test.ts
Expand Up @@ -11,6 +11,7 @@ import { AccessService } from '../../../lib/services/access-service';
import { SegmentService } from '../../../lib/services/segment-service';
import { GroupService } from '../../../lib/services/group-service';
import { FavoritesService } from '../../../lib/services';
import { ChangeRequestAccessReadModel } from '../../../lib/features/change-request-access-service/sql-change-request-access-read-model';

let db;
let stores;
Expand All @@ -26,11 +27,16 @@ beforeAll(async () => {
stores = db.stores;
const groupService = new GroupService(stores, config);
const accessService = new AccessService(stores, config, groupService);
const changeRequestAccessReadModel = new ChangeRequestAccessReadModel(
db.rawDatabase,
accessService,
);
const featureToggleService = new FeatureToggleService(
stores,
config,
new SegmentService(stores, config),
accessService,
changeRequestAccessReadModel,
);
const project = {
id: 'test-project',
Expand Down

0 comments on commit ab91322

Please sign in to comment.