Skip to content

Commit 66d9e55

Browse files
committed
merge main changes
2 parents 75dda34 + e700b3f commit 66d9e55

File tree

89 files changed

+2829
-421
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+2829
-421
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ __pycache__/
8686
*.key
8787
*.csr
8888
*.pem
89+
*.enc
8990

9091
# Localstack
9192
localstack/cache

docs/guides/setting-up-a-new-consumer.md

+6
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,12 @@ tar cvfz hmpps-integration-api-preprod.tar.gz preprod/preprod-client.key preprod
124124
openssl enc -aes-256-cbc -pbkdf2 -iter 310000 -md sha256 -salt -in hmpps-integration-api-preprod.tar.gz -out hmpps-integration-api-preprod.tar.gz.enc -pass file:./symmetric.key
125125
```
126126

127+
### Alternatively
128+
129+
If the client's public key is named `hmpps-integration-api-cred-exchange-public-key.pem`, change directory into the `scripts/client_certificates` directory and run the `zip.sh` command. This should complete the above steps.
130+
131+
### Accessing the credentials
132+
127133
We can now send the **encrypted** symmetric key (`symmetric.key.enc`) and **encrypted** access credentials (`hmpps-integration-api-preprod.tar.gz.enc`) to the client via email. The client may now decrypt the symmetric key using their private key and subsequently the access credentials using the symmetric key
128134

129135
```Bash

scripts/client_certificates/zip.sh

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/bin/bash
2+
set -e
3+
4+
read_certificate_arguments() {
5+
echo "Environment: (dev, preprod or prod)"
6+
read environment
7+
echo "Client identifier (no spaces, lowercase) that will be used for authorisation: e.g. mapps"
8+
read client
9+
}
10+
11+
get_api_key() {
12+
api_key=`kubectl -n hmpps-integration-api-$environment get secrets consumer-api-keys -o json | jq -r .data.$client | base64 -d`
13+
echo -n $api_key > $environment-$client-api-key
14+
}
15+
16+
generate_symmetric_key() {
17+
# Create a symmetric key
18+
head /dev/urandom | sha256sum > symmetric.key
19+
# Encrypt with client's public key
20+
openssl pkeyutl -encrypt -pubin -inkey hmpps-integration-api-cred-exchange-public-key.pem -in symmetric.key -out symmetric.key.enc
21+
}
22+
23+
zip_files() {
24+
# Create a tarball of the access credentials
25+
tar cvfz hmpps-integration-api-$environment.tar.gz $environment-$client-api-key $environment-$client-client.key $environment-$client-client.pem
26+
# Encrypt using symmetric key
27+
openssl enc -aes-256-cbc -pbkdf2 -iter 310000 -md sha256 -salt -in hmpps-integration-api-$environment.tar.gz -out hmpps-integration-api-$environment.tar.gz.enc -pass file:./symmetric.key
28+
}
29+
30+
main() {
31+
read_certificate_arguments
32+
get_api_key
33+
generate_symmetric_key
34+
zip_files
35+
}
36+
37+
main
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.common
2+
3+
import org.springframework.stereotype.Service
4+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response
5+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi
6+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError
7+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.roleconfig.ConsumerFilters
8+
9+
@Service
10+
class ConsumerPrisonAccessService {
11+
fun <T> checkConsumerHasPrisonAccess(
12+
prisonId: String?,
13+
filters: ConsumerFilters?,
14+
): Response<T?> {
15+
val response = Response<T?>(data = null, errors = emptyList<UpstreamApiError>())
16+
if (filters != null && !filters.matchesPrison(prisonId)) {
17+
response.errors = listOf(UpstreamApiError(UpstreamApi.NOMIS, UpstreamApiError.Type.ENTITY_NOT_FOUND, "Not found"))
18+
}
19+
return response
20+
}
21+
}

src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/config/HmppsIntegrationApiExceptionHandler.kt

+33-16
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,20 @@ import io.sentry.Sentry
44
import jakarta.validation.ValidationException
55
import org.slf4j.LoggerFactory
66
import org.springframework.http.HttpStatus
7+
import org.springframework.http.HttpStatus.BAD_GATEWAY
78
import org.springframework.http.HttpStatus.BAD_REQUEST
9+
import org.springframework.http.HttpStatus.CONFLICT
810
import org.springframework.http.HttpStatus.FORBIDDEN
911
import org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR
1012
import org.springframework.http.HttpStatus.NOT_FOUND
1113
import org.springframework.http.ResponseEntity
1214
import org.springframework.web.bind.annotation.ExceptionHandler
1315
import org.springframework.web.bind.annotation.RestControllerAdvice
1416
import org.springframework.web.reactive.function.client.WebClientResponseException
15-
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.AuthenticationFailedException
17+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.ConflictFoundException
1618
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.EntityNotFoundException
17-
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.MessageFailedException
19+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.ForbiddenByUpstreamServiceException
20+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.HmppsAuthFailedException
1821

