Skip to content

Commit

Permalink
PI-2397 Add allow list for domain event detail URLs
Browse files Browse the repository at this point in the history
  • Loading branch information
marcus-bcl committed Mar 11, 2025
1 parent da5bcca commit 467e7b1
Show file tree
Hide file tree
Showing 110 changed files with 388 additions and 576 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package uk.gov.justice.digital.hmpps.detail

import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.beans.factory.annotation.Value
import org.springframework.core.ParameterizedTypeReference
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Service
import org.springframework.web.client.RestClient
import uk.gov.justice.digital.hmpps.message.HmppsDomainEvent
import java.net.URI

@Service
class DomainEventDetailService(
@Qualifier("oauth2Client") val restClient: RestClient?,
@Value("\${messaging.consumer.detail.urls:#{null}}") val allowedUrls: List<String>?
) {
fun validate(detailUrl: String?): URI {
val uri = URI.create(requireNotNull(detailUrl) { "Detail URL must not be null" })
val baseUrl = "${uri.scheme}://${uri.authority}"
require(allowedUrls == null || allowedUrls!!.contains(baseUrl)) { "Unexpected detail URL: $baseUrl" }
return uri
}

final inline fun <reified T> getDetail(event: HmppsDomainEvent): T =
getDetail(validate(event.detailUrl), object : ParameterizedTypeReference<T>() {})

final inline fun <reified T> getDetail(detailUrl: String?): T =
getDetail(validate(detailUrl), object : ParameterizedTypeReference<T>() {})

fun <T> getDetail(uri: URI, type: ParameterizedTypeReference<T>): T =
requireNotNull(restClient).get().uri(uri).retrieve().body<T>(type)!!

final inline fun <reified T> getDetailResponse(event: HmppsDomainEvent): ResponseEntity<T> =
getDetailResponse(validate(event.detailUrl), object : ParameterizedTypeReference<T>() {})

fun <T> getDetailResponse(uri: URI, type: ParameterizedTypeReference<T>): ResponseEntity<T> =
requireNotNull(restClient).get().uri(uri).retrieve().toEntity<T>(type)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package uk.gov.justice.digital.hmpps.detail

import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.equalTo
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.Answers
import org.mockito.Mock
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.any
import org.mockito.kotlin.whenever
import org.springframework.core.ParameterizedTypeReference
import org.springframework.web.client.RestClient
import uk.gov.justice.digital.hmpps.message.HmppsDomainEvent
import java.net.URI

@ExtendWith(MockitoExtension::class)
internal class DomainEventDetailServiceTest {
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
lateinit var client: RestClient

lateinit var service: DomainEventDetailService

@BeforeEach
fun beforeEach() {
service = DomainEventDetailService(client, listOf("http://localhost:8080"))
}

@Test
fun `missing URL is rejected`() {
val exception = assertThrows<IllegalArgumentException> {
service.getDetail<Any>(
HmppsDomainEvent(
eventType = "test",
version = 1
)
)
}
assertThat(exception.message, equalTo("Detail URL must not be null"))
}

@Test
fun `invalid URL is rejected`() {
assertThrows<IllegalArgumentException> {
service.getDetail<Any>(
HmppsDomainEvent(
eventType = "test",
version = 1,
detailUrl = "invalid url"
)
)
}
}

@Test
fun `URL must be in allowed list`() {
val exception = assertThrows<IllegalArgumentException> {
service.getDetail<Any>(
HmppsDomainEvent(
eventType = "test",
version = 1,
detailUrl = "https://example.com"
)
)
}
assertThat(exception.message, equalTo("Unexpected detail URL: https://example.com"))
}

@Test
fun `API is called if URL is valid`() {
whenever(client.get().uri(any<URI>()).retrieve().body(any<ParameterizedTypeReference<String>>()))
.thenReturn("API Response")
val response = service.getDetail<String>(
HmppsDomainEvent(
eventType = "test",
version = 1,
detailUrl = "http://localhost:8080"
)
)
assertThat(response, equalTo("API Response"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ generic-service:
SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_HMPPS-AUTH_TOKEN-URI: http://hmpps-auth.hmpps-auth-dev.svc.cluster.local/auth/oauth/token
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI: http://hmpps-auth.hmpps-auth-dev.svc.cluster.local/auth/.well-known/jwks.json
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: https://sign-in-dev.hmpps.service.justice.gov.uk/auth/issuer
INTEGRATIONS_APPROVED-PREMISES-API_URL: https://approved-premises-api-dev.hmpps.service.justice.gov.uk
MESSAGING_CONSUMER_DETAIL_URLS: https://approved-premises-api-dev.hmpps.service.justice.gov.uk
INTEGRATIONS_ALFRESCO_URL: https://hmpps-delius-alfresco-test.apps.live.cloud-platform.service.justice.gov.uk/alfresco/service/noms-spg/
EVENT_EXCEPTION_THROWNOTFOUND: false

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ generic-service:
SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_HMPPS-AUTH_TOKEN-URI: http://hmpps-auth.hmpps-auth-preprod.svc.cluster.local/auth/oauth/token
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI: http://hmpps-auth.hmpps-auth-preprod.svc.cluster.local/auth/.well-known/jwks.json
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: https://sign-in-preprod.hmpps.service.justice.gov.uk/auth/issuer
INTEGRATIONS_APPROVED-PREMISES-API_URL: https://approved-premises-api-preprod.hmpps.service.justice.gov.uk
MESSAGING_CONSUMER_DETAIL_URLS: https://approved-premises-api-preprod.hmpps.service.justice.gov.uk
INTEGRATIONS_ALFRESCO_URL: https://alfresco.pre-prod.delius.probation.hmpps.dsd.io/alfresco/service/noms-spg/

generic-prometheus-alerts:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ generic-service:
SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_HMPPS-AUTH_TOKEN-URI: http://hmpps-auth.hmpps-auth-prod.svc.cluster.local/auth/oauth/token
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI: http://hmpps-auth.hmpps-auth-prod.svc.cluster.local/auth/.well-known/jwks.json
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: https://sign-in.hmpps.service.justice.gov.uk/auth/issuer
INTEGRATIONS_APPROVED-PREMISES-API_URL: https://approved-premises-api.hmpps.service.justice.gov.uk
MESSAGING_CONSUMER_DETAIL_URLS: https://approved-premises-api.hmpps.service.justice.gov.uk
INTEGRATIONS_ALFRESCO_URL: https://alfresco.probation.service.justice.gov.uk/alfresco/service/noms-spg/

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import uk.gov.justice.digital.hmpps.message.Notification
import uk.gov.justice.digital.hmpps.service.ApprovedPremisesService
import uk.gov.justice.digital.hmpps.telemetry.TelemetryMessagingExtensions.notificationReceived
import uk.gov.justice.digital.hmpps.telemetry.TelemetryService
import java.net.URI

@Component
@Channel("approved-premises-and-delius-queue")
Expand Down Expand Up @@ -113,4 +112,3 @@ fun HmppsDomainEvent.telemetryProperties() = listOfNotNull(
).toMap()

fun HmppsDomainEvent.crn(): String = personReference.findCrn() ?: throw IllegalArgumentException("Missing CRN")
fun HmppsDomainEvent.url(): URI = URI.create(detailUrl ?: throw IllegalArgumentException("Missing detail url"))
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package uk.gov.justice.digital.hmpps.service

import org.springframework.stereotype.Service
import uk.gov.justice.digital.hmpps.integrations.approvedpremises.ApprovedPremisesApiClient
import uk.gov.justice.digital.hmpps.detail.DomainEventDetailService
import uk.gov.justice.digital.hmpps.integrations.approvedpremises.*
import uk.gov.justice.digital.hmpps.integrations.delius.approvedpremises.ApprovedPremisesRepository
import uk.gov.justice.digital.hmpps.integrations.delius.approvedpremises.getApprovedPremises
import uk.gov.justice.digital.hmpps.integrations.delius.approvedpremises.referral.entity.EventRepository
Expand All @@ -14,11 +15,10 @@ import uk.gov.justice.digital.hmpps.integrations.delius.staff.getByCode
import uk.gov.justice.digital.hmpps.message.HmppsDomainEvent
import uk.gov.justice.digital.hmpps.messaging.Notifier
import uk.gov.justice.digital.hmpps.messaging.crn
import uk.gov.justice.digital.hmpps.messaging.url

@Service
class ApprovedPremisesService(
private val approvedPremisesApiClient: ApprovedPremisesApiClient,
private val detailService: DomainEventDetailService,
private val approvedPremisesRepository: ApprovedPremisesRepository,
private val staffRepository: StaffRepository,
private val personRepository: PersonRepository,
Expand All @@ -29,7 +29,7 @@ class ApprovedPremisesService(
private val notifier: Notifier,
) {
fun applicationSubmitted(event: HmppsDomainEvent) {
val details = approvedPremisesApiClient.getApplicationSubmittedDetails(event.url()).eventDetails
val details = detailService.getDetail<EventDetails<ApplicationSubmitted>>(event).eventDetails
val person = personRepository.getByCrn(event.crn())
val dEvent = eventRepository.getEvent(person.id, details.eventNumber)
contactService.createContact(
Expand All @@ -47,7 +47,7 @@ class ApprovedPremisesService(
}

fun applicationAssessed(event: HmppsDomainEvent) {
val details = approvedPremisesApiClient.getApplicationAssessedDetails(event.url()).eventDetails
val details = detailService.getDetail<EventDetails<ApplicationAssessed>>(event).eventDetails
val person = personRepository.getByCrn(event.crn())
val dEvent = eventRepository.getEvent(person.id, details.eventNumber)
contactService.createContact(
Expand All @@ -65,7 +65,7 @@ class ApprovedPremisesService(
}

fun applicationWithdrawn(event: HmppsDomainEvent) {
val details = approvedPremisesApiClient.getApplicationWithdrawnDetails(event.url()).eventDetails
val details = detailService.getDetail<EventDetails<ApplicationWithdrawn>>(event).eventDetails
val person = personRepository.getByCrn(event.crn())
val dEvent = eventRepository.getEvent(person.id, details.eventNumber)
contactService.createContact(
Expand All @@ -85,25 +85,25 @@ class ApprovedPremisesService(
}

fun bookingMade(event: HmppsDomainEvent) {
val details = approvedPremisesApiClient.getBookingMadeDetails(event.url()).eventDetails
val details = detailService.getDetail<EventDetails<BookingMade>>(event).eventDetails
val ap = approvedPremisesRepository.getApprovedPremises(details.premises.legacyApCode)
referralService.bookingMade(event.crn(), details, ap)
}

fun bookingChanged(event: HmppsDomainEvent) {
val details = approvedPremisesApiClient.getBookingChangedDetails(event.url()).eventDetails
val details = detailService.getDetail<EventDetails<BookingChanged>>(event).eventDetails
val ap = approvedPremisesRepository.getApprovedPremises(details.premises.legacyApCode)
referralService.bookingChanged(event.crn(), details, ap)
}

fun bookingCancelled(event: HmppsDomainEvent) {
val details = approvedPremisesApiClient.getBookingCancelledDetails(event.url()).eventDetails
val details = detailService.getDetail<EventDetails<BookingCancelled>>(event).eventDetails
val ap = approvedPremisesRepository.getApprovedPremises(details.premises.legacyApCode)
referralService.bookingCancelled(event.crn(), details, ap)
}

fun personNotArrived(event: HmppsDomainEvent) {
val details = approvedPremisesApiClient.getPersonNotArrivedDetails(event.url())
val details = detailService.getDetail<EventDetails<PersonNotArrived>>(event)
val ap = approvedPremisesRepository.getApprovedPremises(details.eventDetails.premises.legacyApCode)
referralService.personNotArrived(
personRepository.getByCrn(event.crn()),
Expand All @@ -114,7 +114,7 @@ class ApprovedPremisesService(
}

fun personArrived(event: HmppsDomainEvent) {
val details = approvedPremisesApiClient.getPersonArrivedDetails(event.url()).eventDetails
val details = detailService.getDetail<EventDetails<PersonArrived>>(event).eventDetails
val person = personRepository.getByCrn(event.crn())
val ap = approvedPremisesRepository.getApprovedPremises(details.premises.legacyApCode)
nsiService.personArrived(person, details, ap)?.let { (previousAddress, newAddress) ->
Expand All @@ -124,7 +124,7 @@ class ApprovedPremisesService(
}

fun personDeparted(event: HmppsDomainEvent) {
val details = approvedPremisesApiClient.getPersonDepartedDetails(event.url()).eventDetails
val details = detailService.getDetail<EventDetails<PersonDeparted>>(event).eventDetails
val person = personRepository.getByCrn(event.crn())
val ap = approvedPremisesRepository.getApprovedPremises(details.premises.legacyApCode)
nsiService.personDeparted(person, details, ap)?.let { updatedAddress ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,9 @@ spring:
seed.database: true
wiremock.enabled: true

messaging.consumer.queue: message-queue
messaging.producer.topic: domain-events

integrations:
approved-premises-api:
url: http://localhost:${wiremock.port}/approved-premises-api
messaging.consumer.queue: message-queue
messaging.consumer.detail.urls: http://localhost:${wiremock.port}

oauth2:
client-id: $SERVICE_NAME
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,6 @@ internal class HandlerTest {
handler = Handler(telemetryService, approvedPremisesService, converter, true)
}

@Test
fun `throws when no detail url is provided`() {
val event = HmppsDomainEvent(eventType = "test", version = 1, occurredAt = ZonedDateTime.now())
val exception = assertThrows<IllegalArgumentException> { event.url() }
assertThat(exception.message, equalTo("Missing detail url"))
}

@Test
fun `throws when no crn is provided`() {
val event = HmppsDomainEvent(eventType = "test", version = 1, occurredAt = ZonedDateTime.now())
Expand Down
Loading

0 comments on commit 467e7b1

Please sign in to comment.