diff --git a/projects/pathfinder-and-delius/build.gradle.kts b/projects/pathfinder-and-delius/build.gradle.kts index a089a2f210..b4a7fa255e 100644 --- a/projects/pathfinder-and-delius/build.gradle.kts +++ b/projects/pathfinder-and-delius/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-validation") implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-data-ldap") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") @@ -20,6 +21,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") diff --git a/projects/pathfinder-and-delius/deploy/values.yaml b/projects/pathfinder-and-delius/deploy/values.yaml index 495b23be8f..5d732bc329 100644 --- a/projects/pathfinder-and-delius/deploy/values.yaml +++ b/projects/pathfinder-and-delius/deploy/values.yaml @@ -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 pathfinder-and-delius-database: SPRING_DATASOURCE_USERNAME: DB_USERNAME SPRING_DATASOURCE_PASSWORD: DB_PASSWORD diff --git a/projects/pathfinder-and-delius/src/dev/resources/schema.ldif b/projects/pathfinder-and-delius/src/dev/resources/schema.ldif new file mode 100644 index 0000000000..eebd1fd4f1 --- /dev/null +++ b/projects/pathfinder-and-delius/src/dev/resources/schema.ldif @@ -0,0 +1,34 @@ +dn: dc=moj,dc=com +objectclass: top +objectclass: domain +dc: moj + +dn: ou=Users,dc=moj,dc=com +objectclass: top +objectclass: organizationalUnit +ou: Users + +dn: cn=john-smith,ou=Users,dc=moj,dc=com +objectclass: top +objectclass: inetOrgPerson +cn: john-smith +sn: Smith +givenname: John +mail: john.smith@moj.gov.uk +telephoneNumber: 07321165373 + +dn: cn=ndRoleCatalogue,ou=Users,dc=moj,dc=com +description: Role Catalogue +objectclass: top +cn: ndRoleCatalogue + +dn: cn=CTRBT001,cn=ndRoleCatalogue,ou=Users,dc=moj,dc=com +description: Pathfinder CT Probation +Sector: public +Level1: FALSE +Level2: FALSE +Level3: FALSE +UIBusinessInteractionCollection: UPBI003 +objectclass: NDRole +objectclass: top +cn: CTRBT001 diff --git a/projects/pathfinder-and-delius/src/dev/resources/simulations/__files/hmpps-auth-token-body.json b/projects/pathfinder-and-delius/src/dev/resources/simulations/__files/hmpps-auth-token-body.json index ddbcb50b08..3d7ce8a126 100644 --- a/projects/pathfinder-and-delius/src/dev/resources/simulations/__files/hmpps-auth-token-body.json +++ b/projects/pathfinder-and-delius/src/dev/resources/simulations/__files/hmpps-auth-token-body.json @@ -1,5 +1,5 @@ { - "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJwcm9iYXRpb24taW50ZWdyYXRpb24tZGV2IiwiZ3JhbnRfdHlwZSI6ImNsaWVudF9jcmVkZW50aWFscyIsInVzZXJfbmFtZSI6InByb2JhdGlvbi1pbnRlZ3JhdGlvbi1kZXYiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiYXV0aF9zb3VyY2UiOiJub25lIiwiaXNzIjoiaHR0cHM6Ly9zaWduLWluLWRldi5obXBwcy5zZXJ2aWNlLmp1c3RpY2UuZ292LnVrL2F1dGgvaXNzdWVyIiwiZXhwIjo5OTk5OTk5OTk5LCJhdXRob3JpdGllcyI6WyJST0xFX1BBVEhGSU5ERVJfUFJPQkFUSU9OX0NBU0UiXSwianRpIjoiMjVEdVJuMS1oeUhaZXdMY2RKSnh3VkwwM0tVIiwiY2xpZW50X2lkIjoicHJvYmF0aW9uLWludGVncmF0aW9uLWRldiIsImlhdCI6MTY2Mzc1NzMxMX0.HHzOUbZGvhQwcSs-ZJinXOYHProob0N0d_zlE9bCBcae5J7l0EhvlpSvfohN5gJg39RNcu-f1wZZqgBIFMuljWs909FbubVDPyEOpQxY2bim6nenFZcUGCOU8SAz_Tf2yxFf6vTgiwV1TEoasrsKnGcCnVuo47JYuvpjwcSluAY", + "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJwcm9iYXRpb24taW50ZWdyYXRpb24tZGV2IiwiZ3JhbnRfdHlwZSI6ImNsaWVudF9jcmVkZW50aWFscyIsInVzZXJfbmFtZSI6InByb2JhdGlvbi1pbnRlZ3JhdGlvbi1kZXYiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiYXV0aF9zb3VyY2UiOiJub25lIiwiaXNzIjoiaHR0cHM6Ly9zaWduLWluLWRldi5obXBwcy5zZXJ2aWNlLmp1c3RpY2UuZ292LnVrL2F1dGgvaXNzdWVyIiwiZXhwIjo5OTk5OTk5OTk5LCJhdXRob3JpdGllcyI6WyJST0xFX1BBVEhGSU5ERVJfUFJPQkFUSU9OX0NBU0UiLCJST0xFX1BST0JBVElPTl9BUElfX1BBVEhGSU5ERVJfX1VTRVJfUk9MRVMiXSwianRpIjoiMjVEdVJuMS1oeUhaZXdMY2RKSnh3VkwwM0tVIiwiY2xpZW50X2lkIjoicHJvYmF0aW9uLWludGVncmF0aW9uLWRldiIsImlhdCI6MTY2Mzc1NzMxMX0.SUwMkVn868al1ry9P69KLz0pk1ayrxprnEW-6xMAD0nJaJN1gW4acCzwCguHrn7o8jX18gXD9dZAy5rJvcsA4hX2XKWav8FdUmOXI7ePqkgbNOMhjzzb5FVm_6GO_QNIwQUAWSi6o-69sjbaNpYZOvgkwYjJ1GhZy6WcVgaQgjU", "token_type": "bearer", "expires_in": 9999999999, "scope": "read write", diff --git a/projects/pathfinder-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/UserRoleIntegrationTest.kt b/projects/pathfinder-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/UserRoleIntegrationTest.kt new file mode 100644 index 0000000000..3443350ce4 --- /dev/null +++ b/projects/pathfinder-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/UserRoleIntegrationTest.kt @@ -0,0 +1,71 @@ +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.MethodOrderer +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestMethodOrder +import org.junit.jupiter.api.assertThrows +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.ldap.NameNotFoundException +import org.springframework.ldap.core.LdapTemplate +import org.springframework.ldap.support.LdapNameBuilder +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import uk.gov.justice.digital.hmpps.model.DeliusRole +import uk.gov.justice.digital.hmpps.security.withOAuth2Token + +@AutoConfigureMockMvc +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestMethodOrder(MethodOrderer.OrderAnnotation::class) +internal class UserRoleIntegrationTest { + @Autowired + lateinit var mockMvc: MockMvc + + @Autowired + lateinit var wireMockServer: WireMockServer + + @Autowired + lateinit var ldapTemplate: LdapTemplate + + @Order(1) + @Test + fun `successfully updates ldap role`() { + mockMvc.perform( + MockMvcRequestBuilders.put("/users/john-smith/roles/pf_std_probation") + .withOAuth2Token(wireMockServer) + ).andExpect(status().is2xxSuccessful).andReturn() + + val res = ldapTemplate.lookupContext( + LdapNameBuilder.newInstance("ou=Users") + .add("cn", "john-smith") + .add("cn", DeliusRole.CTRBT001.name) + .build() + ) + assertThat(res.dn.toString(), equalTo("cn=CTRBT001,cn=john-smith,ou=Users")) + } + + @Order(2) + @Test + fun `successfully removes ldap role`() { + mockMvc.perform( + MockMvcRequestBuilders.delete("/users/john-smith/roles/pf_std_probation") + .withOAuth2Token(wireMockServer) + ).andExpect(status().is2xxSuccessful).andReturn() + + val res = assertThrows { + ldapTemplate.lookupContext( + LdapNameBuilder.newInstance("ou=Users") + .add("cn", "john-smith") + .add("cn", DeliusRole.CTRBT001.name) + .build() + ) + } + assertThat(res.message, equalTo("[LDAP: error code 32 - Unable to perform the search because base entry 'cn=CTRBT001,cn=john-smith,ou=Users,dc=moj,dc=com' does not exist in the server.]")) + } +} diff --git a/projects/pathfinder-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/App.kt b/projects/pathfinder-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/App.kt index c7faac5b26..2c6b3789ed 100644 --- a/projects/pathfinder-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/App.kt +++ b/projects/pathfinder-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/App.kt @@ -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) { diff --git a/projects/pathfinder-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/controller/IdentifierType.kt b/projects/pathfinder-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/controller/IdentifierType.kt deleted file mode 100644 index 18f9b3181d..0000000000 --- a/projects/pathfinder-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/controller/IdentifierType.kt +++ /dev/null @@ -1,5 +0,0 @@ -package uk.gov.justice.digital.hmpps.controller - -enum class IdentifierType { - CRN, NOMS -} diff --git a/projects/pathfinder-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/controller/UserController.kt b/projects/pathfinder-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/controller/UserController.kt new file mode 100644 index 0000000000..77cf1baac5 --- /dev/null +++ b/projects/pathfinder-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/controller/UserController.kt @@ -0,0 +1,29 @@ +package uk.gov.justice.digital.hmpps.controller + +import org.springframework.http.HttpStatus +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PutMapping +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.model.DeliusRole +import uk.gov.justice.digital.hmpps.service.UserService + +@RestController +@RequestMapping("users") +class UserController(private val userService: UserService) { + @PreAuthorize("hasRole('PROBATION_API__PATHFINDER__USER_ROLES')") + @PutMapping(value = ["/{username}/roles/{roleName}"]) + fun addRole(@PathVariable username: String, @PathVariable roleName: String) = + userService.addRole(username, roleName.deliusRole()) + + @PreAuthorize("hasRole('PROBATION_API__PATHFINDER__USER_ROLES')") + @DeleteMapping(value = ["/{username}/roles/{roleName}"]) + fun removeRole(@PathVariable username: String, @PathVariable roleName: String) = + userService.removeRole(username, roleName.deliusRole()) +} + +private fun String.deliusRole() = + DeliusRole.from(this) ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Role Not Acceptable") diff --git a/projects/pathfinder-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/model/DeliusRole.kt b/projects/pathfinder-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/model/DeliusRole.kt new file mode 100644 index 0000000000..afe2c8b992 --- /dev/null +++ b/projects/pathfinder-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/model/DeliusRole.kt @@ -0,0 +1,14 @@ +package uk.gov.justice.digital.hmpps.model + +enum class DeliusRole(val description: String, val role: String) { + CTRBT001("Pathfinder CT Probation", "PF_STD_PROBATION"), + CTRBT002("Pathfinder CT Approval", "PF_APPROVAL"), + CTRBT003("Pathfinder National Reader", "PF_NATIONAL_READER"), + CTRBT004("Pathfinder HQ User", "PF_HQ"), + CTRBT005("Pathfinder User", "PF_USER"), + CTRBT006("Pathfinder Admin", "PF_ADMIN"); + + companion object { + fun from(role: String): DeliusRole? = entries.firstOrNull { it.role == role.uppercase() } + } +} diff --git a/projects/pathfinder-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/UserService.kt b/projects/pathfinder-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/UserService.kt new file mode 100644 index 0000000000..1fbfedf129 --- /dev/null +++ b/projects/pathfinder-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/UserService.kt @@ -0,0 +1,40 @@ +package uk.gov.justice.digital.hmpps.service + +import org.springframework.ldap.core.LdapTemplate +import org.springframework.ldap.support.LdapNameBuilder +import org.springframework.stereotype.Service +import uk.gov.justice.digital.hmpps.exception.NotFoundException +import uk.gov.justice.digital.hmpps.model.DeliusRole +import javax.naming.directory.Attributes +import javax.naming.directory.BasicAttribute +import javax.naming.directory.BasicAttributes + +@Service +class UserService(private val ldapTemplate: LdapTemplate) { + private val ldapBase = "ou=Users" + + fun addRole(username: String, role: DeliusRole) { + val roleContext = ldapTemplate.lookupContext(role.context()) + ?: throw NotFoundException("NDeliusRole of ${role.name} not found") + val attributes: Attributes = BasicAttributes(true).apply { + put(roleContext.asAttribute("aliasedObjectName")) + put(role.name.asAttribute("cn")) + put(listOf("NDRoleAssociation", "Alias", "top").asAttribute("objectclass")) + } + val userRole = role.context(username) + ldapTemplate.rebind(userRole, null, attributes) + } + + fun removeRole(username: String, role: DeliusRole) = + ldapTemplate.unbind(role.context(username)) + + private fun DeliusRole.context(username: String? = null) = + LdapNameBuilder.newInstance(ldapBase) + .add("cn", username ?: "ndRoleCatalogue") + .add("cn", name) + .build() + + fun Any.asAttribute(key: String) = BasicAttribute(key, this.toString()) + fun List.asAttribute(key: String): BasicAttribute = + BasicAttribute(key).apply { forEach(this::add) } +} diff --git a/projects/pathfinder-and-delius/src/main/resources/application.yml b/projects/pathfinder-and-delius/src/main/resources/application.yml index f6b6e6e351..a712df46af 100644 --- a/projects/pathfinder-and-delius/src/main/resources/application.yml +++ b/projects/pathfinder-and-delius/src/main/resources/application.yml @@ -16,6 +16,10 @@ spring: global_temporary: create_tables: false drop_tables: false + ldap: + base: dc=moj,dc=com + base-environment: + java.naming.ldap.derefAliases: never security.oauth2.client: registration: pathfinder-and-delius: @@ -42,6 +46,9 @@ spring: datasource.url: jdbc:h2:file:./dev;MODE=Oracle;DEFAULT_NULL_ORDERING=HIGH;AUTO_SERVER=true;AUTO_SERVER_PORT=9092 jpa.hibernate.ddl-auto: create-drop security.oauth2.resourceserver.jwt.public-key-location: classpath:local-public-key.pub + ldap.embedded: + validation.enabled: false + base-dn: ${spring.ldap.base} seed.database: true wiremock.enabled: true