1922
@RestControllerAdvice
2023
class HmppsIntegrationApiExceptionHandler {
@@ -46,15 +49,29 @@ class HmppsIntegrationApiExceptionHandler {
4649
)
4750
}
4851

49-
@ExceptionHandler(AuthenticationFailedException::class)
50-
fun handleAuthenticationFailedException(e: AuthenticationFailedException): ResponseEntity<ErrorResponse?>? {
51-
logAndCapture("Authentication error: {}", e)
52+
@ExceptionHandler(HmppsAuthFailedException::class)
53+
fun handleAuthenticationFailedException(e: HmppsAuthFailedException): ResponseEntity<ErrorResponse?>? {
54+
logAndCapture("Authentication error in HMPPS Auth: {}", e)
55+
return ResponseEntity
56+
.status(BAD_GATEWAY)
57+
.body(
58+
ErrorResponse(
59+
status = BAD_GATEWAY,
60+
developerMessage = "Authentication error: ${e.message}",
61+
userMessage = e.message,
62+
),
63+
)
64+
}
65+
66+
@ExceptionHandler(ForbiddenByUpstreamServiceException::class)
67+
fun handleAuthenticationFailedException(e: ForbiddenByUpstreamServiceException): ResponseEntity<ErrorResponse?>? {
68+
logAndCapture("Forbidden to complete action by upstream service: {}", e)
5269
return ResponseEntity
5370
.status(FORBIDDEN)
5471
.body(
5572
ErrorResponse(
5673
status = FORBIDDEN,
57-
developerMessage = "Authentication error: ${e.message}",
74+
developerMessage = "Forbidden to complete action by upstream service: ${e.message}",
5875
userMessage = e.message,
5976
),
6077
)
@@ -74,29 +91,29 @@ class HmppsIntegrationApiExceptionHandler {
7491
)
7592
}
7693

77-
@ExceptionHandler(WebClientResponseException::class)
78-
fun handleWebClientResponseException(e: WebClientResponseException): ResponseEntity<ErrorResponse?>? {
79-
logAndCapture("Upstream service down: {}", e)
94+
@ExceptionHandler(ConflictFoundException::class)
95+
fun handleConflictException(e: ConflictFoundException): ResponseEntity<ErrorResponse> {
96+
logAndCapture("Conflict exception: {}", e)
8097
return ResponseEntity
81-
.status(INTERNAL_SERVER_ERROR)
98+
.status(CONFLICT)
8299
.body(
83100
ErrorResponse(
84-
status = INTERNAL_SERVER_ERROR,
85-
developerMessage = "Unable to complete request as an upstream service is not responding",
101+
status = CONFLICT,
102+
developerMessage = "Unable to complete request as this is a duplicate request",
86103
userMessage = e.message,
87104
),
88105
)
89106
}
90107

91-
@ExceptionHandler(MessageFailedException::class)
92-
fun handleMessageFailedExceptionException(e: MessageFailedException): ResponseEntity<ErrorResponse?>? {
93-
logAndCapture("Unable to send message to SQS: {}", e)
108+
@ExceptionHandler(WebClientResponseException::class)
109+
fun handleWebClientResponseException(e: WebClientResponseException): ResponseEntity<ErrorResponse?>? {
110+
logAndCapture("Upstream service down: {}", e)
94111
return ResponseEntity
95112
.status(INTERNAL_SERVER_ERROR)
96113
.body(
97114
ErrorResponse(
98115
status = INTERNAL_SERVER_ERROR,
99-
developerMessage = "Unable to send message to SQS",
116+
developerMessage = "Unable to complete request as an upstream service is not responding",
100117
userMessage = e.message,
101118
),
102119
)

src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/config/OpenAPIConfig.kt

+18
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,24 @@ class OpenAPIConfig {
7777
"developerMessage" to Schema<String>().type("string").example("Unable to complete request as an upstream service is not responding."),
7878
),
7979
),
80+
).addSchemas(
81+
"TransactionConflict",
82+
Schema<ErrorResponse>().description("Duplicate post - The client_unique_ref has been used before").properties(
83+
mapOf(
84+
"status" to Schema<Int>().type("number").example(409),
85+
"userMessage" to Schema<String>().type("string").example("Conflict"),
86+
"developerMessage" to Schema<String>().type("string").example("Duplicate post - The client_unique_ref has been used before"),
87+
),
88+
),
89+
).addSchemas(
90+
"ForbiddenResponse",
91+
Schema<ErrorResponse>().description("Forbidden to complete action by upstream service").properties(
92+
mapOf(
93+
"status" to Schema<Int>().type("number").example(403),
94+
"userMessage" to Schema<String>().type("string").example("Forbidden to complete action by upstream service"),
95+
"developerMessage" to Schema<String>().type("string").example("Forbidden to complete action by upstream service"),
96+
),
97+
),
8098
)
8199
}
82100
}

