Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add project acl methods and import-project e2e test #996

Merged
merged 3 commits into from
Apr 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { ScenarioLockResultPlural } from '@marxan-api/modules/access-control/sce

export abstract class ProjectAccessControl {
abstract canCreateProject(userId: string): Promise<Permit>;
abstract canImportProject(userId: string): Promise<Permit>;
abstract canCloneProject(userId: string): Promise<Permit>;
abstract canEditProject(userId: string, projectId: string): Promise<Permit>;
abstract canViewProject(userId: string, projectId: string): Promise<Permit>;
abstract canPublishProject(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,24 @@ export class ProjectAclService implements ProjectAccessControl {
return true;
}

// TODO: this will be changed in the following release of user requirements.
// For now, anyone should be able to import projects, regardless of having
// roles or not. In the future project import will be limited to
// organization contributors and organization owners, so this logic will be moved to the access
// control module
async canImportProject(_userId: string): Promise<Permit> {
return true;
}

// TODO: this will be changed in the following release of user requirements.
// For now, anyone should be able to clone projects, regardless of having
// roles or not. In the future clonning a project will be limited to
// organization contributors and organization owners, so this logic will be moved to the access
// control module
async canCloneProject(_userId: string): Promise<Permit> {
return true;
}

async canEditProject(userId: string, projectId: string): Promise<Permit> {
return this.doesUserHaveRole(
await this.getRolesWithinProjectForUser(userId, projectId),
Expand Down
2 changes: 1 addition & 1 deletion api/apps/api/src/modules/projects/projects.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -769,7 +769,7 @@ export class ProjectsController {
return result.right;
}

@IsMissingAclImplementation()
@ImplementsAcl()
@Post('import')
@ApiOkResponse({ type: RequestProjectImportResponseDto })
@ApiConsumes('multipart/form-data')
Expand Down
9 changes: 6 additions & 3 deletions api/apps/api/src/modules/projects/projects.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,11 +357,14 @@ export class ProjectsService {
userId,
projectId,
);

if (!cloning && !canExportProject) return left(forbiddenError);

const canCloneProject =
!cloning ||
(cloning && (await this.projectAclService.canCreateProject(userId)));
(cloning && (await this.projectAclService.canCloneProject(userId)));

if (!canExportProject || !canCloneProject) return left(forbiddenError);
if (!canCloneProject) return left(forbiddenError);

const res = await this.commandBus.execute(
new ExportProject(
Expand Down Expand Up @@ -503,7 +506,7 @@ export class ProjectsService {
new UploadExportFile(exportFile),
);

if (!(await this.projectAclService.canCreateProject(userId))) {
if (!(await this.projectAclService.canImportProject(userId))) {
return left(forbiddenError);
}

Expand Down
155 changes: 155 additions & 0 deletions api/apps/api/test/project/import-project.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { ApiEventsService } from '@marxan-api/modules/api-events';
import { ExportEntity } from '@marxan-api/modules/clone/export/adapters/entities/exports.api.entity';
import { ImportEntity } from '@marxan-api/modules/clone/import/adapters/entities/imports.api.entity';
import { CompleteImportPiece } from '@marxan-api/modules/clone/import/application/complete-import-piece.command';
import { ImportRepository } from '@marxan-api/modules/clone/import/application/import.repository.port';
import {
AllPiecesImported,
ImportId,
} from '@marxan-api/modules/clone/import/domain';
import { API_EVENT_KINDS } from '@marxan/api-events';
import { ClonePiece, ComponentId, ResourceKind } from '@marxan/cloning/domain';
import { ClonePieceUrisResolver } from '@marxan/cloning/infrastructure/clone-piece-data';
import {
exportVersion,
ProjectExportConfigContent,
} from '@marxan/cloning/infrastructure/clone-piece-data/export-config';
import { FileRepository } from '@marxan/files-repository';
import { FixtureType } from '@marxan/utils/tests/fixture-type';
import { CommandBus, CqrsModule } from '@nestjs/cqrs';
import * as archiver from 'archiver';
import { isLeft } from 'fp-ts/lib/Either';
import * as request from 'supertest';
import { Connection } from 'typeorm';
import { v4 } from 'uuid';
import { GivenUserIsLoggedIn } from '../steps/given-user-is-logged-in';
import { bootstrapApplication } from '../utils/api-application';
import { EventBusTestUtils } from '../utils/event-bus.test.utils';
import { ApiEventByTopicAndKind } from '@marxan-api/modules/api-events/api-event.topic+kind.api.entity';

let fixtures: FixtureType<typeof getFixtures>;

beforeEach(async () => {
fixtures = await getFixtures();
}, 10_000);

afterEach(async () => {
await fixtures?.cleanup();
});

test('should permit importing project ', async () => {
await fixtures.GivenImportFile();
await fixtures.GivenImportWasRequested();

await fixtures.WhenProjectIsImported();

await fixtures.ThenImportIsCompleted();
});

export const getFixtures = async () => {
const app = await bootstrapApplication([CqrsModule], [EventBusTestUtils]);
const eventBusTestUtils = app.get(EventBusTestUtils);
eventBusTestUtils.startInspectingEvents();
const commandBus = app.get(CommandBus);
const importRepo = app.get(ImportRepository);
const apiEvents = app.get(ApiEventsService);
const fileRepository = app.get(FileRepository);

const ownerToken = await GivenUserIsLoggedIn(app, 'aa');
const oldProjectId = v4();

let projectId: string;
let importId: ImportId;
let uriZipFile: string;

return {
cleanup: async () => {
const connection = app.get<Connection>(Connection);
const exportRepo = connection.getRepository(ExportEntity);
const importRepo = connection.getRepository(ImportEntity);

await exportRepo.delete({});
await importRepo.delete({});
eventBusTestUtils.stopInspectingEvents();
await app.close();
},
GivenImportFile: async () => {
const exportConfigContent: ProjectExportConfigContent = {
isCloning: false,
version: exportVersion,
name: 'random name',
description: 'random desc',
resourceKind: ResourceKind.Project,
resourceId: oldProjectId,
pieces: {
project: [ClonePiece.ExportConfig, ClonePiece.ProjectMetadata],
scenarios: {},
},
scenarios: [],
};
const [exportConfig] = ClonePieceUrisResolver.resolveFor(
ClonePiece.ExportConfig,
'export location',
);

const zipFile = archiver(`zip`, {
zlib: { level: 9 },
});
zipFile.append(JSON.stringify(exportConfigContent), {
name: exportConfig.relativePath,
});

await zipFile.finalize();

const saveZipFileOrError = await fileRepository.save(zipFile);

if (isLeft(saveZipFileOrError)) throw new Error('could not save zip');

uriZipFile = saveZipFileOrError.right;
},
GivenImportWasRequested: async () => {
const response = await request(app.getHttpServer())
.post(`/api/v1/projects/import`)
.set('Authorization', `Bearer ${ownerToken}`)
.attach('file', uriZipFile)
.expect(201);

importId = new ImportId(response.body.importId);
projectId = response.body.projectId;

const importInstance = await importRepo.find(importId);

expect(importInstance).toBeDefined();

importInstance!.toSnapshot().importPieces.forEach((piece) => {
commandBus.execute(
new CompleteImportPiece(importId, new ComponentId(piece.id)),
);
});
},
WhenProjectIsImported: async () => {
await eventBusTestUtils.waitUntilEventIsPublished(AllPiecesImported);
},
ThenImportIsCompleted: async () => {
const res = await new Promise<ApiEventByTopicAndKind>(
(resolve, reject) => {
const findApiEventInterval = setInterval(async () => {
try {
const event = await apiEvents.getLatestEventForTopic({
topic: projectId,
kind: API_EVENT_KINDS.project__import__finished__v1__alpha,
});
clearInterval(findApiEventInterval);
resolve(event);
} catch (error) {}
}, 1500);
const apiEventTimeOut = setTimeout(() => {
clearInterval(findApiEventInterval);
reject('Import API event was not found');
}, 6000);
},
);
expect(res!.data?.importId).toEqual(importId.value);
},
};
};