From 97ebbb0c24aafd60f636fe9dd0600a56f0f6802d Mon Sep 17 00:00:00 2001 From: nttdata-rtorsoli Date: Tue, 16 Jan 2024 10:13:24 +0100 Subject: [PATCH] PIN-4371 BKE - Tenant process added route to revoke certified attribute --- .../resources/interface-specification.yml | 37 ++++ .../api/impl/TenantApiServiceImpl.scala | 71 +++++++- .../error/ResponseHandlers.scala | 12 ++ .../error/TenantProcessErrors.scala | 10 +- src/test/resources/authz.json | 9 +- .../authz/TenantApiServiceAuthzSpec.scala | 9 + .../provider/CertifiedAttributeSpec.scala | 169 ++++++++++++++++-- 7 files changed, 295 insertions(+), 22 deletions(-) diff --git a/src/main/resources/interface-specification.yml b/src/main/resources/interface-specification.yml index cf825f9..6c0e774 100644 --- a/src/main/resources/interface-specification.yml +++ b/src/main/resources/interface-specification.yml @@ -483,6 +483,43 @@ paths: application/problem+json: schema: $ref: '#/components/schemas/Problem' + /tenants/{tenantId}/attributes/certified/{attributeId}: + parameters: + - $ref: '#/components/parameters/CorrelationIdHeader' + - name: tenantId + in: path + description: Tenant id + required: true + schema: + type: string + format: uuid + - name: attributeId + in: path + description: Attribute id + required: true + schema: + type: string + format: uuid + delete: + tags: + - tenant + operationId: revokeCertifiedAttributeById + description: Revoke a certified attribute to a Tenant by the requester Tenant + responses: + '204': + description: Certified Attribute revoked + '403': + description: Forbidden + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + '404': + description: Tenant Not Found + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' /tenants/attributes/declared: parameters: - $ref: '#/components/parameters/CorrelationIdHeader' diff --git a/src/main/scala/it/pagopa/interop/tenantprocess/api/impl/TenantApiServiceImpl.scala b/src/main/scala/it/pagopa/interop/tenantprocess/api/impl/TenantApiServiceImpl.scala index 6be3019..f67fbe5 100644 --- a/src/main/scala/it/pagopa/interop/tenantprocess/api/impl/TenantApiServiceImpl.scala +++ b/src/main/scala/it/pagopa/interop/tenantprocess/api/impl/TenantApiServiceImpl.scala @@ -426,6 +426,72 @@ final case class TenantApiServiceImpl( } } + override def revokeCertifiedAttributeById(tenantId: String, attributeId: String)(implicit + contexts: Seq[(String, String)], + toEntityMarshallerProblem: ToEntityMarshaller[Problem] + ): Route = authorize(ADMIN_ROLE) { + val operationLabel = s"Revoke certified attribute ${attributeId} to tenant $tenantId" + logger.info(operationLabel) + + val now: OffsetDateTime = dateTimeSupplier.get() + + val result: Future[Unit] = for { + requesterTenantUuid <- getOrganizationIdFutureUUID(contexts) + targetTenantUuid <- tenantId.toFutureUUID + attributeUuid <- attributeId.toFutureUUID + requesterTenant <- tenantManagementService.getTenantById(requesterTenantUuid).map(_.toManagement) + _ <- requesterTenant.features + .collectFirstSome(_.certifier.map(_.certifierId)) + .toFuture(TenantIsNotACertifier(requesterTenantUuid)) + attribute <- attributeRegistryManagementService + .getAttributeById(attributeUuid) + _ <- attribute.kind match { + case Certified => Future.unit + case _ => Future.failed(RegistryAttributeIdNotFound(attribute.id)) + } + targetTenant <- tenantManagementService.getTenantById(targetTenantUuid).map(_.toManagement) + attributeToRevoke <- targetTenant.attributes + .mapFilter(_.certified) + .find(_.id == attributeUuid) + .toFuture( + CertifiedAttributeNotFoundInTenant( + targetTenant.id, + attribute.id, + attribute.origin.getOrElse("none"), + attribute.code.getOrElse("none") + ) + ) + revokedAttribute = attributeToRevoke.copy(revocationTimestamp = now.some) + updatedTenant <- tenantManagementService + .updateTenantAttribute( + targetTenant.id, + attributeToRevoke.id, + DependencyTenantAttribute(certified = revokedAttribute.some) + ) + tenantKind <- getTenantKindLoadingCertifiedAttributes(updatedTenant.attributes, updatedTenant.externalId) + updatedTenant <- updatedTenant.kind match { + case Some(x) if (x == tenantKind) => Future.successful(updatedTenant) + case _ => + tenantManagementService.updateTenant( + updatedTenant.id, + DependencyTenantDelta( + selfcareId = updatedTenant.selfcareId, + features = updatedTenant.features, + kind = tenantKind + ) + ) + } + _ <- agreementProcessService.computeAgreementsByAttribute( + attributeUuid, + CompactTenant(updatedTenant.id, updatedTenant.attributes.map(_.toAgreementApi)) + ) + } yield () + + onComplete(result) { + revokeCertifiedAttributeByIdResponse[Unit](operationLabel)(_ => revokeCertifiedAttributeById204) + } + } + override def verifyVerifiedAttribute(tenantId: String, seed: VerifiedTenantAttributeSeed)(implicit contexts: Seq[(String, String)], toEntityMarshallerProblem: ToEntityMarshaller[Problem], @@ -723,7 +789,9 @@ final case class TenantApiServiceImpl( attributeToModify <- tenantToModify.attributes .mapFilter(_.certified) .find(_.id == attributeIdToRevoke) - .toFuture(CertifiedAttributeNotFoundInTenant(tenantToModify.id, attributeOrigin, attributeExternalId)) + .toFuture( + CertifiedAttributeNotFoundInTenant(tenantToModify.id, attributeIdToRevoke, attributeOrigin, attributeExternalId) + ) modifiedAttribute = attributeToModify.copy(revocationTimestamp = dateTimeSupplier.get().some) updatedTenant <- tenantManagementService .updateTenantAttribute( @@ -775,7 +843,6 @@ final case class TenantApiServiceImpl( attributeToAssign.toCertifiedSeed(now) ) ) - tenantKind <- getTenantKindLoadingCertifiedAttributes(updatedTenant.attributes, updatedTenant.externalId) updatedTenant <- updatedTenant.kind match { case Some(x) if (x == tenantKind) => Future.successful(updatedTenant) diff --git a/src/main/scala/it/pagopa/interop/tenantprocess/error/ResponseHandlers.scala b/src/main/scala/it/pagopa/interop/tenantprocess/error/ResponseHandlers.scala index c253abb..2b24072 100644 --- a/src/main/scala/it/pagopa/interop/tenantprocess/error/ResponseHandlers.scala +++ b/src/main/scala/it/pagopa/interop/tenantprocess/error/ResponseHandlers.scala @@ -119,6 +119,18 @@ object ResponseHandlers extends AkkaResponses { case Failure(ex) => internalServerError(ex, logMessage) } + def revokeCertifiedAttributeByIdResponse[T](logMessage: String)( + success: T => Route + )(result: Try[T])(implicit contexts: Seq[(String, String)], logger: LoggerTakingImplicit[ContextFieldsToLog]): Route = + result match { + case Success(s) => success(s) + case Failure(ex: CertifiedAttributeNotFoundInTenant) => badRequest(ex, logMessage) + case Failure(ex: TenantIsNotACertifier) => forbidden(ex, logMessage) + case Failure(ex: TenantByIdNotFound) => notFound(ex, logMessage) + case Failure(ex: CertifiedAttributeAlreadyExists) => conflict(ex, logMessage) + case Failure(ex) => internalServerError(ex, logMessage) + } + def verifyVerifiedAttributeResponse[T](logMessage: String)( success: T => Route )(result: Try[T])(implicit contexts: Seq[(String, String)], logger: LoggerTakingImplicit[ContextFieldsToLog]): Route = diff --git a/src/main/scala/it/pagopa/interop/tenantprocess/error/TenantProcessErrors.scala b/src/main/scala/it/pagopa/interop/tenantprocess/error/TenantProcessErrors.scala index bb0b995..abb7850 100644 --- a/src/main/scala/it/pagopa/interop/tenantprocess/error/TenantProcessErrors.scala +++ b/src/main/scala/it/pagopa/interop/tenantprocess/error/TenantProcessErrors.scala @@ -50,10 +50,14 @@ object TenantProcessErrors { final case class TenantByIdNotFound(tenantId: UUID) extends ComponentError("0011", s"Tenant $tenantId not found") - final case class CertifiedAttributeNotFoundInTenant(tenantId: UUID, attributeOrigin: String, attributeCode: String) - extends ComponentError( + final case class CertifiedAttributeNotFoundInTenant( + tenantId: UUID, + attributeId: UUID, + attributeOrigin: String, + attributeCode: String + ) extends ComponentError( "0012", - s"Certified Attribute ($attributeOrigin, $attributeCode) not found in tenant $tenantId" + s"Certified Attribute $attributeId ($attributeOrigin, $attributeCode) not found in tenant $tenantId" ) final case class DeclaredAttributeNotFoundInTenant(tenantId: UUID, attributeId: UUID) diff --git a/src/test/resources/authz.json b/src/test/resources/authz.json index de3a11c..49ec267 100644 --- a/src/test/resources/authz.json +++ b/src/test/resources/authz.json @@ -171,6 +171,13 @@ "roles": [ "admin" ] - } + }, + { + "route": "revokeCertifiedAttributeById", + "verb": "DELETE", + "roles": [ + "admin" + ] + } ] } \ No newline at end of file diff --git a/src/test/scala/it/pagopa/interop/tenantprocess/authz/TenantApiServiceAuthzSpec.scala b/src/test/scala/it/pagopa/interop/tenantprocess/authz/TenantApiServiceAuthzSpec.scala index eb62ee7..4590d1f 100644 --- a/src/test/scala/it/pagopa/interop/tenantprocess/authz/TenantApiServiceAuthzSpec.scala +++ b/src/test/scala/it/pagopa/interop/tenantprocess/authz/TenantApiServiceAuthzSpec.scala @@ -214,4 +214,13 @@ class TenantApiServiceAuthzSpec extends ClusteredMUnitRouteTest with SpecData { } ) } + + test("Tenant api should accept authorized roles for revokeCertifiedAttributeById") { + validateAuthorization( + endpoints("revokeCertifiedAttributeById"), + { implicit c: Seq[(String, String)] => + tenantService.revokeCertifiedAttributeById(UUID.randomUUID().toString, UUID.randomUUID().toString) + } + ) + } } diff --git a/src/test/scala/it/pagopa/interop/tenantprocess/provider/CertifiedAttributeSpec.scala b/src/test/scala/it/pagopa/interop/tenantprocess/provider/CertifiedAttributeSpec.scala index 0708dee..e74290e 100644 --- a/src/test/scala/it/pagopa/interop/tenantprocess/provider/CertifiedAttributeSpec.scala +++ b/src/test/scala/it/pagopa/interop/tenantprocess/provider/CertifiedAttributeSpec.scala @@ -19,7 +19,7 @@ class CertifiedAttributeSpec extends AnyWordSpecLike with SpecHelper with Scalat "succeed" in { implicit val context: Seq[(String, String)] = adminContext - val tenantUuid = UUID.randomUUID() + val tenantId = UUID.randomUUID() val attributeId = UUID.randomUUID() val seed = CertifiedTenantAttributeSeed(attributeId) val managementSeed = Dependency.TenantAttribute( @@ -36,18 +36,18 @@ class CertifiedAttributeSpec extends AnyWordSpecLike with SpecHelper with Scalat ) val tenant = persistentTenant.copy( - id = tenantUuid, + id = tenantId, attributes = List(persistentCertifiedAttribute, persistentDeclaredAttribute, persistentVerifiedAttribute) ) mockDateTimeGet() mockGetTenantById(organizationId, requester) - mockGetTenantById(tenantUuid, tenant) + mockGetTenantById(tenantId, tenant) mockGetAttributeById(seed.id, persistentAttribute.copy(id = seed.id)) - mockAddTenantAttribute(tenantUuid, managementSeed) - mockComputeAgreementState(attributeId, CompactTenant(tenantUuid, Nil)) + mockAddTenantAttribute(tenantId, managementSeed) + mockComputeAgreementState(attributeId, CompactTenant(tenantId, Nil)) - Post() ~> tenantService.addCertifiedAttribute(tenantUuid.toString, seed) ~> check { + Post() ~> tenantService.addCertifiedAttribute(tenantId.toString, seed) ~> check { assert(status == StatusCodes.OK) } } @@ -55,7 +55,7 @@ class CertifiedAttributeSpec extends AnyWordSpecLike with SpecHelper with Scalat "fail if requester is not a certifier" in { implicit val context: Seq[(String, String)] = adminContext - val tenantUuid = UUID.randomUUID() + val tenantId = UUID.randomUUID() val attributeId = UUID.randomUUID() val seed = CertifiedTenantAttributeSeed(attributeId) @@ -64,7 +64,7 @@ class CertifiedAttributeSpec extends AnyWordSpecLike with SpecHelper with Scalat mockDateTimeGet() mockGetTenantById(organizationId, requester) - Post() ~> tenantService.addCertifiedAttribute(tenantUuid.toString, seed) ~> check { + Post() ~> tenantService.addCertifiedAttribute(tenantId.toString, seed) ~> check { assert(status == StatusCodes.Forbidden) } } @@ -72,7 +72,7 @@ class CertifiedAttributeSpec extends AnyWordSpecLike with SpecHelper with Scalat "fail if attribute does not exists" in { implicit val context: Seq[(String, String)] = adminContext - val tenantUuid = UUID.randomUUID() + val tenantId = UUID.randomUUID() val attributeId = UUID.randomUUID() val seed = CertifiedTenantAttributeSeed(attributeId) @@ -85,7 +85,7 @@ class CertifiedAttributeSpec extends AnyWordSpecLike with SpecHelper with Scalat mockGetTenantById(organizationId, requester) mockGetAttributeByIdNotFound(seed.id) - Post() ~> tenantService.addCertifiedAttribute(tenantUuid.toString, seed) ~> check { + Post() ~> tenantService.addCertifiedAttribute(tenantId.toString, seed) ~> check { assert(status == StatusCodes.InternalServerError) } } @@ -93,7 +93,7 @@ class CertifiedAttributeSpec extends AnyWordSpecLike with SpecHelper with Scalat "fail if attribute exists but is not certified" in { implicit val context: Seq[(String, String)] = adminContext - val tenantUuid = UUID.randomUUID() + val tenantId = UUID.randomUUID() val attributeId = UUID.randomUUID() val seed = CertifiedTenantAttributeSeed(attributeId) @@ -106,7 +106,7 @@ class CertifiedAttributeSpec extends AnyWordSpecLike with SpecHelper with Scalat mockGetTenantById(organizationId, requester) mockGetAttributeById(seed.id, persistentAttribute.copy(id = seed.id, kind = Declared)) - Post() ~> tenantService.addCertifiedAttribute(tenantUuid.toString, seed) ~> check { + Post() ~> tenantService.addCertifiedAttribute(tenantId.toString, seed) ~> check { assert(status == StatusCodes.InternalServerError) } } @@ -114,7 +114,7 @@ class CertifiedAttributeSpec extends AnyWordSpecLike with SpecHelper with Scalat "fail if certified tenant attribute already exists" in { implicit val context: Seq[(String, String)] = adminContext - val tenantUuid = UUID.randomUUID() + val tenantId = UUID.randomUUID() val attributeId = UUID.randomUUID() val seed = CertifiedTenantAttributeSeed(attributeId) @@ -124,18 +124,155 @@ class CertifiedAttributeSpec extends AnyWordSpecLike with SpecHelper with Scalat ) val tenant = persistentTenant.copy( - id = tenantUuid, + id = tenantId, attributes = List(persistentCertifiedAttribute.copy(id = seed.id), persistentDeclaredAttribute, persistentVerifiedAttribute) ) mockDateTimeGet() mockGetTenantById(organizationId, requester) - mockGetTenantById(tenantUuid, tenant) + mockGetTenantById(tenantId, tenant) mockGetAttributeById(seed.id, persistentAttribute.copy(id = seed.id)) - Post() ~> tenantService.addCertifiedAttribute(tenantUuid.toString, seed) ~> check { + Post() ~> tenantService.addCertifiedAttribute(tenantId.toString, seed) ~> check { assert(status == StatusCodes.Conflict) } } + + "Certified attribute revoke" should { + "succeed" in { + implicit val context: Seq[(String, String)] = adminContext + + val tenantId = UUID.randomUUID() + val attributeId = UUID.randomUUID() + + val requester = persistentTenant.copy( + id = organizationId, + features = List(PersistentTenantFeature.PersistentCertifier("certifier")) + ) + + val tenant = persistentTenant.copy( + id = tenantId, + attributes = List( + persistentCertifiedAttribute.copy(id = attributeId), + persistentDeclaredAttribute, + persistentVerifiedAttribute + ) + ) + + val managementSeed = Dependency.TenantAttribute( + declared = None, + certified = Some( + Dependency.CertifiedTenantAttribute( + attributeId, + assignmentTimestamp = timestamp, + revocationTimestamp = Some(timestamp) + ) + ), + verified = None + ) + + mockDateTimeGet() + mockGetTenantById(organizationId, requester) + mockGetTenantById(tenantId, tenant) + mockGetAttributeById(attributeId, persistentAttribute.copy(id = attributeId)) + mockUpdateTenantAttribute(tenantId, attributeId, managementSeed) + mockUpdateTenant( + tenantId, + Dependency.TenantDelta( + selfcareId = None, + features = Nil, + kind = Dependency.TenantKind.PA, + onboardedAt = None, + subUnitType = None + ) + ) + + mockComputeAgreementState(attributeId, CompactTenant(tenantId, Nil)) + + Delete() ~> tenantService.revokeCertifiedAttributeById(tenantId.toString, attributeId.toString) ~> check { + assert(status == StatusCodes.NoContent) + } + } + } + "revoke fail if requester is not a certifier" in { + implicit val context: Seq[(String, String)] = adminContext + + val tenantId = UUID.randomUUID() + val attributeId = UUID.randomUUID() + + val requester = persistentTenant.copy(id = organizationId) + + mockDateTimeGet() + mockGetTenantById(organizationId, requester) + + Delete() ~> tenantService.revokeCertifiedAttributeById(tenantId.toString, attributeId.toString) ~> check { + assert(status == StatusCodes.Forbidden) + } + } + + "revoke fail if attribute does not exists" in { + implicit val context: Seq[(String, String)] = adminContext + + val tenantId = UUID.randomUUID() + val attributeId = UUID.randomUUID() + + val requester = persistentTenant.copy( + id = organizationId, + features = List(PersistentTenantFeature.PersistentCertifier("certifier")) + ) + + mockDateTimeGet() + mockGetTenantById(organizationId, requester) + mockGetAttributeByIdNotFound(attributeId) + + Delete() ~> tenantService.revokeCertifiedAttributeById(tenantId.toString, attributeId.toString) ~> check { + assert(status == StatusCodes.InternalServerError) + } + } + + "revoke fail if attribute exists but is not certified" in { + implicit val context: Seq[(String, String)] = adminContext + + val tenantId = UUID.randomUUID() + val attributeId = UUID.randomUUID() + + val requester = persistentTenant.copy( + id = organizationId, + features = List(PersistentTenantFeature.PersistentCertifier("certifier")) + ) + + mockDateTimeGet() + mockGetTenantById(organizationId, requester) + mockGetAttributeById(attributeId, persistentAttribute.copy(id = attributeId, kind = Declared)) + + Delete() ~> tenantService.revokeCertifiedAttributeById(tenantId.toString, attributeId.toString) ~> check { + assert(status == StatusCodes.InternalServerError) + } + } + "revoke fail if attribute does not exists on tenant" in { + implicit val context: Seq[(String, String)] = adminContext + + val tenantId = UUID.randomUUID() + val attributeId = UUID.randomUUID() + + val requester = persistentTenant.copy( + id = organizationId, + features = List(PersistentTenantFeature.PersistentCertifier("certifier")) + ) + + val tenant = persistentTenant.copy( + id = tenantId, + attributes = List(persistentCertifiedAttribute, persistentDeclaredAttribute, persistentVerifiedAttribute) + ) + + mockDateTimeGet() + mockGetTenantById(organizationId, requester) + mockGetTenantById(tenantId, tenant) + mockGetAttributeById(attributeId, persistentAttribute.copy(id = attributeId)) + + Delete() ~> tenantService.revokeCertifiedAttributeById(tenantId.toString, attributeId.toString) ~> check { + assert(status == StatusCodes.BadRequest) + } + } }