Skip to content

Commit

Permalink
APIS-7388 Add new field deleteRestriction to application (#532)
Browse files Browse the repository at this point in the history
* APIS-7388 Add new field deleteRestriction to application

* APIS-7388 Add new field deleteRestriction to application

* APIS-7388 Set value of DeleteRestriction

* APIS-7388 Set value of DeleteRestriction

* APIS-7388 Set value of DeleteRestriction

* APIS-7388 Use latest application/event domain library

* APIS-7388 Make SetDeleteRestrictionJob set DELETED apps also
  • Loading branch information
petekirby-ee authored Dec 11, 2024
1 parent 6f6d2a7 commit 4c88f49
Show file tree
Hide file tree
Showing 10 changed files with 325 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ class ConfigurationModule extends Module {
bind[ClientSecretsHashingConfig].toProvider[ClientSecretsHashingConfigProvider],
bind[ApplicationNamingService.ApplicationNameValidationConfig].toProvider[ApplicationNameValidationConfigConfigProvider],
bind[ResetLastAccessDateJobConfig].toProvider[ResetLastAccessDateJobConfigProvider],
bind[TermsOfUseInvitationConfig].toProvider[TermsOfUseInvitationConfigProvider]
bind[TermsOfUseInvitationConfig].toProvider[TermsOfUseInvitationConfigProvider],
bind[SetDeleteRestrictionJobConfig].toProvider[SetDeleteRestrictionJobConfigProvider]
)
}
}
Expand Down Expand Up @@ -378,3 +379,15 @@ class ResetLastAccessDateJobConfigProvider @Inject() (configuration: Configurati
ResetLastAccessDateJobConfig(LocalDate.parse(noLastAccessDateBeforeAsString), enabled, dryRun)
}
}