src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/config/RequestLogger.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import jakarta.servlet.http.HttpServletResponse
55
import org.slf4j.LoggerFactory
66
import org.springframework.stereotype.Component
77
import org.springframework.web.servlet.HandlerInterceptor
8-
import java.util.stream.Collectors
98

109
// Intercepts incoming requests and logs them
1110
@Component
@@ -32,8 +31,9 @@ class RequestLogger : HandlerInterceptor {
3231
val method: String = "Method: " + request.method
3332
val endpoint: String = "Request URI: " + request.requestURI // Could this expose authentication credentials?
3433
val requestURL: String = "Full Request URL: " + request.requestURL
35-
val body: String = ("Body: " + request.reader.lines().collect(Collectors.joining()))
34+
// Unable to use reader here and read the request body into a controller method
35+
// val body: String = ("Body: " + request.reader.lines().collect(Collectors.joining()))
3636

37-
return "$requestIp \n $method \n $endpoint \n $body \n $requestURL"
37+
return "$requestIp \n $method \n $endpoint \n $requestURL"
3838
}
3939
}

src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/RiskManagementController.kt

+13-4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.RequestParam
1414
import org.springframework.web.bind.annotation.RestController
1515
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.config.AuthorisationConfig
1616
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.EntityNotFoundException
17+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.ForbiddenByUpstreamServiceException
1718
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.extensions.decodeUrlCharacters
1819
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.RiskManagementPlan
1920
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi
@@ -35,6 +36,7 @@ class RiskManagementController(
3536
summary = "Returns a list of Risk Management Plans created for the person with the provided HMPPS ID.",
3637
responses = [
3738
ApiResponse(responseCode = "200", useReturnTypeSchema = true, description = "Successfully found risk management plans for a person with the provided HMPPS ID."),
39+
ApiResponse(responseCode = "403", content = [Content(schema = Schema(ref = "#/components/schemas/ForbiddenResponse"))]),
3840
ApiResponse(responseCode = "404", content = [Content(schema = Schema(ref = "#/components/schemas/PersonNotFound"))]),
3941
ApiResponse(responseCode = "500", content = [Content(schema = Schema(ref = "#/components/schemas/InternalServerError"))]),
4042
],
@@ -46,14 +48,21 @@ class RiskManagementController(
4648
): PaginatedResponse<RiskManagementPlan> {
4749
val hmppsId = encodedHmppsId.decodeUrlCharacters()
4850
val response = getRiskManagementPlansForCrnService.execute(hmppsId)
51+
52+
if (response.hasErrorCausedBy(UpstreamApiError.Type.ENTITY_NOT_FOUND, causedBy = UpstreamApi.RISK_MANAGEMENT_PLAN)) {
53+
throw EntityNotFoundException("Could not find person with id: $hmppsId")
54+
}
55+
56+
if (response.hasErrorCausedBy(UpstreamApiError.Type.FORBIDDEN, causedBy = UpstreamApi.RISK_MANAGEMENT_PLAN)) {
57+
throw ForbiddenByUpstreamServiceException("Upstream API service returned a forbidden error")
58+
}
59+
60+
auditService.createEvent("GET_RISK_MANAGEMENT_PLANS", mapOf("hmppsId" to hmppsId))
61+
4962
if (response.data.isNullOrEmpty()) {
50-
if (response.hasErrorCausedBy(UpstreamApiError.Type.ENTITY_NOT_FOUND, causedBy = UpstreamApi.RISK_MANAGEMENT_PLAN)) {
51-
throw EntityNotFoundException("Could not find person with id: $hmppsId")
52-
}
5363
return emptyList<RiskManagementPlan>().paginateWith(page, perPage)
5464
}
5565

56-
auditService.createEvent("GET_RISK_MANAGEMENT_PLANS", mapOf("hmppsId" to hmppsId))
5766
return response.data.paginateWith(page, perPage)
5867
}
5968
}

0 commit comments

Comments
 (0)