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

PIN-5442 - Update e-service request caller assertions to accept delegates #1099

Open
wants to merge 12 commits into
base: feature/pin-5126-capofila
Choose a base branch
from
199 changes: 156 additions & 43 deletions packages/catalog-process/src/services/catalogService.ts

Large diffs are not rendered by default.

40 changes: 40 additions & 0 deletions packages/catalog-process/src/services/readModelService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import {
EServiceReadModel,
TenantReadModel,
genericInternalError,
Delegation,
DelegationState,
} from "pagopa-interop-models";
import { match } from "ts-pattern";
import { z } from "zod";
Expand Down Expand Up @@ -99,6 +101,7 @@ export function readModelServiceBuilder(
const agreements = readModelRepository.agreements;
const attributes = readModelRepository.attributes;
const tenants = readModelRepository.tenants;
const delegations = readModelRepository.delegations;

return {
async getEServices(
Expand Down Expand Up @@ -498,6 +501,43 @@ export function readModelServiceBuilder(
async getTenantById(id: TenantId): Promise<Tenant | undefined> {
return getTenant(tenants, { "data.id": id });
},

async getLatestDelegation({
eserviceId,
states,
delegateId,
}: {
eserviceId: EServiceId;
states: DelegationState[];
delegateId?: TenantId;
}): Promise<Delegation | undefined> {
const data = await delegations.findOne(
{
"data.eserviceId": eserviceId,
...(states.length > 0 ? { "data.state": { $in: states } } : {}),
...(delegateId ? { "data.delegateId": delegateId } : {}),
},
{
projection: { data: true },
sort: { "data.createdAt": -1 },
}
);

if (!data) {
return undefined;
}
const result = Delegation.safeParse(data.data);

if (!result.success) {
throw genericInternalError(
`Unable to parse delegation item: result ${JSON.stringify(
result
)} - data ${JSON.stringify(data)} `
);
}

return result.data;
},
};
}

Expand Down
48 changes: 43 additions & 5 deletions packages/catalog-process/src/services/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
Tenant,
TenantKind,
Descriptor,
EServiceId,
delegationState,
} from "pagopa-interop-models";
import { catalogApi } from "pagopa-interop-api-clients";
import {
Expand All @@ -24,15 +26,51 @@ import {
eServiceRiskAnalysisIsRequired,
riskAnalysisNotValid,
} from "../model/domain/errors.js";
import { ReadModelService } from "./readModelService.js";

export function assertRequesterAllowed(
export async function assertRequesterIsDelegateOrProducer(
producerId: TenantId,
eserviceId: EServiceId,
authData: AuthData,
readModelService: ReadModelService
): Promise<void> {
if (authData.userRoles.includes("internal")) {
return;
}

const delegation = await readModelService.getLatestDelegation({
eserviceId,
states: [delegationState.active],
});

if (delegation) {
if (authData.organizationId !== delegation.delegateId) {
throw operationForbidden;
}
} else {
assertRequesterIsProducer(producerId, authData);
}
}

export function assertRequesterIsProducer(
producerId: TenantId,
authData: AuthData
): void {
if (
!authData.userRoles.includes("internal") &&
producerId !== authData.organizationId
) {
if (producerId !== authData.organizationId) {
throw operationForbidden;
}
}

export async function assertNoValidDelegationAssociated(
eserviceId: EServiceId,
readModelService: ReadModelService
): Promise<void> {
const delegation = await readModelService.getLatestDelegation({
eserviceId,
states: [delegationState.active, delegationState.waitingForApproval],
});

if (delegation) {
throw operationForbidden;
}
}
Expand Down
91 changes: 90 additions & 1 deletion packages/catalog-process/test/activateDescriptor.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import { genericLogger } from "pagopa-interop-commons";
import { decodeProtobufPayload } from "pagopa-interop-commons-test/index.js";
import {
decodeProtobufPayload,
getMockDelegationProducer,
} from "pagopa-interop-commons-test/index.js";
import {
Descriptor,
descriptorState,
EService,
EServiceDescriptorActivatedV2,
toEServiceV2,
operationForbidden,
Delegation,
delegationState,
} from "pagopa-interop-models";
import { expect, describe, it } from "vitest";
import {
Expand All @@ -23,6 +28,7 @@ import {
getMockEService,
getMockDescriptor,
getMockDocument,
addOneDelegation,
} from "./utils.js";

describe("activate descriptor", () => {
Expand Down Expand Up @@ -70,6 +76,60 @@ describe("activate descriptor", () => {
expect(writtenPayload.descriptorId).toEqual(descriptor.id);
});

it("should write on event-store for the activation of a descriptor (delegate)", async () => {
const descriptor: Descriptor = {
...mockDescriptor,
interface: mockDocument,
state: descriptorState.suspended,
};

const delegate = getMockAuthData();

const eservice: EService = {
...mockEService,
descriptors: [descriptor],
};

const delegation: Delegation = {
...getMockDelegationProducer(),
delegateId: delegate.organizationId,
eserviceId: eservice.id,
state: delegationState.active,
};

await addOneEService(eservice);
await addOneDelegation(delegation);

await catalogService.activateDescriptor(eservice.id, descriptor.id, {
authData: delegate,
correlationId: "",
serviceName: "",
logger: genericLogger,
});

const updatedDescriptor = {
...descriptor,
state: descriptorState.published,
};

const writtenEvent = await readLastEserviceEvent(eservice.id);
expect(writtenEvent.stream_id).toBe(eservice.id);
expect(writtenEvent.version).toBe("1");
expect(writtenEvent.type).toBe("EServiceDescriptorActivated");
expect(writtenEvent.event_version).toBe(2);
const writtenPayload = decodeProtobufPayload({
messageType: EServiceDescriptorActivatedV2,
payload: writtenEvent.data,
});

const expectedEservice = toEServiceV2({
...eservice,
descriptors: [updatedDescriptor],
});
expect(writtenPayload.eservice).toEqual(expectedEservice);
expect(writtenPayload.descriptorId).toEqual(descriptor.id);
});

it("should throw eServiceNotFound if the eservice doesn't exist", () => {
expect(
catalogService.activateDescriptor(mockEService.id, mockDescriptor.id, {
Expand Down Expand Up @@ -121,6 +181,35 @@ describe("activate descriptor", () => {
).rejects.toThrowError(operationForbidden);
});

it("should throw operationForbidden if the requester if the given e-service has been delegated and caller is not the delegate", async () => {
const descriptor: Descriptor = {
...mockDescriptor,
interface: mockDocument,
state: descriptorState.suspended,
};
const eservice: EService = {
...mockEService,
descriptors: [descriptor],
};
const delegation: Delegation = {
...getMockDelegationProducer(),
eserviceId: eservice.id,
state: delegationState.active,
};

await addOneEService(eservice);
await addOneDelegation(delegation);

expect(
catalogService.activateDescriptor(eservice.id, descriptor.id, {
authData: getMockAuthData(eservice.producerId),
correlationId: "",
serviceName: "",
logger: genericLogger,
})
).rejects.toThrowError(operationForbidden);
});

it("should throw notValidDescriptor if the descriptor is in draft state", async () => {
const descriptor: Descriptor = {
...mockDescriptor,
Expand Down
92 changes: 91 additions & 1 deletion packages/catalog-process/test/archiveDescriptor.test.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-floating-promises */
import { genericLogger } from "pagopa-interop-commons";
import { decodeProtobufPayload } from "pagopa-interop-commons-test/index.js";
import {
decodeProtobufPayload,
getMockDelegationProducer,
} from "pagopa-interop-commons-test/index.js";
import {
Descriptor,
descriptorState,
EService,
EServiceDescriptorActivatedV2,
toEServiceV2,
operationForbidden,
delegationState,
Delegation,
} from "pagopa-interop-models";
import { expect, describe, it } from "vitest";
import {
eServiceNotFound,
eServiceDescriptorNotFound,
} from "../src/model/domain/errors.js";
import {
addOneDelegation,
addOneEService,
catalogService,
getMockAuthData,
Expand Down Expand Up @@ -73,6 +79,62 @@ describe("archive descriptor", () => {
expect(writtenPayload.descriptorId).toEqual(descriptor.id);
});

it("should write on event-store for the archiving of a descriptor (delegate)", async () => {
const descriptor: Descriptor = {
...mockDescriptor,
interface: mockDocument,
state: descriptorState.suspended,
};
const delegate = getMockAuthData();

const eservice: EService = {
...mockEService,
descriptors: [descriptor],
};

const delegation: Delegation = {
...getMockDelegationProducer(),
delegateId: delegate.organizationId,
eserviceId: eservice.id,
state: delegationState.active,
};

await addOneEService(eservice);
await addOneDelegation(delegation);

await catalogService.archiveDescriptor(eservice.id, descriptor.id, {
authData: getMockAuthData(delegate.organizationId),
correlationId: "",
serviceName: "",
logger: genericLogger,
});

const writtenEvent = await readLastEserviceEvent(eservice.id);
expect(writtenEvent.stream_id).toBe(eservice.id);
expect(writtenEvent.version).toBe("1");
expect(writtenEvent.type).toBe("EServiceDescriptorArchived");
expect(writtenEvent.event_version).toBe(2);
const writtenPayload = decodeProtobufPayload({
messageType: EServiceDescriptorActivatedV2,
payload: writtenEvent.data,
});

const updatedDescriptor = {
...descriptor,
state: descriptorState.archived,
archivedAt: new Date(
Number(writtenPayload.eservice!.descriptors[0]!.archivedAt)
),
};

const expectedEService = toEServiceV2({
...eservice,
descriptors: [updatedDescriptor],
});
expect(writtenPayload.eservice).toEqual(expectedEService);
expect(writtenPayload.descriptorId).toEqual(descriptor.id);
});

it("should throw eServiceNotFound if the eservice doesn't exist", () => {
expect(
catalogService.archiveDescriptor(mockEService.id, mockDescriptor.id, {
Expand Down Expand Up @@ -122,4 +184,32 @@ describe("archive descriptor", () => {
})
).rejects.toThrowError(operationForbidden);
});

it("should throw operationForbidden if the requester if the given e-service has been delegated and caller is not the delegate", async () => {
const descriptor: Descriptor = {
...mockDescriptor,
state: descriptorState.draft,
};
const eservice: EService = {
...mockEService,
descriptors: [descriptor],
};
const delegation: Delegation = {
...getMockDelegationProducer(),
eserviceId: eservice.id,
state: delegationState.active,
};

await addOneEService(eservice);
await addOneDelegation(delegation);

expect(
catalogService.archiveDescriptor(eservice.id, descriptor.id, {
authData: getMockAuthData(eservice.producerId),
correlationId: "",
serviceName: "",
logger: genericLogger,
})
).rejects.toThrowError(operationForbidden);
});
});
Loading