@Singleton
class SetDeleteRestrictionJobConfigProvider @Inject() (configuration: Configuration)
extends ServicesConfig(configuration)
with Provider[SetDeleteRestrictionJobConfig] {

override def get(): SetDeleteRestrictionJobConfig = {
val enabled = configuration.get[Boolean]("setDeleteRestrictionJob.enabled")

SetDeleteRestrictionJobConfig(enabled)
}
}
4 changes: 3 additions & 1 deletion app/uk/gov/hmrc/thirdpartyapplication/config/Scheduler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class Scheduler @Inject() (
responsibleIndividualUpdateVerificationRemovalJob: ResponsibleIndividualUpdateVerificationRemovalJob,
termsOfUseInvitationReminderJob: TermsOfUseInvitationReminderJob,
termsOfUseInvitationOverdueJob: TermsOfUseInvitationOverdueJob,
setDeleteRestrictionJob: SetDeleteRestrictionJob,
override val applicationLifecycle: ApplicationLifecycle,
override val application: Application
)(implicit val ec: ExecutionContext
Expand All @@ -66,7 +67,8 @@ class Scheduler @Inject() (
responsibleIndividualUpdateVerificationRemovalJob,
responsibleIndividualVerificationSetDefaultTypeJob,
termsOfUseInvitationReminderJob,
termsOfUseInvitationOverdueJob
termsOfUseInvitationOverdueJob,
setDeleteRestrictionJob
)
.filter(_.isEnabled) ++ Seq(bcryptPerformanceMeasureJob)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,39 @@
package uk.gov.hmrc.thirdpartyapplication.connector

import java.net.URL
import java.time.Instant
import javax.inject.Inject
import scala.concurrent.{ExecutionContext, Future}

import play.api.libs.json.Json
import play.api.libs.json.{Json, OFormat}
import uk.gov.hmrc.http.HttpReads.Implicits._
import uk.gov.hmrc.http.client.HttpClientV2
import uk.gov.hmrc.http.{HeaderCarrier, StringContextOps}

import uk.gov.hmrc.apiplatform.modules.common.domain.models.{Actor, ApplicationId}
import uk.gov.hmrc.apiplatform.modules.common.services.ApplicationLogger
import uk.gov.hmrc.apiplatform.modules.events.applications.domain.models._
import uk.gov.hmrc.apiplatform.modules.events.applications.domain.services.EventsInterServiceCallJsonFormatters._

case class DisplayEvent(
applicationId: ApplicationId,
eventDateTime: Instant,
actor: Actor,
eventTagDescription: String,
eventType: String,
metaData: List[String]
)

object DisplayEvent {
implicit val format: OFormat[DisplayEvent] = Json.format[DisplayEvent]
}

case class QueryResponse(events: List[DisplayEvent])

object QueryResponse {
implicit val format: OFormat[QueryResponse] = Json.format[QueryResponse]
}

object ApiPlatformEventsConnector {
case class Config(baseUrl: String, enabled: Boolean)
}
Expand All @@ -45,7 +66,7 @@ class ApiPlatformEventsConnector @Inject() (http: HttpClientV2, config: ApiPlatf
implicit val headersWithoutAuthorization: HeaderCarrier = hc.copy(authorization = None)

if (config.enabled) {
http.post(addEventURI(uri))
http.post(eventURI(uri))
.withBody(Json.toJson(event))
.execute[ErrorOr[Unit]]
.map {
Expand All @@ -62,8 +83,24 @@ class ApiPlatformEventsConnector @Inject() (http: HttpClientV2, config: ApiPlatf
}
}

private def addEventURI(path: String): URL = {
private def eventURI(path: String): URL = {
val x = s"$serviceBaseUrl$path"
url"$x"
}

def query(appId: ApplicationId, tag: Option[String], actorType: Option[String])(implicit hc: HeaderCarrier): Future[List[DisplayEvent]] = {
val queryParams =
Seq(
tag.map(et => "eventTag" -> et),
actorType.map(at => "actorType" -> at)
).collect {
case Some((a, b)) => a -> b
}

http.get(url"${eventURI(applicationEventUri)}/${appId}?$queryParams").execute[Option[QueryResponse]]
.map {
case None => List.empty
case Some(response) => response.events
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ case class StoredApplication(
checkInformation: Option[CheckInformation] = None,
blocked: Boolean = false,
ipAllowlist: IpAllowlist = IpAllowlist(),
allowAutoDelete: Boolean = true
allowAutoDelete: Boolean = true,
deleteRestriction: DeleteRestriction = DeleteRestriction.NoRestriction
) extends HasState with HasAccess with HasCollaborators with HasEnvironment {
protected val deployedTo = environment

Expand Down Expand Up @@ -88,7 +89,8 @@ object StoredApplication {
data.blocked,
ipAllowlist = data.ipAllowlist,
allowAutoDelete = data.allowAutoDelete,
lastActionActor = ActorType.UNKNOWN
lastActionActor = ActorType.UNKNOWN,
deleteRestriction = data.deleteRestriction
),
data.collaborators
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ object ApplicationRepository {
(JsPath \ "checkInformation").readNullable[CheckInformation] and
((JsPath \ "blocked").read[Boolean] or Reads.pure(false)) and
((JsPath \ "ipAllowlist").read[IpAllowlist] or Reads.pure(IpAllowlist())) and
((JsPath \ "allowAutoDelete").read[Boolean] or Reads.pure(true))
((JsPath \ "allowAutoDelete").read[Boolean] or Reads.pure(true)) and
((JsPath \ "deleteRestriction").read[DeleteRestriction] or Reads.pure[DeleteRestriction](DeleteRestriction.NoRestriction))
)(StoredApplication.apply _)

implicit val formatStoredApplication: OFormat[StoredApplication] = OFormat(readStoredApplication, Json.writes[StoredApplication])
Expand Down Expand Up @@ -702,7 +703,8 @@ class ApplicationRepository @Inject() (mongo: MongoComponent, val metrics: Metri
"rateLimitTier",
"environment",
"blocked",
"allowAutoDelete"
"allowAutoDelete",
"deleteRestriction"
)
))

Expand Down Expand Up @@ -747,8 +749,8 @@ class ApplicationRepository @Inject() (mongo: MongoComponent, val metrics: Metri
def fetchAllWithNoSubscriptions(): Future[List[StoredApplication]] =
searchApplications("fetchAllWithNoSubscriptions")(new ApplicationSearch(filters = List(NoAPISubscriptions))).map(_.applications)

def fetchAll(): Future[List[StoredApplication]] = {
val result = searchApplications("fetchAll")(new ApplicationSearch())
def fetchAll(includeDeleted: Boolean = false): Future[List[StoredApplication]] = {
val result = searchApplications("fetchAll")(new ApplicationSearch(includeDeleted = includeDeleted))

result.map(_.applications)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* 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.scheduled

import java.time.Clock
import javax.inject.Inject
import scala.concurrent.duration.{Duration, DurationInt, FiniteDuration}
import scala.concurrent.{ExecutionContext, Future}

import org.mongodb.scala.model.Updates

import uk.gov.hmrc.http.HeaderCarrier
import uk.gov.hmrc.mongo.lock.{LockRepository, LockService}
import uk.gov.hmrc.mongo.play.json.Codecs
import uk.gov.hmrc.mongo.play.json.formats.MongoJavatimeFormats

import uk.gov.hmrc.apiplatform.modules.common.domain.models.{Actors, ApplicationId}
import uk.gov.hmrc.apiplatform.modules.common.services.{ApplicationLogger, ClockNow}
import uk.gov.hmrc.apiplatform.modules.applications.core.domain.models.DeleteRestriction
import uk.gov.hmrc.thirdpartyapplication.connector.{ApiPlatformEventsConnector, DisplayEvent}
import uk.gov.hmrc.thirdpartyapplication.models.db.StoredApplication
import uk.gov.hmrc.thirdpartyapplication.repository.ApplicationRepository

class SetDeleteRestrictionJob @Inject() (
setDeleteRestrictionJobLockService: SetDeleteRestrictionJobLockService,
applicationRepository: ApplicationRepository,
eventsConnector: ApiPlatformEventsConnector,
val clock: Clock,
jobConfig: SetDeleteRestrictionJobConfig
)(implicit val ec: ExecutionContext
) extends ScheduledMongoJob
with ApplicationLogger
with ClockNow
with MongoJavatimeFormats.Implicits {

override def name: String = "SetDeleteRestrictionJob"
override def isEnabled: Boolean = jobConfig.enabled
override def initialDelay: FiniteDuration = 1.minutes
override def interval: FiniteDuration = 100.days
override val lockService: LockService = setDeleteRestrictionJobLockService
implicit val hc: HeaderCarrier = HeaderCarrier()

override def runJob(implicit ec: ExecutionContext): Future[RunningOfJobSuccessful] = {
logger.info("Running SetDeleteRestrictionJob")

val result: Future[RunningOfJobSuccessful.type] = for {
applications <- applicationRepository.fetchAll(includeDeleted = true)
_ = logger.info(s"Scheduled job $name found ${applications.size} applications")
_ <- Future.sequence(applications.map(setDeleteRestriction(_)))
} yield RunningOfJobSuccessful

result.recoverWith {
case e: Throwable => Future.failed(RunningOfJobFailed(name, e))
}
}

private def getDoNotDelete(event: Option[DisplayEvent]): DeleteRestriction = {
event match {
case None => DeleteRestriction.DoNotDelete("Set by process - Reason not found", Actors.ScheduledJob(name), instant())
case Some(evt) => DeleteRestriction.DoNotDelete(evt.metaData.mkString, evt.actor, evt.eventDateTime)
}
}

private def getDeleteRestriction(applicationId: ApplicationId, allowAutoDelete: Boolean): Future[DeleteRestriction] = {
if (allowAutoDelete) {
Future.successful(DeleteRestriction.NoRestriction)
} else {
eventsConnector.query(applicationId, Some("APP_LIFECYCLE"), None)
.map(events => events.find(e => e.eventType == "Application auto delete blocked"))
.map(getDoNotDelete(_))
}
}

private def setDeleteRestriction(app: StoredApplication) = {
for {
deleteRestriction <- getDeleteRestriction(app.id, app.allowAutoDelete)
savedApp <- applicationRepository.updateApplication(app.id, Updates.set("deleteRestriction", Codecs.toBson(deleteRestriction)))
_ = logger.info(s"[SetDeleteRestrictionJob]: Set deleteRestriction of application [${app.name} (${app.id})] to [$deleteRestriction]")
} yield savedApp
}
}

class SetDeleteRestrictionJobLockService @Inject() (repository: LockRepository)
extends LockService {

override val lockId: String = "SetDeleteRestriction"
override val lockRepository: LockRepository = repository
override val ttl: Duration = 1.hours
}

case class SetDeleteRestrictionJobConfig(enabled: Boolean)
6 changes: 6 additions & 0 deletions conf/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,12 @@ termsOfUseInvitationOverdueJob {
enabled = false
}

setDeleteRestrictionJob {
initialDelay = 10m
interval = 100d
enabled = false
}

metricsJob {
enabled = false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,8 @@ object ApplicationRepositoryISpecExample extends ServerBaseISpec with FixedClock
),
"blocked" -> JsFalse,
"ipAllowlist" -> Json.obj("required" -> JsFalse, "allowlist" -> JsArray.empty),
"allowAutoDelete" -> JsTrue
"allowAutoDelete" -> JsTrue,
"deleteRestriction" -> Json.obj("deleteRestrictionType" -> JsString("NO_RESTRICTION"))
)
}

Expand Down Expand Up @@ -1326,25 +1327,40 @@ class ApplicationRepositoryISpec
}

"fetchAll" should {
val application1 = anApplicationDataForTest(
id = ApplicationId.random,
prodClientId = generateClientId
)
val application2 = anApplicationDataForTest(
id = ApplicationId.random,
prodClientId = generateClientId
)
val application3 = createAppWithStatusUpdatedOn(
state = State.DELETED
)

"fetch all existing applications" in {
val application1 = anApplicationDataForTest(
id = ApplicationId.random,
prodClientId = generateClientId
)
val application2 = anApplicationDataForTest(
id = ApplicationId.random,
prodClientId = generateClientId
)

"fetch all existing applications - except DELETED apps" in {
await(applicationRepository.save(application1))
await(applicationRepository.save(application2))
await(applicationRepository.save(application3))

await(applicationRepository.fetchAll()) mustBe List(
application1,
application2
)
}

"fetch all existing applications - including DELETED apps" in {
await(applicationRepository.save(application1))
await(applicationRepository.save(application2))
await(applicationRepository.save(application3))

await(applicationRepository.fetchAll(includeDeleted = true)) mustBe List(
application1,
application2,
application3
)
}
}

"fetchAllForContext" should {
Expand Down
4 changes: 2 additions & 2 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.68.0"
lazy val applicationDomainVersion = "0.64.0"
lazy val applicationEventVersion = "0.69.0"
lazy val applicationDomainVersion = "0.65.0"

private lazy val compileDeps = Seq(
"uk.gov.hmrc" %% "bootstrap-backend-play-30" % bootstrapVersion,
Expand Down
Loading

0 comments on commit 4c88f49

Please sign in to comment.