Skip to content

Commit

Permalink
PI-1493 Add endpoints for HMPPS Auth integration
Browse files Browse the repository at this point in the history
  • Loading branch information
marcus-bcl committed Oct 2, 2023
1 parent 3731feb commit b6d39c2
Show file tree
Hide file tree
Showing 19 changed files with 413 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,15 @@ import io.opentelemetry.instrumentation.annotations.SpanAttribute
import io.opentelemetry.instrumentation.annotations.WithSpan
import org.springframework.ldap.core.AttributesMapper
import org.springframework.ldap.core.LdapTemplate
import org.springframework.ldap.query.LdapQuery
import org.springframework.ldap.query.LdapQueryBuilder
import org.springframework.ldap.query.LdapQueryBuilder.query
import org.springframework.ldap.query.SearchScope

fun LdapQueryBuilder.byUsername(username: String): LdapQuery = base("ou=Users").searchScope(SearchScope.ONELEVEL).where("cn").`is`(username)

@WithSpan
inline fun <reified T> LdapTemplate.findByUsername(@SpanAttribute username: String) = find(
query()
.base("ou=Users")
.searchScope(SearchScope.ONELEVEL)
.where("cn").`is`(username),
T::class.java
).singleOrNull()
inline fun <reified T> LdapTemplate.findByUsername(@SpanAttribute username: String) = find(query().byUsername(username), T::class.java).singleOrNull()

