Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PI-1689 #2749

Merged
merged 19 commits into from
Nov 30, 2023
Merged

PI-1689 #2749

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3938e64
Bump org.springframework.boot from 3.1.5 to 3.2.0
dependabot[bot] Nov 24, 2023
891e2b8
PI-1689 initial commit
anthony-britton-moj Nov 27, 2023
2462592
Merge remote-tracking branch 'origin/main' into ab_PI-1689
anthony-britton-moj Nov 27, 2023
b5ec288
PI-1689 fix make recall decisions
anthony-britton-moj Nov 27, 2023
5f9c8ea
Merge remote-tracking branch 'origin/main' into ab_PI-1689
anthony-britton-moj Nov 29, 2023
478d35f
update approved premises to use document management library
anthony-britton-moj Nov 29, 2023
28b6338
Merge remote-tracking branch 'origin/main' into ab_PI-1689
anthony-britton-moj Nov 29, 2023
1854e7a
Merge remote-tracking branch 'origin/main' into ab_PI-1689
anthony-britton-moj Nov 29, 2023
8bb53fa
fix prison identifier merge issues
anthony-britton-moj Nov 29, 2023
c4215fa
interceptor for alfresco headers
anthony-britton-moj Nov 29, 2023
47b30d7
generify hmpps auth client and update templates
anthony-britton-moj Nov 30, 2023
62f4395
ktlint templates
anthony-britton-moj Nov 30, 2023
5c9c4b1
Fix SQLRestriction imports
anthony-britton-moj Nov 30, 2023
65ba5ef
remove client library from server only template
anthony-britton-moj Nov 30, 2023
5d31b15
revert accidental removal of spring docs from court case and delius
anthony-britton-moj Nov 30, 2023
58f0efa
rename client bean function
anthony-britton-moj Nov 30, 2023
1acb754
ktlintFormat
anthony-britton-moj Nov 30, 2023
942f006
fix ap and oasys
anthony-britton-moj Nov 30, 2023
a68edab
update name for oauth2Client
anthony-britton-moj Nov 30, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ plugins {
kotlin("plugin.spring") version "1.9.21" apply false
kotlin("plugin.jpa") version "1.9.21" apply false
kotlin("kapt") version "1.9.21" apply false
id("org.springframework.boot") version "3.1.5" apply false
id("org.springframework.boot") version "3.2.0" apply false
id("io.spring.dependency-management") version "1.1.4" apply false
id("com.google.cloud.tools.jib") apply false
id("org.jlleitschuh.gradle.ktlint") version "11.6.1"
Expand Down
19 changes: 19 additions & 0 deletions libs/document-management/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import uk.gov.justice.digital.hmpps.extensions.ClassPathExtension

dependencies {
implementation(project(":libs:commons"))
implementation(project(":libs:audit"))
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")

testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation(libs.bundles.mockito)
}

configure<ClassPathExtension> {
jacocoExclusions = listOf(
"**/exception/**",
"**/config/**",
"**/logging/**"
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package uk.gov.justice.digital.hmpps.alfresco

import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.ContentDisposition
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpRequest
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.http.client.ClientHttpRequestExecution
import org.springframework.http.client.ClientHttpRequestInterceptor
import org.springframework.http.client.ClientHttpResponse
import org.springframework.http.client.JdkClientHttpRequestFactory
import org.springframework.stereotype.Component
import org.springframework.web.client.RestClient
import org.springframework.web.client.RestClient.RequestHeadersSpec.ConvertibleClientHttpResponse
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody
import uk.gov.justice.digital.hmpps.exception.NotFoundException
import uk.gov.justice.digital.hmpps.security.ServiceContext
import java.net.http.HttpClient
import java.time.Duration

@Component
@ConditionalOnProperty("integrations.alfresco.url")
class AlfrescoClient(
@Qualifier("alfrescoRestClient") private val restClient: RestClient
) {

fun getDocumentById(id: String): RestClient.RequestHeadersSpec<*> = restClient.get().uri("/fetch/$id")
.accept(MediaType.MULTIPART_FORM_DATA)

fun streamDocument(id: String, filename: String): ResponseEntity<StreamingResponseBody> =
getDocumentById(id).exchange({ _, res ->
when (res.statusCode) {
HttpStatus.OK -> ResponseEntity.ok()
.headers {
it.copy(HttpHeaders.CONTENT_LENGTH, res)
it.copy(HttpHeaders.ETAG, res)
it.copy(HttpHeaders.LAST_MODIFIED, res)
}
.header(
HttpHeaders.CONTENT_DISPOSITION,
ContentDisposition.attachment().filename(filename, Charsets.UTF_8).build().toString()
)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(StreamingResponseBody { output -> res.body.use { it.copyTo(output) } })

HttpStatus.NOT_FOUND -> throw NotFoundException("Document content", "alfrescoId", id)

else -> throw RuntimeException("Failed to download document. Alfresco responded with ${res.statusCode}.")
}
}, false)

private fun HttpHeaders.copy(key: String, res: ConvertibleClientHttpResponse) {
res.headers[key]?.also { this[key] = it }
}
}

@Configuration
class AlfrescoClientConfig(@Value("\${integrations.alfresco.url}") private val alfrescoBaseUrl: String) {
@Bean
fun alfrescoRestClient() = RestClient.builder()
.requestFactory(withTimeouts(Duration.ofSeconds(1), Duration.ofSeconds(30)))
.requestInterceptor(AlfrescoInterceptor())
.baseUrl(alfrescoBaseUrl)
.build()
}

fun withTimeouts(connection: Duration, read: Duration) =
JdkClientHttpRequestFactory(HttpClient.newBuilder().connectTimeout(connection).build())
.also { it.setReadTimeout(read) }

class AlfrescoInterceptor : ClientHttpRequestInterceptor {
override fun intercept(
request: HttpRequest,
body: ByteArray,
execution: ClientHttpRequestExecution
): ClientHttpResponse {
request.headers["X-DocRepository-Remote-User"] = "N00"
request.headers["X-DocRepository-Real-Remote-User"] = ServiceContext.servicePrincipal()?.username
return execution.execute(request, body)
}
}
2 changes: 0 additions & 2 deletions libs/messaging/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@ dependencies {
implementation(project(":libs:commons"))
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
compileOnly(libs.openfeign)
compileOnly("org.springframework.boot:spring-boot-starter-data-jpa")

api(libs.bundles.aws.messaging)

testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation(libs.bundles.mockito)
testImplementation(libs.openfeign)
testImplementation("org.springframework.boot:spring-boot-starter-data-jpa")
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package uk.gov.justice.digital.hmpps.listener

import feign.FeignException
import io.awspring.cloud.sqs.annotation.SqsListener
import io.awspring.cloud.sqs.listener.AsyncAdapterBlockingExecutionFailedException
import io.awspring.cloud.sqs.listener.ListenerExecutionFailedException
Expand All @@ -16,6 +15,7 @@ import org.springframework.orm.ObjectOptimisticLockingFailureException
import org.springframework.stereotype.Component
import org.springframework.transaction.CannotCreateTransactionException
import org.springframework.transaction.UnexpectedRollbackException
import org.springframework.web.client.HttpStatusCodeException
import uk.gov.justice.digital.hmpps.config.AwsCondition
import uk.gov.justice.digital.hmpps.messaging.NotificationHandler
import uk.gov.justice.digital.hmpps.retry.retry
Expand All @@ -35,7 +35,7 @@ class AwsNotificationListener(
retry(
3,
listOf(
FeignException.NotFound::class,
HttpStatusCodeException::class,
CannotAcquireLockException::class,
ObjectOptimisticLockingFailureException::class,
CannotCreateTransactionException::class,
Expand All @@ -52,9 +52,15 @@ class AwsNotificationListener(
fun unwrapSqsExceptions(e: Throwable): Throwable {
fun unwrap(e: Throwable) = e.cause ?: e
var cause = e
if (cause is CompletionException) { cause = unwrap(cause) }
if (cause is AsyncAdapterBlockingExecutionFailedException) { cause = unwrap(cause) }
if (cause is ListenerExecutionFailedException) { cause = unwrap(cause) }
if (cause is CompletionException) {
cause = unwrap(cause)
}
if (cause is AsyncAdapterBlockingExecutionFailedException) {
cause = unwrap(cause)
}
if (cause is ListenerExecutionFailedException) {
cause = unwrap(cause)
}
return cause
}
}
1 change: 0 additions & 1 deletion libs/oauth-client/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ dependencies {
implementation(project(":libs:commons"))
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation(libs.openfeign)

api("org.springframework.boot:spring-boot-starter-oauth2-client")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package uk.gov.justice.digital.hmpps.config.security

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.client.JdkClientHttpRequestFactory
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager
import org.springframework.web.client.RestClient
import org.springframework.web.client.RestClient.Builder
import org.springframework.web.client.support.RestClientAdapter
import org.springframework.web.service.invoker.HttpServiceProxyFactory
import java.net.http.HttpClient
import java.time.Duration

@Configuration
class HmppsAuthClientConfig(
private val restClientBuilder: Builder,
private val clientManager: OAuth2AuthorizedClientManager
) {
@Bean
fun oauth2Client() = restClientBuilder
.requestFactory(withTimeouts(Duration.ofSeconds(1), Duration.ofSeconds(5)))
.requestInterceptor(HmppsAuthInterceptor(clientManager, "default"))
.build()
}

fun withTimeouts(connection: Duration, read: Duration) =
JdkClientHttpRequestFactory(HttpClient.newBuilder().connectTimeout(connection).build())
.also { it.setReadTimeout(read) }

inline fun <reified T> createClient(client: RestClient): T {
return HttpServiceProxyFactory.builderFor(RestClientAdapter.create(client)).build()
.createClient(T::class.java)
}
Original file line number Diff line number Diff line change
@@ -1,41 +1,41 @@
package uk.gov.justice.digital.hmpps.config.feign
package uk.gov.justice.digital.hmpps.config.security

import feign.RequestInterceptor
import feign.Retryer
import org.springframework.context.annotation.Bean
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpRequest
import org.springframework.http.client.ClientHttpRequestExecution
import org.springframework.http.client.ClientHttpRequestInterceptor
import org.springframework.http.client.ClientHttpResponse
import org.springframework.security.authentication.AnonymousAuthenticationToken
import org.springframework.security.core.authority.AuthorityUtils
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager
import org.springframework.security.oauth2.core.OAuth2AuthenticationException

abstract class FeignConfig(
private val authorizedClientManager: OAuth2AuthorizedClientManager
) {

abstract fun registrationId(): String

@Bean
open fun retryer() = Retryer.Default()

@Bean
open fun requestInterceptor() = RequestInterceptor { template ->
template.header(HttpHeaders.AUTHORIZATION, "Bearer ${getAccessToken()}")
class HmppsAuthInterceptor(
private val clientManager: OAuth2AuthorizedClientManager,
private val registrationId: String
) : ClientHttpRequestInterceptor {
override fun intercept(
request: HttpRequest,
body: ByteArray,
execution: ClientHttpRequestExecution
): ClientHttpResponse {
request.headers[HttpHeaders.AUTHORIZATION] = "Bearer ${getToken()}"
return execution.execute(request, body)
}

private fun getAccessToken(): String {
private fun getToken(): String {
val authentication = SecurityContextHolder.getContext().authentication ?: AnonymousAuthenticationToken(
"hmpps-auth",
"anonymous",
AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")
)
val request = OAuth2AuthorizeRequest
.withClientRegistrationId(registrationId())
.withClientRegistrationId(registrationId)
.principal(authentication)
.build()
return authorizedClientManager.authorize(request)?.accessToken?.tokenValue
return clientManager.authorize(request)?.accessToken?.tokenValue
?: throw OAuth2AuthenticationException("Unable to retrieve access token")
}
}
2 changes: 1 addition & 1 deletion projects/approved-premises-and-delius/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ apply(plugin = "com.google.cloud.tools.jib")
dependencies {
implementation(project(":libs:audit"))
implementation(project(":libs:commons"))
implementation(project(":libs:document-management"))
implementation(project(":libs:limited-access"))
implementation(project(":libs:messaging"))
implementation(project(":libs:oauth-client"))
Expand All @@ -18,7 +19,6 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation(libs.openfeign)
implementation(libs.springdoc)

dev(project(":libs:dev-tools"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.MvcResult
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
Expand All @@ -30,6 +31,8 @@ internal class DocIntegrationTest {
mockMvc.perform(
get("/documents/A000001/uuid1").accept("application/octet-stream").withOAuth2Token(wireMockserver)
)
.andExpect(MockMvcResultMatchers.request().asyncStarted())
.andDo(MvcResult::getAsyncResult)
.andExpect(status().is2xxSuccessful)
.andExpect(header().string("Content-Type", "application/octet-stream"))
.andExpect(
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package uk.gov.justice.digital.hmpps.config

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.client.RestClient
import uk.gov.justice.digital.hmpps.config.security.createClient
import uk.gov.justice.digital.hmpps.integrations.approvedpremises.ApprovedPremisesApiClient

@Configuration
class RestClientConfig(private val oauth2Client: RestClient) {
@Bean
fun approvedPremisesApiClient() = createClient<ApprovedPremisesApiClient>(oauth2Client)
}

This file was deleted.

Loading