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

Security enhancements hardening #94

Merged
merged 5 commits into from
Mar 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ import uk.gov.justice.digital.hmpps.electronicmonitoringdatastoreapi.service.int
class CurfewTimetableController(
@Autowired val curfewTimetableService: CurfewTimetableService,
val athenaRoleService: AthenaRoleService,

// TODO: Re-enable audit as @autowired once Cloud Platform in place
val auditService: AuditService? = null,
@Autowired val auditService: AuditService,
) {
@GetMapping("/getCurfewTimetable/{orderId}")
fun getCurfewTimetable(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ import uk.gov.justice.digital.hmpps.electronicmonitoringdatastoreapi.service.int
class EquipmentDetailsController(
@Autowired val equipmentDetailsService: EquipmentDetailsService,
val athenaRoleService: AthenaRoleService,

// TODO: Re-enable audit as @autowired once Cloud Platform in place
val auditService: AuditService? = null,
@Autowired val auditService: AuditService,
) {
@GetMapping("/getEquipmentDetails/{orderId}")
fun getEquipmentDetails(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,65 +18,19 @@ import uk.gov.justice.digital.hmpps.electronicmonitoringdatastoreapi.service.Ord
import uk.gov.justice.digital.hmpps.electronicmonitoringdatastoreapi.service.internal.AuditService

@RestController
@PreAuthorize("hasAnyAuthority('ROLE_EM_DATASTORE_GENERAL_RO', 'ROLE_EM_DATASTORE_RESTRICTED_RO', 'ROLE_ELECTRONIC_MONITORING_DATASTORE_API_SEARCH')")
@PreAuthorize("hasAnyAuthority('ROLE_EM_DATASTORE_GENERAL_RO', 'ROLE_EM_DATASTORE_RESTRICTED_RO')")
@RequestMapping(value = ["/orders"], produces = [MediaType.APPLICATION_JSON_VALUE])
class OrderController(
@Autowired val orderService: OrderService,
val amOrderService: AmOrderService,
val athenaRoleService: AthenaRoleService,

// TODO: Re-enable audit as @autowired once Cloud Platform in place
val auditService: AuditService? = null,
@Autowired val auditService: AuditService,
) {

@GetMapping("/getMockOrderSummary/{orderId}")
fun getMockOrderSummary(
authentication: Authentication,
@PathVariable orderId: String,
): ResponseEntity<OrderInformation> {
val validatedRole = athenaRoleService.getRoleFromAuthentication(authentication)

val result = orderService.getOrderInformation(orderId, validatedRole)

auditService?.createEvent(
authentication.name,
"GET_MOCK_ORDER_SUMMARY",
mapOf("orderId" to orderId),
)

return ResponseEntity.ok(result)
}

// TODO: This is a temporary endpoint to validate code interacting with user claims
@PreAuthorize("hasRole('ROLE_EM_DATASTORE_RESTRICTED_RO') and hasRole('ROLE_EM_DATASTORE_GENERAL_RO')")
@GetMapping("/getOrderSummary/specials/{orderId}")
fun getSpecialsOrder(
authentication: Authentication,
@PathVariable(required = true) orderId: String,
): ResponseEntity<OrderInformation> {
val validatedRole = athenaRoleService.getRoleFromAuthentication(authentication)

val result = orderService.getOrderInformation(orderId, validatedRole)

auditService?.createEvent(
authentication.name,
"GET_SPECIALS_ORDER_SUMMARY",
mapOf("orderId" to orderId),
)

return ResponseEntity.ok(result)
}

@GetMapping("/getOrderSummary/{orderId}")
fun getOrderSummary(
authentication: Authentication,
@PathVariable(required = true) orderId: String,
): ResponseEntity<OrderInformation> = getOrder(authentication, orderId)

@GetMapping("/{orderId}")
fun getOrder(
authentication: Authentication,
@PathVariable(required = true) orderId: String,
): ResponseEntity<OrderInformation> {
val validatedRole = athenaRoleService.getRoleFromAuthentication(authentication)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@ import uk.gov.justice.digital.hmpps.electronicmonitoringdatastoreapi.service.int
class OrderEventsController(
@Autowired val orderEventsService: OrderEventsService,
val athenaRoleService: AthenaRoleService,

// TODO: Re-enable audit as @autowired once Cloud Platform in place
val auditService: AuditService? = null,
@Autowired val auditService: AuditService,
) {
@GetMapping("/getMonitoringEvents/{orderId}")
fun getMonitoringEvents(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,42 +10,36 @@ import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.server.ResponseStatusException
import uk.gov.justice.digital.hmpps.electronicmonitoringdatastoreapi.model.OrderSearchCriteria
import uk.gov.justice.digital.hmpps.electronicmonitoringdatastoreapi.model.OrderSearchResult
import uk.gov.justice.digital.hmpps.electronicmonitoringdatastoreapi.model.QueryExecutionResponse
import uk.gov.justice.digital.hmpps.electronicmonitoringdatastoreapi.model.athena.AthenaQueryResponse
import uk.gov.justice.digital.hmpps.electronicmonitoringdatastoreapi.model.athena.AthenaStringQuery
import uk.gov.justice.digital.hmpps.electronicmonitoringdatastoreapi.service.AthenaRoleService
import uk.gov.justice.digital.hmpps.electronicmonitoringdatastoreapi.service.OrderService
import uk.gov.justice.digital.hmpps.electronicmonitoringdatastoreapi.service.internal.AuditService

@RestController
@PreAuthorize("hasAnyAuthority('ROLE_EM_DATASTORE_GENERAL_RO', 'ROLE_EM_DATASTORE_RESTRICTED_RO', 'ROLE_ELECTRONIC_MONITORING_DATASTORE_API_SEARCH')")
@PreAuthorize("hasAnyAuthority('ROLE_EM_DATASTORE_GENERAL_RO', 'ROLE_EM_DATASTORE_RESTRICTED_RO')")
@RequestMapping(value = ["/search"], produces = [MediaType.APPLICATION_JSON_VALUE])
class SearchController(
@Autowired val orderService: OrderService,
val athenaRoleService: AthenaRoleService,

// TODO: Re-enable audit as @autowired once Cloud Platform in place
val auditService: AuditService? = null,
@Autowired val auditService: AuditService,
) {
@GetMapping("/confirmConnection")
fun confirmConnection(
authentication: Authentication,
): ResponseEntity<Map<String, String>> {
try {
// TODO: Re-enable audit once Cloud Platform in place
// auditService.createEvent(
// authentication.principal.toString(),
// "CONFIRM_CONNECTION",
// mapOf("confirmConnection" to "true"),
// )
auditService.createEvent(
authentication.principal.toString(),
"CONFIRM_CONNECTION",
mapOf("confirmConnection" to "true"),
)

val athenaAccess: Boolean = confirmAthenaAccess(authentication).body
val athenaAccess: Boolean = confirmAthenaAccess(authentication)
val message: String = if (athenaAccess) "Connection successful" else "API Connection successful, but no access to Athena"

return ResponseEntity(
Expand All @@ -60,77 +54,14 @@ class SearchController(
}
}

@GetMapping("/testEndpoint")
fun confirmAthenaAccess(
authentication: Authentication,
): ResponseEntity<Boolean> {
fun confirmAthenaAccess(authentication: Authentication): Boolean {
val validatedRole = athenaRoleService.getRoleFromAuthentication(authentication)

var isAvailable: Boolean
try {
isAvailable = orderService.checkAvailability(validatedRole)
return try {
orderService.checkAvailability(validatedRole)
} catch (ex: Exception) {
throw ResponseStatusException(HttpStatus.BAD_REQUEST, ex.localizedMessage, ex)
}

auditService?.createEvent(
authentication.principal.toString(),
"CONFIRM_ATHENA_ACCESS",
mapOf(
"isAvailable" to isAvailable.toString(),
),
)

return ResponseEntity.ok(isAvailable)
}

@PostMapping("/custom-query")
fun queryAthena(
authentication: Authentication,
@RequestBody(required = true) athenaQuery: AthenaStringQuery,
@RequestHeader("X-Role", required = false) unvalidatedRole: String = "unset",
): ResponseEntity<AthenaQueryResponse<String>> {
val validatedRole = athenaRoleService.fromString(unvalidatedRole)

var result: AthenaQueryResponse<String>
try {
val queryResponse = orderService.query(athenaQuery, validatedRole)

result = AthenaQueryResponse<String>(
queryString = athenaQuery.queryString,
athenaRole = validatedRole.name,
isErrored = false,
queryResponse = queryResponse,
)

auditService?.createEvent(
authentication.name,
"SEARCH_WITH_CUSTOM_QUERY",
mapOf(
"query" to athenaQuery.queryString,
"isErrored" to "false",
),
)
} catch (ex: Exception) {
auditService?.createEvent(
authentication.name,
"SEARCH_WITH_CUSTOM_QUERY",
mapOf(
"query" to athenaQuery.queryString,
"isErrored" to "true",
"error" to ex.localizedMessage,
),
)

result = AthenaQueryResponse<String>(
queryString = athenaQuery.queryString,
athenaRole = validatedRole.name,
isErrored = true,
errorMessage = ex.localizedMessage,
)
}

return ResponseEntity.ok(result)
}

@PostMapping("/orders")
Expand All @@ -142,7 +73,6 @@ class SearchController(

val queryExecutionId = orderService.getQueryExecutionId(orderSearchCriteria, validatedRole)

// TODO: Error-handling for the audit service
auditService?.createEvent(
authentication.name,
"SEARCH_ORDERS",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ import uk.gov.justice.digital.hmpps.electronicmonitoringdatastoreapi.service.int
class SuspensionOfVisitsController(
@Autowired val suspensionOfVisitsService: SuspensionOfVisitsService,
val athenaRoleService: AthenaRoleService,

// TODO: Re-enable audit as @autowired once Cloud Platform in place
val auditService: AuditService? = null,
@Autowired val auditService: AuditService,
) {
@GetMapping("/getSuspensionOfVisits/{orderId}")
fun getSuspensionOfVisits(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ import uk.gov.justice.digital.hmpps.electronicmonitoringdatastoreapi.service.int
class VisitDetailsController(
@Autowired val visitDetailsService: VisitDetailsService,
val athenaRoleService: AthenaRoleService,

// TODO: Re-enable audit as @autowired once Cloud Platform in place
val auditService: AuditService? = null,
@Autowired val auditService: AuditService,
) {
@GetMapping("/getVisitDetails/{orderId}")
fun getVisitDetails(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,120 +48,4 @@ class OrderControllerIntegrationTest : ControllerIntegrationBase() {
.isOk
}
}

@Nested
@DisplayName("GET /orders/getOrderSummary/specials/{orderId}")
inner class GetSpecialsOrder {
@BeforeEach
fun setup() {
MockEmDatastoreClient.addResponseFile("successfulKeyOrderInformationResponse")
MockEmDatastoreClient.addResponseFile("successfulSubjectHistoryReportResponse")
MockEmDatastoreClient.addResponseFile("successfulDocumentListResponse")
}

val baseUri: String = "/orders/getOrderSummary/specials"

@Test
fun `should return 401 unauthorized if no authorization header`() {
noAuthHeaderRespondsWithUnauthorizedTest("$baseUri/234")
}

@Test
fun `should return 403 forbidden if no role in authorization header`() {
noRoleInAuthHeaderRespondsWithForbiddenTest("$baseUri/234")
}

@Test
fun `should return 403 Forbidden if unknown role is present`() {
wrongRolesRespondsWithForbiddenTest("$baseUri/234", listOf("ROLE_UNKNOWN"))
}

@Test
fun `should return 403 Forbidden if no specials role is present`() {
wrongRolesRespondsWithForbiddenTest("$baseUri/234", listOf("ROLE_EM_DATASTORE_GENERAL_RO"))
}

// Note: the @Preauthorize on the method is taken in *PREFERENCE* to the preauthorization on the controller
@Test
fun `should return 403 Forbidden if no search role is present`() {
wrongRolesRespondsWithForbiddenTest("$baseUri/234", listOf("ROLE_EM_DATASTORE_RESTRICTED_RO"))
}

@Test
fun `should return 200 OK if all correct roles are present`() {
val uri = "$baseUri/234"

webTestClient.get()
.uri(uri)
.headers(
setAuthorisation(
roles = listOf(
"ROLE_EM_DATASTORE_RESTRICTED_RO",
"ROLE_EM_DATASTORE_GENERAL_RO",
),
),
)
.exchange()
.expectStatus()
.isOk
}

@Test
fun `should return 200 OK only if both roles are present`() {
val uri = "$baseUri/234"

webTestClient.get()
.uri(uri)
.headers(
setAuthorisation(
roles = listOf(
"ROLE_EM_DATASTORE_RESTRICTED_RO",
"ROLE_EM_DATASTORE_GENERAL_RO",
"ROLE_EM_UNKNOWN_ROLE",
),
),
)
.exchange()
.expectStatus()
.isOk
}
}

@Nested
@DisplayName("GET /orders/{orderId}")
inner class GetOrder {
@BeforeEach
fun setup() {
MockEmDatastoreClient.addResponseFile("successfulKeyOrderInformationResponse")
MockEmDatastoreClient.addResponseFile("successfulSubjectHistoryReportResponse")
MockEmDatastoreClient.addResponseFile("successfulDocumentListResponse")
}

@Test
fun `should return 401 unauthorized if no authorization header`() {
noAuthHeaderRespondsWithUnauthorizedTest("/orders/234")
}

@Test
fun `should return 403 forbidden if no role in authorization header`() {
noRoleInAuthHeaderRespondsWithForbiddenTest("/orders/234")
}

@Test
fun `should return 403 forbidden if wrong role in authorization header`() {
wrongRolesRespondsWithForbiddenTest("/orders/234", listOf("ROLE_WRONG"))
}

@Test
fun `should return OK with valid auth header, role`() {
val uri = "/orders/234"

webTestClient.get()
.uri(uri)
.headers(setAuthorisation())
.exchange()
.expectStatus()
.isOk
}
}
}
Loading