Skip to content

Commit

Permalink
Pin 5687: Verify tenant certified attributes for agreement creation (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
AsterITA authored Dec 5, 2024
1 parent d8c2364 commit 3b514ed
Show file tree
Hide file tree
Showing 13 changed files with 741 additions and 47 deletions.
24 changes: 24 additions & 0 deletions collections/agreement/Verify Certified Attributes.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
meta {
name: Verify Certified Attributes
type: http
seq: 21
}

post {
url: {{host-agreement}}/agreements/verify
body: json
auth: none
}

headers {
Authorization: {{JWT}}
x-correlation-id: {{correlation-id}}
}

body:json {
{
"eserviceId": "d6fe98a1-e4f6-4b86-95ea-3bb75b406e56",
"descriptorId": "d4193900-ddaf-4c91-a336-d7cd2cb9b3a7",
"tenantId": "69e2865e-65ab-4e48-a638-2037a9ee2ee7"
}
}
24 changes: 24 additions & 0 deletions collections/bff/agreements/Verify Certified Attributes.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
meta {
name: Verify Certified Attributes
type: http
seq: 21
}

post {
url: {{host-bff}}/agreements/verify
body: json
auth: none
}

headers {
Authorization: {{JWT}}
x-correlation-id: {{correlation-id}}
}

body:json {
{
"eserviceId": "17f8a7a9-9da2-462e-b327-471ed63d5aa0",
"descriptorId": "d4193900-ddaf-4c91-a336-d7cd2cb9b3a7",
"tenantId": "69e2865e-65ab-4e48-a638-2037a9ee2ee7"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,38 @@ export const assertRequesterIsDelegateConsumer = (
}
};

export const assertRequesterCanCreateAgrementForTenant = async (
{
requesterId,
tenantIdToVerify,
eserviceId,
}: {
requesterId: TenantId;
tenantIdToVerify: TenantId;
eserviceId: EServiceId;
},
readModelService: ReadModelService
): Promise<Promise<void>> => {
const isSameOrganization = requesterId === tenantIdToVerify;

const validDelegation =
await readModelService.getActiveConsumerDelegationByEserviceAndIds({
eserviceId,
// if same organization, there's no delegate, otherwise the requester is the delegate
delegateId: isSameOrganization ? undefined : requesterId,
// if same organization, we have to check that it is not a delegator, otherwise tenantIdToVerify is the delegator
delegatorId: isSameOrganization ? requesterId : tenantIdToVerify,
});

const isAuthorized =
(isSameOrganization && !validDelegation) ||
(!isSameOrganization && validDelegation);

if (!isAuthorized) {
throw operationNotAllowed(requesterId);
}
};

/* ========= VALIDATIONS ========= */

const validateDescriptorState = (
Expand Down
31 changes: 31 additions & 0 deletions packages/agreement-process/src/routers/AgreementRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
updateAgreementErrorMapper,
upgradeAgreementErrorMapper,
computeAgreementsStateErrorMapper,
verifyTenantCertifiedAttributesErrorMapper,
} from "../utilities/errorMappers.js";
import { makeApiProblem } from "../model/domain/errors.js";

Expand Down Expand Up @@ -689,6 +690,36 @@ const agreementRouter = (
}
);

agreementRouter.post(
"/agreements/verify",
authorizationMiddleware([ADMIN_ROLE]),
async (req, res) => {
const ctx = fromAppContext(req.ctx);

try {
const result = await agreementService.verifyTenantCertifiedAttributes(
{
tenantId: unsafeBrandId<TenantId>(req.body.tenantId),
descriptorId: unsafeBrandId<DescriptorId>(req.body.descriptorId),
eserviceId: unsafeBrandId<EServiceId>(req.body.eserviceId),
},
ctx
);
return res
.status(200)
.send(agreementApi.HasCertifiedAttributes.parse(result));
} catch (error) {
const errorRes = makeApiProblem(
error,
verifyTenantCertifiedAttributesErrorMapper,
ctx.logger,
ctx.correlationId
);
return res.status(errorRes.status).send(errorRes);
}
}
);

return agreementRouter;
};
export default agreementRouter;
65 changes: 47 additions & 18 deletions packages/agreement-process/src/services/agreementService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
DelegationId,
} from "pagopa-interop-models";
import {
certifiedAttributesSatisfied,
declaredAttributesSatisfied,
verifiedAttributesSatisfied,
} from "pagopa-interop-agreement-lifecycle";
Expand Down Expand Up @@ -91,6 +92,7 @@ import {
assertRequesterCanRetrieveConsumerDocuments,
assertCanWorkOnConsumerDocuments,
assertExpectedState,
assertRequesterCanCreateAgrementForTenant,
assertRequesterIsConsumer,
assertRequesterIsDelegateConsumer,
assertSubmittableState,
Expand Down Expand Up @@ -175,12 +177,6 @@ export const retrieveTenant = async (
return tenant;
};

export const retrieveActiveProducerDelegationByEserviceId = async (
eserviceId: EServiceId,
readModelService: ReadModelService
): Promise<Delegation | undefined> =>
await readModelService.getActiveProducerDelegationByEserviceId(eserviceId);

export const retrieveDescriptor = (
descriptorId: DescriptorId,
eservice: EService
Expand Down Expand Up @@ -430,9 +426,8 @@ export function agreementServiceBuilder(
);

const activeProducerDelegation =
await retrieveActiveProducerDelegationByEserviceId(
agreement.data.eserviceId,
readModelService
await readModelService.getActiveProducerDelegationByEserviceId(
agreement.data.eserviceId
);
const delegateProducerId = activeProducerDelegation?.delegateId;

Expand Down Expand Up @@ -807,9 +802,8 @@ export function agreementServiceBuilder(

const agreement = await retrieveAgreement(agreementId, readModelService);
const activeProducerDelegation =
await retrieveActiveProducerDelegationByEserviceId(
agreement.data.eserviceId,
readModelService
await readModelService.getActiveProducerDelegationByEserviceId(
agreement.data.eserviceId
);

const delegateProducerId = activeProducerDelegation?.delegateId;
Expand Down Expand Up @@ -925,9 +919,8 @@ export function agreementServiceBuilder(
readModelService
);
const activeProducerDelegation =
await retrieveActiveProducerDelegationByEserviceId(
agreementToBeRejected.data.eserviceId,
readModelService
await readModelService.getActiveProducerDelegationByEserviceId(
agreementToBeRejected.data.eserviceId
);

assertRequesterCanActAsProducer(
Expand Down Expand Up @@ -1002,9 +995,8 @@ export function agreementServiceBuilder(

const agreement = await retrieveAgreement(agreementId, readModelService);
const activeProducerDelegation =
await retrieveActiveProducerDelegationByEserviceId(
agreement.data.eserviceId,
readModelService
await readModelService.getActiveProducerDelegationByEserviceId(
agreement.data.eserviceId
);

const delegateProducerId = activeProducerDelegation?.delegateId;
Expand Down Expand Up @@ -1204,6 +1196,43 @@ export function agreementServiceBuilder(
await repository.createEvent(event);
}
},
async verifyTenantCertifiedAttributes(
{
tenantId,
descriptorId,
eserviceId,
}: {
tenantId: TenantId;
descriptorId: DescriptorId;
eserviceId: EServiceId;
},
{ logger, authData }: WithLogger<AppContext>
): Promise<agreementApi.HasCertifiedAttributes> {
logger.info(
`Veryfing tenant ${tenantId} has required certified attributes for descriptor ${descriptorId} of eservice ${eserviceId}`
);

await assertRequesterCanCreateAgrementForTenant(
{
requesterId: authData.organizationId,
tenantIdToVerify: tenantId,
eserviceId,
},
readModelService
);
const consumer = await retrieveTenant(tenantId, readModelService);
const eservice = await retrieveEService(eserviceId, readModelService);
const descriptor = retrieveDescriptor(descriptorId, eservice);

return {
hasCertifiedAttributes:
eservice.producerId === consumer.id || // in case the consumer is also the producer, we don't need to check the attributes
certifiedAttributesSatisfied(
descriptor.attributes,
consumer.attributes
),
};
},
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,7 @@ import {
toCreateEventAgreementArchivedByUpgrade,
toCreateEventAgreementUpgraded,
} from "../model/domain/toEvent.js";
import {
createAndCopyDocumentsForClonedAgreement,
retrieveActiveProducerDelegationByEserviceId,
} from "./agreementService.js";
import { createAndCopyDocumentsForClonedAgreement } from "./agreementService.js";
import { createStamp } from "./agreementStampUtils.js";
import { ReadModelService } from "./readModelService.js";
import { ContractBuilder } from "./agreementContractBuilder.js";
Expand Down Expand Up @@ -70,9 +67,8 @@ export async function createUpgradeOrNewDraft({

// If current eservice has an active producer delegation the new contract will be created with the delegation data
const activeProducerDelegation =
await retrieveActiveProducerDelegationByEserviceId(
eservice.id,
readModelService
await readModelService.getActiveProducerDelegationByEserviceId(
eservice.id
);

const stamp =
Expand Down
66 changes: 45 additions & 21 deletions packages/agreement-process/src/services/readModelService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
RemoveDataPrefix,
Metadata,
AttributeCollection,
DelegationCollection,
} from "pagopa-interop-commons";
import {
Agreement,
Expand All @@ -23,6 +24,7 @@ import {
descriptorState,
EServiceId,
AttributeReadmodel,
DelegationReadModel,
TenantId,
genericInternalError,
Delegation,
Expand Down Expand Up @@ -255,6 +257,27 @@ async function getAttribute(
return undefined;
}

async function getDelegation(
delegations: DelegationCollection,
filter: Filter<{ data: DelegationReadModel }>
): Promise<Delegation | undefined> {
const data = await delegations.findOne(filter, {
projection: { data: true },
});
if (data) {
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;
}
return undefined;
}

// eslint-disable-next-line max-params
async function searchTenantsByName(
agreements: AgreementCollection,
Expand Down Expand Up @@ -613,27 +636,11 @@ export function readModelServiceBuilder(
async getActiveProducerDelegationByEserviceId(
eserviceId: EServiceId
): Promise<Delegation | undefined> {
const data = await delegations.findOne(
{
"data.eserviceId": eserviceId,
"data.state": delegationState.active,
"data.kind": delegationKind.delegatedProducer,
},
{ projection: { data: true } }
);

if (!data) {
return undefined;
}
const result = z.object({ data: Delegation }).safeParse(data);
if (!result.success) {
throw genericInternalError(
`Unable to parse delegation item: result ${JSON.stringify(
result
)} - data ${JSON.stringify(data)} `
);
}
return result.data.data;
return getDelegation(delegations, {
"data.eserviceId": eserviceId,
"data.state": delegationState.active,
"data.kind": delegationKind.delegatedProducer,
});
},
async getActiveConsumerDelegationsByEserviceId(
eserviceId: EServiceId
Expand All @@ -659,6 +666,23 @@ export function readModelServiceBuilder(
}
return result.data;
},
async getActiveConsumerDelegationByEserviceAndIds({
eserviceId,
delegatorId,
delegateId,
}: {
eserviceId: EServiceId;
delegatorId: TenantId;
delegateId?: TenantId;
}): Promise<Delegation | undefined> {
return getDelegation(delegations, {
"data.eserviceId": eserviceId,
"data.delegatorId": delegatorId,
"data.delegateId": delegateId,
"data.state": delegationState.active,
"data.kind": delegationKind.delegatedConsumer,
});
},
};
}

Expand Down
13 changes: 13 additions & 0 deletions packages/agreement-process/src/utilities/errorMappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,16 @@ export const computeAgreementsStateErrorMapper = (
match(error.code)
.with("badRequestError", () => HTTP_STATUS_BAD_REQUEST)
.otherwise(() => HTTP_STATUS_INTERNAL_SERVER_ERROR);

export const verifyTenantCertifiedAttributesErrorMapper = (
error: ApiError<ErrorCodes>
): number =>
match(error.code)
.with("tenantNotFound", () => HTTP_STATUS_NOT_FOUND)
.with(
"eServiceNotFound",
"descriptorNotFound",
() => HTTP_STATUS_BAD_REQUEST
)
.with("operationNotAllowed", () => HTTP_STATUS_FORBIDDEN)
.otherwise(() => HTTP_STATUS_INTERNAL_SERVER_ERROR);
Loading

0 comments on commit 3b514ed

Please sign in to comment.