Skip to content

Commit

Permalink
APIS-7339: Gatekeeper User Can Set Deleted Restricted (#538)
Browse files Browse the repository at this point in the history
* APIS-7339: Gatekeeper User Can Set Deleted Restricted

* APIS-7339: Gatekeeper User Can Set Deleted Restricted -Update events

* APIS-7339: Gatekeeper User Can Set Deleted Restricted -Update events - bump api-platform-application-domain and api-platform-application-events versions
  • Loading branch information
kskourtis1 authored Jan 15, 2025
1 parent b7c3e3b commit 09e56b0
Show file tree
Hide file tree
Showing 12 changed files with 363 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,9 @@ class ApplicationRepository @Inject() (mongo: MongoComponent, val metrics: Metri
def updateAllowAutoDelete(applicationId: ApplicationId, allowAutoDelete: Boolean): Future[StoredApplication] =
updateApplication(applicationId, Updates.set("allowAutoDelete", Codecs.toBson(allowAutoDelete)))

def updateDeleteRestriction(applicationId: ApplicationId, deleteRestriction: DeleteRestriction): Future[StoredApplication] =
updateApplication(applicationId, Updates.set("deleteRestriction", Codecs.toBson(deleteRestriction)))

def updateApplicationRateLimit(applicationId: ApplicationId, rateLimit: RateLimitTier): Future[StoredApplication] =
updateApplication(applicationId, Updates.set("rateLimitTier", Codecs.toBson(rateLimit)))

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2023 HM Revenue & Customs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package uk.gov.hmrc.thirdpartyapplication.services.commands.delete

import javax.inject.{Inject, Singleton}
import scala.concurrent.ExecutionContext

import cats._
import cats.data._
import cats.implicits._

import uk.gov.hmrc.apiplatform.modules.common.domain.models.Actors
import uk.gov.hmrc.apiplatform.modules.applications.core.domain.models.DeleteRestriction.NoRestriction
import uk.gov.hmrc.apiplatform.modules.applications.core.domain.models.DeleteRestrictionType.DO_NOT_DELETE
import uk.gov.hmrc.apiplatform.modules.commands.applications.domain.models.ApplicationCommands.AllowApplicationDelete
import uk.gov.hmrc.apiplatform.modules.commands.applications.domain.models.CommandFailures
import uk.gov.hmrc.apiplatform.modules.events.applications.domain.models._
import uk.gov.hmrc.thirdpartyapplication.models.db.StoredApplication
import uk.gov.hmrc.thirdpartyapplication.repository._
import uk.gov.hmrc.thirdpartyapplication.services.commands.CommandHandler

@Singleton
class AllowApplicationDeleteCommandHandler @Inject() (
applicationRepository: ApplicationRepository
)(implicit val ec: ExecutionContext
) extends CommandHandler {

import CommandHandler._

private def validate(app: StoredApplication): Validated[Failures, Unit] = {
Apply[Validated[Failures, *]].map(
cond((app.deleteRestriction.deleteRestrictionType == DO_NOT_DELETE), CommandFailures.GenericFailure(s"Delete is already allowed"))
) { case _ => () }
}

private def asEvents(app: StoredApplication, cmd: AllowApplicationDelete): NonEmptyList[ApplicationEvent] = {
NonEmptyList.of(
ApplicationEvents.AllowApplicationDelete(
id = EventId.random,
applicationId = app.id,
reasons = cmd.reasons,
eventDateTime = cmd.timestamp,
actor = Actors.GatekeeperUser(cmd.gatekeeperUser)
)
)
}

def process(app: StoredApplication, cmd: AllowApplicationDelete): AppCmdResultT = {

for {
valid <- E.fromEither(validate(app).toEither)
savedApp <- E.liftF(applicationRepository.updateDeleteRestriction(app.id, NoRestriction))
events = asEvents(app, cmd)
} yield (savedApp, events)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ class DeleteCommandsProcessor @Inject() (
deleteApplicationByGatekeeperCmdHdlr: DeleteApplicationByGatekeeperCommandHandler,
allowApplicationAutoDeleteCmdHdlr: AllowApplicationAutoDeleteCommandHandler,
blockApplicationAutoDeleteCmdHdlr: BlockApplicationAutoDeleteCommandHandler,
allowApplicationDeleteCmdHdlr: AllowApplicationDeleteCommandHandler,
restrictApplicationDeleteCmdHdlr: RestrictApplicationDeleteCommandHandler,
deleteApplicationByCollaboratorCmdHdlr: DeleteApplicationByCollaboratorCommandHandler,
deleteUnusedApplicationCmdHdlr: DeleteUnusedApplicationCommandHandler,
deleteProductionCredentialsApplicationCmdHdlr: DeleteProductionCredentialsApplicationCommandHandler
Expand All @@ -40,6 +42,8 @@ class DeleteCommandsProcessor @Inject() (
case cmd: DeleteApplicationByGatekeeper => deleteApplicationByGatekeeperCmdHdlr.process(app, cmd)
case cmd: AllowApplicationAutoDelete => allowApplicationAutoDeleteCmdHdlr.process(app, cmd)
case cmd: BlockApplicationAutoDelete => blockApplicationAutoDeleteCmdHdlr.process(app, cmd)
case cmd: AllowApplicationDelete => allowApplicationDeleteCmdHdlr.process(app, cmd)
case cmd: RestrictApplicationDelete => restrictApplicationDeleteCmdHdlr.process(app, cmd)
case cmd: DeleteUnusedApplication => deleteUnusedApplicationCmdHdlr.process(app, cmd)
case cmd: DeleteProductionCredentialsApplication => deleteProductionCredentialsApplicationCmdHdlr.process(app, cmd)
case cmd: DeleteApplicationByCollaborator => deleteApplicationByCollaboratorCmdHdlr.process(app, cmd)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2023 HM Revenue & Customs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package uk.gov.hmrc.thirdpartyapplication.services.commands.delete

import javax.inject.{Inject, Singleton}
import scala.concurrent.ExecutionContext

import cats._
import cats.data._
import cats.implicits._

import uk.gov.hmrc.apiplatform.modules.common.domain.models.Actors
import uk.gov.hmrc.apiplatform.modules.applications.core.domain.models.DeleteRestriction.DoNotDelete
import uk.gov.hmrc.apiplatform.modules.applications.core.domain.models.DeleteRestrictionType.NO_RESTRICTION
import uk.gov.hmrc.apiplatform.modules.commands.applications.domain.models.ApplicationCommands.RestrictApplicationDelete
import uk.gov.hmrc.apiplatform.modules.commands.applications.domain.models.CommandFailures
import uk.gov.hmrc.apiplatform.modules.events.applications.domain.models._
import uk.gov.hmrc.thirdpartyapplication.models.db.StoredApplication
import uk.gov.hmrc.thirdpartyapplication.repository._
import uk.gov.hmrc.thirdpartyapplication.services.commands.CommandHandler

@Singleton
class RestrictApplicationDeleteCommandHandler @Inject() (
applicationRepository: ApplicationRepository
)(implicit val ec: ExecutionContext
) extends CommandHandler {

import CommandHandler._

private def validate(app: StoredApplication): Validated[Failures, Unit] = {
Apply[Validated[Failures, *]].map(
cond((app.deleteRestriction.deleteRestrictionType == NO_RESTRICTION), CommandFailures.GenericFailure(s"Delete is already restricted"))
) { case _ => () }
}

private def asEvents(app: StoredApplication, cmd: RestrictApplicationDelete): NonEmptyList[ApplicationEvent] = {
NonEmptyList.of(
ApplicationEvents.RestrictApplicationDelete(
id = EventId.random,
applicationId = app.id,
reasons = cmd.reasons,
eventDateTime = cmd.timestamp,
actor = Actors.GatekeeperUser(cmd.gatekeeperUser)
)
)
}

def process(app: StoredApplication, cmd: RestrictApplicationDelete): AppCmdResultT = {

for {
valid <- E.fromEither(validate(app).toEither)
savedApp <- E.liftF(applicationRepository.updateDeleteRestriction(app.id, DoNotDelete(cmd.reasons, Actors.GatekeeperUser(cmd.gatekeeperUser), cmd.timestamp)))
events = asEvents(app, cmd)
} yield (savedApp, events)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import uk.gov.hmrc.apiplatform.modules.common.utils.FixedClock
import uk.gov.hmrc.apiplatform.modules.apis.domain.models.ApiIdentifierSyntax._
import uk.gov.hmrc.apiplatform.modules.applications.access.domain.models._
import uk.gov.hmrc.apiplatform.modules.applications.common.domain.models.FullName
import uk.gov.hmrc.apiplatform.modules.applications.core.domain.models.DeleteRestriction.DoNotDelete
import uk.gov.hmrc.apiplatform.modules.applications.core.domain.models.DeleteRestrictionType.NO_RESTRICTION
import uk.gov.hmrc.apiplatform.modules.applications.core.domain.models._
import uk.gov.hmrc.apiplatform.modules.applications.submissions.domain.models._
import uk.gov.hmrc.apiplatform.modules.submissions.SubmissionsTestData
Expand Down Expand Up @@ -311,6 +313,30 @@ class ApplicationRepositoryISpec
}
}

"updateDeleteRestriction" should {

"set the deleteRestriction field on an Application document to do not delete" in {
val deleteRestrictionDoNotDelete = DoNotDelete(reasons, Actors.GatekeeperUser("User"), instant)

val savedApplication = await(
applicationRepository.save(
anApplicationDataForTest(applicationId)
)
)

savedApplication.deleteRestriction.deleteRestrictionType mustBe NO_RESTRICTION

val updatedApplication = await(
applicationRepository.updateDeleteRestriction(
applicationId,
deleteRestrictionDoNotDelete
)
)

updatedApplication.deleteRestriction mustBe deleteRestrictionDoNotDelete
}
}

"updateApplicationRateLimit" should {

"set the rateLimitTier field on an Application document" in {
Expand Down
7 changes: 4 additions & 3 deletions project/AppDependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ object AppDependencies {

lazy val bootstrapVersion = "9.2.0"
lazy val hmrcMongoVersion = "1.7.0"
lazy val applicationEventVersion = "0.69.0"
lazy val applicationDomainVersion = "0.65.0"
lazy val applicationEventVersion = "0.72.0"
lazy val applicationDomainVersion = "0.69.0"

private lazy val compileDeps = Seq(
"uk.gov.hmrc" %% "bootstrap-backend-play-30" % bootstrapVersion,
Expand All @@ -17,7 +17,8 @@ object AppDependencies {
"commons-validator" % "commons-validator" % "1.7",
"uk.gov.hmrc" %% "internal-auth-client-play-30" % "3.0.0",
"uk.gov.hmrc" %% "api-platform-application-events" % applicationEventVersion,
"com.iheart" %% "ficus" % "1.5.2"
"com.iheart" %% "ficus" % "1.5.2",
"uk.gov.hmrc" %% "api-platform-application-domain" % applicationDomainVersion
)

private lazy val testDeps = Seq(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,16 @@ trait ApplicationRepositoryMockModule extends MockitoSugar with ArgumentMatchers
ApplicationRepoMock.verify.updateAllowAutoDelete(eqTo(applicationId), eqTo(allowAutoDelete))
}

object UpdateDeleteRestriction {

def thenReturnWhen(deleteRestriction: DeleteRestriction)(updatedApplication: StoredApplication) = {
when(aMock.updateDeleteRestriction(eqTo(updatedApplication.id), eqTo(deleteRestriction))).thenReturn(successful(updatedApplication))
}

def verifyCalledWith(applicationId: ApplicationId, deleteRestriction: DeleteRestriction) =
ApplicationRepoMock.verify.updateDeleteRestriction(eqTo(applicationId), eqTo(deleteRestriction))
}

object AddCollaborator {

def succeeds(applicationData: StoredApplication) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ class ApplicationCommandDispatcherSpec
mockDeleteApplicationByGatekeeperCommandHandler,
mockAllowApplicationAutoDeleteCommandHandler,
mockBlockApplicationAutoDeleteCommandHandler,
mockAllowApplicationDeleteCommandHandler,
mockRestrictApplicationDeleteCommandHandler,
mockChangeGrantLengthCommandHandler,
mockChangeRateLimitTierCommandHandler,
mockChangeProductionApplicationNameCommandHandler,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,8 @@ class ApplicationServiceSpec
blocked = false,
ipAllowlist = IpAllowlist(),
allowAutoDelete = true,
lastActionActor = ActorType.UNKNOWN
lastActionActor = ActorType.UNKNOWN,
deleteRestriction = DeleteRestriction.NoRestriction
),
collaborators = data.collaborators
))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright 2023 HM Revenue & Customs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package uk.gov.hmrc.thirdpartyapplication.services.commands.delete

import scala.concurrent.ExecutionContext.Implicits.global

import uk.gov.hmrc.http.HeaderCarrier

import uk.gov.hmrc.apiplatform.modules.common.domain.models.Actors
import uk.gov.hmrc.apiplatform.modules.common.utils.FixedClock
import uk.gov.hmrc.apiplatform.modules.applications.core.domain.models.DeleteRestriction.{DoNotDelete, NoRestriction}
import uk.gov.hmrc.apiplatform.modules.commands.applications.domain.models.ApplicationCommands
import uk.gov.hmrc.apiplatform.modules.events.applications.domain.models._
import uk.gov.hmrc.thirdpartyapplication.mocks.repository.ApplicationRepositoryMockModule
import uk.gov.hmrc.thirdpartyapplication.services.commands.{CommandHandler, CommandHandlerBaseSpec}

class AllowApplicationDeleteCommandHandlerSpec extends CommandHandlerBaseSpec {

val reasons = "Some reasons"

trait Setup extends ApplicationRepositoryMockModule {

implicit val hc: HeaderCarrier = HeaderCarrier()

val appWithDeleteAllowed = storedApp.inSandbox()
val timestamp = FixedClock.instant
val deleteRestrictionDoNotDelete = DoNotDelete(reasons, Actors.GatekeeperUser(gatekeeperUser), timestamp)
val appWithDeleteBlocked = appWithDeleteAllowed.copy(deleteRestriction = deleteRestrictionDoNotDelete)

val underTest = new AllowApplicationDeleteCommandHandler(ApplicationRepoMock.aMock)

def checkSuccessResult(expectedActor: Actors.GatekeeperUser)(fn: => CommandHandler.AppCmdResultT) = {
val testMe = await(fn.value).value

inside(testMe) { case (app, events) =>
events should have size 1
val event = events.head

inside(event) {
case ApplicationEvents.AllowApplicationDelete(_, appId, eventDateTime, anActor, reason) =>
appId shouldBe app.id
anActor shouldBe expectedActor
eventDateTime shouldBe timestamp
reason shouldBe reasons
}
}
}
}

"AllowApplicationDelete" should {
val cmd = ApplicationCommands.AllowApplicationDelete(gatekeeperUser, reasons, instant)

"create correct event for a valid request with app" in new Setup {
ApplicationRepoMock.UpdateDeleteRestriction.thenReturnWhen(NoRestriction)(appWithDeleteAllowed)

checkSuccessResult(Actors.GatekeeperUser(gatekeeperUser)) {
underTest.process(appWithDeleteBlocked, cmd)
}
}

"return an error if delete is already allowed" in new Setup {

checkFailsWith("Delete is already allowed") {
underTest.process(appWithDeleteAllowed, cmd)
}
}

}

}
Loading

0 comments on commit 09e56b0

Please sign in to comment.