@WithSpan
fun LdapTemplate.findEmailByUsername(@SpanAttribute username: String) = search(
Expand Down
2 changes: 2 additions & 0 deletions projects/hmpps-auth-and-delius/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ dependencies {

implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-data-ldap")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-web")
Expand All @@ -21,6 +22,7 @@ dependencies {
dev(project(":libs:dev-tools"))
dev("com.h2database:h2")
dev("org.testcontainers:oracle-xe")
dev("com.unboundid:unboundid-ldapsdk")

runtimeOnly("com.oracle.database.jdbc:ojdbc11")

Expand Down
3 changes: 3 additions & 0 deletions projects/hmpps-auth-and-delius/deploy/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ generic-service:
namespace_secrets:
common:
SPRING_DATASOURCE_URL: DB_URL
SPRING_LDAP_URLS: LDAP_URL
SPRING_LDAP_USERNAME: LDAP_USERNAME
SPRING_LDAP_PASSWORD: LDAP_PASSWORD
hmpps-auth-and-delius-database:
SPRING_DATASOURCE_USERNAME: DB_USERNAME
SPRING_DATASOURCE_PASSWORD: DB_PASSWORD
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDo3hw1/oChbttEOxEH4NUDrH+Y
n2x0DavAmDjMbhcSiQ6+/t8Nz/N03BauWzFOGBtftnQrHfnF+O7RAKj8zMjcbIq4
QrYeXEpnaFCGEwTtOBpxvSEWPrLEpr1gCarBQZDp67ag+SYqrDgkn2Vme/dMvMUQ
xUO3DT6jg9921J6TlwIDAQAB
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQChGeUcaJnFldtX2x3eqCZVXvXC
eXVzoka/DR1D52HvhYdmDc/gx1kyg+S+A1ppdEw1K3qGrUUu2VCgI9aQogZE61NY
CBFblMsHfgQyB076ghz/KdzVF+y5KVu61/alFzDkZ7q/z+k3Mn2xaWrcHP/MPReu
7r9FF41O5uPWza/7aQIDAQAB
-----END PUBLIC KEY-----
33 changes: 33 additions & 0 deletions projects/hmpps-auth-and-delius/src/dev/resources/schema.ldif
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
dn: dc=moj,dc=com
dc: moj
objectclass: top
objectclass: domain

dn: ou=Users,dc=moj,dc=com
objectclass: top
objectclass: organizationalUnit
ou: Users

dn: cn=test.user,ou=Users,dc=moj,dc=com
cn: test.user
objectclass: NDUser
objectclass: inetOrgPerson
objectclass: top
givenName: Test
sn: User
userHomeArea: N01
userSector: public
mail: [email protected]
userPassword: secret

dn: cn=ABC001,cn=test.user,ou=Users,dc=moj,dc=com
cn: ABC001
description: Test role 1
objectclass: top
objectClass: NDRoleAssociation

dn: cn=ABC002,cn=test.user,ou=Users,dc=moj,dc=com
cn: ABC002
description: Test role 2
objectclass: top
objectClass: NDRoleAssociation
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJwcm9iYXRpb24taW50ZWdyYXRpb24tZGV2IiwiZ3JhbnRfdHlwZSI6ImNsaWVudF9jcmVkZW50aWFscyIsInVzZXJfbmFtZSI6InByb2JhdGlvbi1pbnRlZ3JhdGlvbi1kZXYiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiYXV0aF9zb3VyY2UiOiJub25lIiwiaXNzIjoiaHR0cHM6Ly9zaWduLWluLWRldi5obXBwcy5zZXJ2aWNlLmp1c3RpY2UuZ292LnVrL2F1dGgvaXNzdWVyIiwiZXhwIjo5OTk5OTk5OTk5LCJhdXRob3JpdGllcyI6WyJST0xFX0VYQU1QTEUiLCJST0xFX1dPUktMT0FEX1JFQUQiLCJST0xFX0FMTE9DQVRJT05fQ09OVEVYVCJdLCJqdGkiOiIyNUR1Um4xLWh5SFpld0xjZEpKeHdWTDAzS1UiLCJjbGllbnRfaWQiOiJwcm9iYXRpb24taW50ZWdyYXRpb24tZGV2IiwiaWF0IjoxNjYzNzU3MzExfQ.5FTCUjA7QZMPxO_EMzkGNSM-IkPk2hfPXyzuNiAa7uuqYva_yCducrC5FdetAiC1W6XpUB7wfoMNDmbW2xepj5oRhcxDx18r92aLPYnKkxaA68hLQF90euMtTzfBzOPg-rKDTNIJKrUC-YoQlFKuCauw0Z5cw1XT6R9GIfi5Yx4",
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJwcm9iYXRpb24taW50ZWdyYXRpb24tZGV2IiwiZ3JhbnRfdHlwZSI6ImNsaWVudF9jcmVkZW50aWFscyIsInVzZXJfbmFtZSI6InByb2JhdGlvbi1pbnRlZ3JhdGlvbi1kZXYiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiYXV0aF9zb3VyY2UiOiJub25lIiwiaXNzIjoiaHR0cHM6Ly9zaWduLWluLWRldi5obXBwcy5zZXJ2aWNlLmp1c3RpY2UuZ292LnVrL2F1dGgvaXNzdWVyIiwiZXhwIjo5OTk5OTk5OTk5LCJhdXRob3JpdGllcyI6WyJST0xFX0RFTElVU19VU0VSX0FVVEgiXSwianRpIjoiMjVEdVJuMS1oeUhaZXdMY2RKSnh3VkwwM0tVIiwiY2xpZW50X2lkIjoicHJvYmF0aW9uLWludGVncmF0aW9uLWRldiIsImlhdCI6MTY2Mzc1NzMxMX0.Xt-xLPTsU6mmADsJKJ8TPVW8-ZC2P3Srw6SbxmnPW53dd_yH-RjX3EFiX9FzHUFW6Was6dWdyRfyPWTva-9SD3y1hPTdwjVU6aRNvG71UH4THz3yXDJQ43amKXi-OLE3B1CaIrPxt3v5jGzGoRcMmQt_OjrEC2nVFx5jr1IS0zc",
"token_type": "bearer",
"expires_in": 9999999999,
"scope": "read write",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package uk.gov.justice.digital.hmpps

import com.github.tomakehurst.wiremock.WireMockServer
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.equalTo
import org.junit.jupiter.api.Test
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.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
import org.springframework.http.MediaType
import org.springframework.ldap.core.AttributesMapper
import org.springframework.ldap.core.LdapTemplate
import org.springframework.test.annotation.DirtiesContext
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import uk.gov.justice.digital.hmpps.security.withOAuth2Token

@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = RANDOM_PORT)
internal class AuthenticationIntegrationTest {
@Autowired
lateinit var mockMvc: MockMvc

@Autowired
lateinit var wireMockServer: WireMockServer

@Autowired
lateinit var ldapTemplate: LdapTemplate

@Test
fun `successful authentication`() {
mockMvc.perform(authenticate("""{"username": "test.user", "password": "secret"}"""))
.andExpect(status().isOk)
}

@Test
fun `failed authentication`() {
mockMvc.perform(authenticate("""{"username": "test.user", "password": "incorrect"}"""))
.andExpect(status().isUnauthorized)
}

@Test
fun `invalid authentication request`() {
mockMvc.perform(authenticate("""{"username": "", "password": " "}"""))
.andExpect(status().isBadRequest)
}

@Test
@DirtiesContext
fun `successful password change`() {
assertThat(currentPassword(), equalTo("secret"))
mockMvc.perform(changePassword("test.user", """{"password": "new"}"""))
.andExpect(status().isOk)
assertThat(currentPassword(), equalTo("new"))
}

@Test
fun `attempt to change password for non-existent user`() {
mockMvc.perform(changePassword("some user", """{"password": "new"}"""))
.andExpect(status().isNotFound)
}

@Test
fun `invalid password change request`() {
mockMvc.perform(changePassword("some user", """{}"""))
.andExpect(status().isBadRequest)
}

private fun authenticate(json: String) = post("/authenticate").withOAuth2Token(wireMockServer)
.contentType(MediaType.APPLICATION_JSON)
.content(json)

private fun changePassword(username: String, json: String) =
post("/user/$username/password").withOAuth2Token(wireMockServer)
.contentType(MediaType.APPLICATION_JSON)
.content(json)

private fun currentPassword() =
ldapTemplate.search("ou=Users", "cn=test.user", AttributesMapper { String(it["userPassword"].get() as ByteArray) })
.toList().single()
}

This file was deleted.

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

import com.fasterxml.jackson.databind.ObjectMapper
import com.github.tomakehurst.wiremock.WireMockServer
import org.junit.jupiter.api.Test
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.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.ResultActions
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import uk.gov.justice.digital.hmpps.model.UserDetails
import uk.gov.justice.digital.hmpps.security.withOAuth2Token

@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = RANDOM_PORT)
internal class UserDetailsIntegrationTest {
@Autowired
lateinit var mockMvc: MockMvc

@Autowired
lateinit var wireMockServer: WireMockServer

@Autowired
lateinit var objectMapper: ObjectMapper

@Test
fun `missing user returns 404`() {
mockMvc.perform(get("/user/does.not.exist").withOAuth2Token(wireMockServer))
.andExpect(status().isNotFound)
}

@Test
fun `get user`() {
mockMvc.perform(get("/user/test.user").withOAuth2Token(wireMockServer))
.andExpect(status().isOk)
.andExpectJson(
UserDetails(
username = "test.user",
firstName = "Test",
surname = "User",
email = "[email protected]",
enabled = true,
roles = listOf("ABC001", "ABC002")
)
)
}

@Test
fun `search by email`() {
mockMvc.perform(get("/[email protected]").withOAuth2Token(wireMockServer))
.andExpect(status().isOk)
.andExpectJson(
listOf(
UserDetails(
username = "test.user",
firstName = "Test",
surname = "User",
email = "[email protected]",
enabled = true,
roles = listOf("ABC001", "ABC002")
)
)
)
}

fun <T> ResultActions.andExpectJson(obj: T) = this.andExpect(content().json(objectMapper.writeValueAsString(obj)))
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package uk.gov.justice.digital.hmpps

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.autoconfigure.data.ldap.LdapRepositoriesAutoConfiguration
import org.springframework.boot.runApplication

@SpringBootApplication
@SpringBootApplication(exclude = [LdapRepositoriesAutoConfiguration::class])
class App

fun main(args: Array<String>) {
Expand Down

This file was deleted.

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

import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.validation.Valid
import org.slf4j.LoggerFactory
import org.springframework.http.ResponseEntity
import org.springframework.ldap.core.LdapTemplate
import org.springframework.ldap.query.LdapQueryBuilder.query
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController
import uk.gov.justice.digital.hmpps.ldap.byUsername
import uk.gov.justice.digital.hmpps.model.AuthenticationRequest

@Validated
@RestController
@Tag(name = "Authentication")
class AuthenticationController(private val ldapTemplate: LdapTemplate) {
companion object {
private val log = LoggerFactory.getLogger(this::class.java)
}

@PostMapping("/authenticate")
@PreAuthorize("hasRole('ROLE_DELIUS_USER_AUTH')")
@Operation(description = "Authenticate a Delius username and password. Requires ROLE_DELIUS_USER_AUTH.")
fun authenticate(
@Valid @RequestBody
request: AuthenticationRequest
) = try {
ldapTemplate.authenticate(query().byUsername(request.username), request.password)
ResponseEntity.ok().build()
} catch (e: Exception) {
log.error("Authentication failed for '${request.username}'", e)
ResponseEntity.status(401).body("Authentication failed for '${request.username}'")
}
}
Loading

0 comments on commit b6d39c2

Please sign in to comment.