diff --git a/src/logion/controllers/locrequest.controller.ts b/src/logion/controllers/locrequest.controller.ts index aa93e766..5d73d7c9 100644 --- a/src/logion/controllers/locrequest.controller.ts +++ b/src/logion/controllers/locrequest.controller.ts @@ -28,6 +28,7 @@ import { requireLength, AuthenticationService, forbidden, + unauthorized, } from "@logion/rest-api-core"; import { UUID } from "@logion/node-api"; @@ -636,8 +637,13 @@ export class LocRequestController extends ApiController { @SendsResponse() async confirmFile(_body: any, requestId: string, hash: string) { await this.locRequestService.update(requestId, async request => { - await this.locAuthorizationService.ensureContributor(this.request, request); - request.confirmFile(hash); + const contributor = await this.locAuthorizationService.ensureContributor(this.request, request); + const file = request.getFile(hash); + if((file.submitter.type !== "Polkadot" && request.isOwner(contributor)) || accountEquals(file.submitter, contributor)) { + request.confirmFile(hash); + } else { + throw unauthorized("Contributor cannot confirm"); + } }); this.response.sendStatus(204); } @@ -859,8 +865,13 @@ export class LocRequestController extends ApiController { async confirmMetadata(_body: any, requestId: string, name: string) { const decodedName = decodeURIComponent(name); await this.locRequestService.update(requestId, async request => { - await this.locAuthorizationService.ensureContributor(this.request, request); - request.confirmMetadataItem(decodedName); + const contributor = await this.locAuthorizationService.ensureContributor(this.request, request); + const item = request.getMetadataItem(decodedName); + if((item.submitter.type !== "Polkadot" && request.isOwner(contributor)) || accountEquals(item.submitter, contributor)) { + request.confirmMetadataItem(decodedName); + } else { + throw unauthorized("Contributor cannot confirm"); + } }); this.response.sendStatus(204); } diff --git a/src/logion/model/locrequest.model.ts b/src/logion/model/locrequest.model.ts index db4b1751..da35a5b2 100644 --- a/src/logion/model/locrequest.model.ts +++ b/src/logion/model/locrequest.model.ts @@ -142,11 +142,12 @@ export class EmbeddableLifecycle { this.reviewedOn = moment().toDate(); } - confirm() { - if (this.status !== "REVIEW_ACCEPTED" && this.status !== "PUBLISHED") { + confirm(isAcknowledged: boolean) { + const acknowledgedOrPublished = isAcknowledged ? "ACKNOWLEDGED" : "PUBLISHED"; + if (this.status !== "REVIEW_ACCEPTED" && this.status !== acknowledgedOrPublished) { throw badRequest(`Cannot confirm item with status ${ this.status }`); } - this.status = "PUBLISHED"; + this.status = acknowledgedOrPublished; } confirmAcknowledged(acknowledgedOn?: Moment) { @@ -356,28 +357,32 @@ export class LocRequestAggregateRoot { } requestFileReview(hash: string) { - this.mutateFile(hash, item => item.requestReview()); + this.mutateFile(hash, item => item.lifecycle!.requestReview()); } acceptFile(hash: string) { - this.mutateFile(hash, item => item.accept()); + this.mutateFile(hash, item => item.lifecycle!.accept()); } rejectFile(hash: string, reason: string) { - this.mutateFile(hash, item => item.reject(reason)); + this.mutateFile(hash, item => item.lifecycle!.reject(reason)); } confirmFile(hash: string) { - this.mutateFile(hash, item => item.confirm()); + this.mutateFile(hash, item => item.lifecycle!.confirm(item.submitter?.type !== "Polkadot" || this.isOwner(item.submitter.toSupportedAccountId()))); + } + + isOwner(account?: SupportedAccountId) { + return accountEquals(account, { address: this.ownerAddress, type: "Polkadot" }); } confirmFileAcknowledged(hash: string, acknowledgedOn?: Moment) { - this.mutateFile(hash, item => item.confirmAcknowledged(acknowledgedOn)); + this.mutateFile(hash, item => item.lifecycle!.confirmAcknowledged(acknowledgedOn)); } - private mutateFile(hash: string, mutator: (item: EmbeddableLifecycle) => void) { + private mutateFile(hash: string, mutator: (item: LocFile) => void) { const file = this.getFileOrThrow(hash); - mutator(file.lifecycle!); + mutator(file); file._toUpdate = true; } @@ -537,31 +542,31 @@ export class LocRequestAggregateRoot { } requestMetadataItemReview(name: string) { - this.mutateMetadataItem(name, item => item.requestReview()); + this.mutateMetadataItem(name, item => item.lifecycle!.requestReview()); } acceptMetadataItem(name: string) { - this.mutateMetadataItem(name, item => item.accept()); + this.mutateMetadataItem(name, item => item.lifecycle!.accept()); } rejectMetadataItem(name: string, reason: string) { - this.mutateMetadataItem(name, item => item.reject(reason)); + this.mutateMetadataItem(name, item => item.lifecycle!.reject(reason)); } confirmMetadataItem(name: string) { - this.mutateMetadataItem(name, item => item.confirm()); + this.mutateMetadataItem(name, item => item.lifecycle!.confirm(item.submitter?.type !== "Polkadot" || this.isOwner(item.submitter.toSupportedAccountId()))); } confirmMetadataItemAcknowledged(name: string, acknowledgedOn?: Moment) { - this.mutateMetadataItem(name, item => item.confirmAcknowledged(acknowledgedOn)); + this.mutateMetadataItem(name, item => item.lifecycle!.confirmAcknowledged(acknowledgedOn)); } - private mutateMetadataItem(name: string, mutator: (item: EmbeddableLifecycle) => void) { + private mutateMetadataItem(name: string, mutator: (item: LocMetadataItem) => void) { const metadataItem = requireDefined( this.metadataItem(name), () => badRequest(`Metadata Item not found: ${ name }`) ); - mutator(metadataItem.lifecycle!); + mutator(metadataItem); metadataItem._toUpdate = true; } diff --git a/test/unit/controllers/locrequest.controller.items.spec.ts b/test/unit/controllers/locrequest.controller.items.spec.ts index 9f298157..c1a93f63 100644 --- a/test/unit/controllers/locrequest.controller.items.spec.ts +++ b/test/unit/controllers/locrequest.controller.items.spec.ts @@ -4,7 +4,7 @@ import { writeFile } from 'fs/promises'; import { LocRequestController } from "../../../src/logion/controllers/locrequest.controller.js"; import { Container } from "inversify"; import request from "supertest"; -import { BOB, ALICE_ACCOUNT } from "../../helpers/addresses.js"; +import { BOB, ALICE_ACCOUNT, ALICE } from "../../helpers/addresses.js"; import { Mock, It } from "moq.ts"; import { LocRequestAggregateRoot, @@ -185,13 +185,11 @@ describe('LocRequestController - Items -', () => { }); it('confirms a metadata item', async () => { - const locRequest = mockRequestForMetadata(); - const app = setupApp(LocRequestController, (container) => mockModelForAnyItem(container, locRequest, 'NOT_ISSUER')) + const app = setupApp(LocRequestController, (container) => mockModelForConfirmMetadata(container)) const dataName = encodeURIComponent(SOME_DATA_NAME) await request(app) .put(`/api/loc-request/${ REQUEST_ID }/metadata/${ dataName }/confirm`) - .expect(204) - locRequest.verify(instance => instance.confirmMetadataItem(SOME_DATA_NAME)) + .expect(204); }); it('adds a link', async () => { @@ -381,6 +379,7 @@ async function testDeleteFileSuccess(app: Express, locRequest: Mock instance.getFile(SOME_DATA_HASH)).returns({ submitter: { type: "Polkadot", address: ALICE } } as FileDescription); request.setup(instance => instance.confirmFile(SOME_DATA_HASH)).returns(); mockPolkadotIdentityLoc(repository, false); } @@ -463,3 +462,11 @@ function mockModelForAddLink(container: Container, request: Mock instance.getMetadataItem(SOME_DATA_NAME)).returns({ submitter: { type: "Polkadot", address: ALICE } } as MetadataItemDescription); + request.setup(instance => instance.confirmMetadataItem(SOME_DATA_NAME)).returns(); + mockPolkadotIdentityLoc(repository, false); +}