diff --git a/libs/commons/src/main/kotlin/uk/gov/justice/digital/hmpps/ldap/DeliusRole.kt b/libs/commons/src/main/kotlin/uk/gov/justice/digital/hmpps/ldap/DeliusRole.kt new file mode 100644 index 0000000000..dfb63bdbaa --- /dev/null +++ b/libs/commons/src/main/kotlin/uk/gov/justice/digital/hmpps/ldap/DeliusRole.kt @@ -0,0 +1,7 @@ +package uk.gov.justice.digital.hmpps.ldap + +interface DeliusRole { + val description: String + val mappedRole: String + val name: String +} diff --git a/libs/commons/src/main/kotlin/uk/gov/justice/digital/hmpps/ldap/LdapTemplateExtensions.kt b/libs/commons/src/main/kotlin/uk/gov/justice/digital/hmpps/ldap/LdapTemplateExtensions.kt index 19d596f43a..dce66e9668 100644 --- a/libs/commons/src/main/kotlin/uk/gov/justice/digital/hmpps/ldap/LdapTemplateExtensions.kt +++ b/libs/commons/src/main/kotlin/uk/gov/justice/digital/hmpps/ldap/LdapTemplateExtensions.kt @@ -8,20 +8,55 @@ 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 +import org.springframework.ldap.support.LdapNameBuilder +import uk.gov.justice.digital.hmpps.exception.NotFoundException +import javax.naming.directory.Attributes +import javax.naming.directory.BasicAttribute +import javax.naming.directory.BasicAttributes -fun LdapQueryBuilder.byUsername(username: String): LdapQuery = base("ou=Users").searchScope(SearchScope.ONELEVEL).where("cn").`is`(username) +private const val ldapBase = "ou=Users" +fun LdapQueryBuilder.byUsername(username: String): LdapQuery = + base(ldapBase).searchScope(SearchScope.ONELEVEL).where("cn").`is`(username) @WithSpan -inline fun LdapTemplate.findByUsername(@SpanAttribute username: String) = find(query().byUsername(username), T::class.java).singleOrNull() +inline fun LdapTemplate.findByUsername(@SpanAttribute username: String) = + find(query().byUsername(username), T::class.java).singleOrNull() @WithSpan fun LdapTemplate.findEmailByUsername(@SpanAttribute username: String) = search( query() .attributes("mail") - .base("ou=Users") + .base(ldapBase) .searchScope(SearchScope.ONELEVEL) .where("objectclass").`is`("inetOrgPerson") .and("objectclass").`is`("top") .and("cn").`is`(username), AttributesMapper { it["mail"]?.get()?.toString() } ).singleOrNull() + +@WithSpan +fun LdapTemplate.addRole(@SpanAttribute username: String, @SpanAttribute role: DeliusRole) { + val roleContext = 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) + rebind(userRole, null, attributes) +} + +@WithSpan +fun LdapTemplate.removeRole(@SpanAttribute username: String, @SpanAttribute role: DeliusRole) = + unbind(role.context(username)) + +private fun DeliusRole.context(username: String? = null) = + LdapNameBuilder.newInstance(ldapBase) + .add("cn", username ?: "ndRoleCatalogue") + .add("cn", name) + .build() + +private fun Any.asAttribute(key: String) = BasicAttribute(key, this.toString()) +private fun List.asAttribute(key: String): BasicAttribute = + BasicAttribute(key).apply { forEach(this::add) } diff --git a/libs/commons/src/test/kotlin/uk/gov/justice/digital/hmpps/ldap/LdapTemplateExtensionsTest.kt b/libs/commons/src/test/kotlin/uk/gov/justice/digital/hmpps/ldap/LdapTemplateExtensionsTest.kt index 25f303e2e3..2b9fb0ed06 100644 --- a/libs/commons/src/test/kotlin/uk/gov/justice/digital/hmpps/ldap/LdapTemplateExtensionsTest.kt +++ b/libs/commons/src/test/kotlin/uk/gov/justice/digital/hmpps/ldap/LdapTemplateExtensionsTest.kt @@ -3,15 +3,21 @@ package uk.gov.justice.digital.hmpps.ldap import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.equalTo import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.springframework.ldap.core.AttributesMapper +import org.springframework.ldap.core.DirContextOperations import org.springframework.ldap.core.LdapTemplate +import uk.gov.justice.digital.hmpps.exception.NotFoundException import uk.gov.justice.digital.hmpps.ldap.entity.LdapUser +import javax.naming.directory.Attributes import javax.naming.ldap.LdapName @ExtendWith(MockitoExtension::class) @@ -19,6 +25,9 @@ class LdapTemplateExtensionsTest { @Mock private lateinit var ldapTemplate: LdapTemplate + @Mock + private lateinit var dirContextOperations: DirContextOperations + @Test fun `find by username`() { val expected = LdapUser(LdapName("cn=test,ou=Users"), "test", "test@example.com") @@ -38,4 +47,65 @@ class LdapTemplateExtensionsTest { assertThat(email, equalTo("test@example.com")) } + + @Test + fun `add role successfully`() { + whenever(ldapTemplate.lookupContext(any())) + .thenReturn(dirContextOperations) + whenever(dirContextOperations.toString()) + .thenReturn("cn=ROLE1,cn=ndRoleCatalogue,ou=Users,dc=moj,dc=com") + + ldapTemplate.addRole( + "john-smith", + object : DeliusRole { + override val description = "Role One Description" + override val mappedRole = "MAPPED_ROLE_ONE" + override val name = "ROLE1" + } + ) + + val nameCapture = argumentCaptor() + val attributeCapture = argumentCaptor() + verify(ldapTemplate).rebind(nameCapture.capture(), eq(null), attributeCapture.capture()) + + assertThat(nameCapture.firstValue.toString(), equalTo("cn=ROLE1,cn=john-smith,ou=Users")) + assertThat(attributeCapture.firstValue["cn"].toString(), equalTo("cn: ROLE1")) + assertThat(attributeCapture.firstValue["objectclass"].toString(), equalTo("objectclass: NDRoleAssociation, Alias, top")) + } + + @Test + fun `unable to add unknown role`() { + whenever(ldapTemplate.lookupContext(any())) + .thenReturn(null) + + val res = assertThrows { + ldapTemplate.addRole( + "john-smith", + object : DeliusRole { + override val description = "Unknown Description" + override val mappedRole = "MAPPED_ROLE_UKN" + override val name = "UNKNOWN" + } + ) + } + + assertThat(res.message, equalTo("NDeliusRole of UNKNOWN not found")) + } + + @Test + fun `remove role successfully`() { + ldapTemplate.removeRole( + "john-smith", + object : DeliusRole { + override val description = "Role One Description" + override val mappedRole = "MAPPED_ROLE_ONE" + override val name = "ROLE1" + } + ) + + val nameCapture = argumentCaptor() + verify(ldapTemplate).unbind(nameCapture.capture()) + + assertThat(nameCapture.firstValue.toString(), equalTo("cn=ROLE1,cn=john-smith,ou=Users")) + } } diff --git a/projects/create-and-vary-a-licence-and-delius/src/dev/resources/schema.ldif b/projects/create-and-vary-a-licence-and-delius/src/dev/resources/schema.ldif index 72f78c7293..23256542a8 100644 --- a/projects/create-and-vary-a-licence-and-delius/src/dev/resources/schema.ldif +++ b/projects/create-and-vary-a-licence-and-delius/src/dev/resources/schema.ldif @@ -20,4 +20,22 @@ objectclass: top objectclass: inetOrgPerson cn: bob-smith sn: Staff -mail: bob.smith@moj.gov.uk \ No newline at end of file +mail: bob.smith@moj.gov.uk + +dn: cn=ndRoleCatalogue,ou=Users,dc=moj,dc=com +description: Role Catalogue +objectclass: top +cn: ndRoleCatalogue + +dn: cn=LHDCBT002,cn=ndRoleCatalogue,ou=Users,dc=moj,dc=com +description: Digital Licence Create +Level1: TRUE +Level2: FALSE +Level3: FALSE +UIBusinessInteractionCollection: SEBI200 +UIBusinessInteractionCollection: SEBI203 +UIBusinessInteractionCollection: SEBI201 +UIBusinessInteractionCollection: SEBI202 +objectClass: NDRole +objectClass: top +cn: LHDCBT002 \ No newline at end of file diff --git a/projects/create-and-vary-a-licence-and-delius/src/dev/resources/simulations/__files/hmpps-auth-token-body.json b/projects/create-and-vary-a-licence-and-delius/src/dev/resources/simulations/__files/hmpps-auth-token-body.json index e23f64a7e1..d02fb7caa4 100644 --- a/projects/create-and-vary-a-licence-and-delius/src/dev/resources/simulations/__files/hmpps-auth-token-body.json +++ b/projects/create-and-vary-a-licence-and-delius/src/dev/resources/simulations/__files/hmpps-auth-token-body.json @@ -1,5 +1,5 @@ { - "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJwcm9iYXRpb24taW50ZWdyYXRpb24tZGV2IiwiZ3JhbnRfdHlwZSI6ImNsaWVudF9jcmVkZW50aWFscyIsInVzZXJfbmFtZSI6InByb2JhdGlvbi1pbnRlZ3JhdGlvbi1kZXYiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiYXV0aF9zb3VyY2UiOiJub25lIiwiaXNzIjoiaHR0cHM6Ly9zaWduLWluLWRldi5obXBwcy5zZXJ2aWNlLmp1c3RpY2UuZ292LnVrL2F1dGgvaXNzdWVyIiwiZXhwIjo5OTk5OTk5OTk5LCJhdXRob3JpdGllcyI6WyJST0xFX0NWTF9DT05URVhUIl0sImp0aSI6IjI1RHVSbjEtaHlIWmV3TGNkSkp4d1ZMMDNLVSIsImNsaWVudF9pZCI6InByb2JhdGlvbi1pbnRlZ3JhdGlvbi1kZXYiLCJpYXQiOjE2NjM3NTczMTF9.WtJQ8l8ifx2F2FPD90Cl03PMLPw9UPdVN2xfqioOB9VKJlyaK8-OoHM_0OxGF3adPRNYXDqR_dHnGUB57EnvjMyWlX5MtgmKIWiEBy5OoZnp1cezmKSEqUopfm7dMWnDT2jJA7xHHQ2kWWjJ9wzG6N3Fa2Z-pWjH-W9omg_GJJo", + "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJwcm9iYXRpb24taW50ZWdyYXRpb24tZGV2IiwiZ3JhbnRfdHlwZSI6ImNsaWVudF9jcmVkZW50aWFscyIsInVzZXJfbmFtZSI6InByb2JhdGlvbi1pbnRlZ3JhdGlvbi1kZXYiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiYXV0aF9zb3VyY2UiOiJub25lIiwiaXNzIjoiaHR0cHM6Ly9zaWduLWluLWRldi5obXBwcy5zZXJ2aWNlLmp1c3RpY2UuZ292LnVrL2F1dGgvaXNzdWVyIiwiZXhwIjo5OTk5OTk5OTk5LCJhdXRob3JpdGllcyI6WyJST0xFX0NWTF9DT05URVhUIiwiUk9MRV9QUk9CQVRJT05fQVBJX19DVkxfX1VTRVJfUk9MRVMiXSwianRpIjoiMjVEdVJuMS1oeUhaZXdMY2RKSnh3VkwwM0tVIiwiY2xpZW50X2lkIjoicHJvYmF0aW9uLWludGVncmF0aW9uLWRldiIsImlhdCI6MTY2Mzc1NzMxMX0.SaqwK1QCp2-GvcninYfbKub7U4_F3i5XFQImkaJe57Z2uhbWV2_m10iuKJnK3ob2GcVwiQeMHzPXBhKN27vCOnden57bVWaeNQMHlPm1tYNTw7vCfRmaQAdn7awxH5DJxwpS-xWJENEhgLdXabUODcwif-0agu-Wgum0EA7C1gE", "token_type": "bearer", "expires_in": 9999999999, "scope": "read write", diff --git a/projects/create-and-vary-a-licence-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/UserRoleIntegrationTest.kt b/projects/create-and-vary-a-licence-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/UserRoleIntegrationTest.kt new file mode 100644 index 0000000000..a72de65c82 --- /dev/null +++ b/projects/create-and-vary-a-licence-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.api.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") + .withOAuth2Token(wireMockServer) + ).andExpect(status().is2xxSuccessful).andReturn() + + val res = ldapTemplate.lookupContext( + LdapNameBuilder.newInstance("ou=Users") + .add("cn", "john-smith") + .add("cn", DeliusRole.LHDCBT002.name) + .build() + ) + assertThat(res.dn.toString(), equalTo("cn=LHDCBT002,cn=john-smith,ou=Users")) + } + + @Order(2) + @Test + fun `successfully removes ldap role`() { + mockMvc.perform( + MockMvcRequestBuilders.delete("/users/john-smith/roles") + .withOAuth2Token(wireMockServer) + ).andExpect(status().is2xxSuccessful).andReturn() + + val res = assertThrows { + ldapTemplate.lookupContext( + LdapNameBuilder.newInstance("ou=Users") + .add("cn", "john-smith") + .add("cn", DeliusRole.LHDCBT002.name) + .build() + ) + } + assertThat(res.message, equalTo("[LDAP: error code 32 - Unable to perform the search because base entry 'cn=LHDCBT002,cn=john-smith,ou=Users,dc=moj,dc=com' does not exist in the server.]")) + } +} diff --git a/projects/create-and-vary-a-licence-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/DeliusRole.kt b/projects/create-and-vary-a-licence-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/DeliusRole.kt new file mode 100644 index 0000000000..b35c7ff68d --- /dev/null +++ b/projects/create-and-vary-a-licence-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/DeliusRole.kt @@ -0,0 +1,12 @@ +package uk.gov.justice.digital.hmpps.api.model + +enum class DeliusRole( + override val description: String, + override val mappedRole: String +) : uk.gov.justice.digital.hmpps.ldap.DeliusRole { + LHDCBT002("Digital Licence Create", "CVL_DLC"); + + companion object { + fun from(role: String): DeliusRole? = entries.firstOrNull { it.mappedRole == role.uppercase() } + } +} diff --git a/projects/create-and-vary-a-licence-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/resource/UserResource.kt b/projects/create-and-vary-a-licence-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/resource/UserResource.kt new file mode 100644 index 0000000000..7348aae587 --- /dev/null +++ b/projects/create-and-vary-a-licence-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/resource/UserResource.kt @@ -0,0 +1,26 @@ +package uk.gov.justice.digital.hmpps.api.resource + +import org.springframework.ldap.core.LdapTemplate +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 uk.gov.justice.digital.hmpps.api.model.DeliusRole +import uk.gov.justice.digital.hmpps.ldap.addRole +import uk.gov.justice.digital.hmpps.ldap.removeRole + +@RestController +@RequestMapping("users") +class UserResource(private val ldapTemplate: LdapTemplate) { + @PreAuthorize("hasRole('PROBATION_API__CVL__USER_ROLES')") + @PutMapping(value = ["/{username}/roles"]) + fun addRole(@PathVariable username: String) = + ldapTemplate.addRole(username, DeliusRole.LHDCBT002) + + @PreAuthorize("hasRole('PROBATION_API__CVL__USER_ROLES')") + @DeleteMapping(value = ["/{username}/roles"]) + fun removeRole(@PathVariable username: String) = + ldapTemplate.removeRole(username, DeliusRole.LHDCBT002) +} diff --git a/projects/create-and-vary-a-licence-and-delius/src/main/resources/application.yml b/projects/create-and-vary-a-licence-and-delius/src/main/resources/application.yml index 880f7ad57c..e1bdb8b674 100644 --- a/projects/create-and-vary-a-licence-and-delius/src/main/resources/application.yml +++ b/projects/create-and-vary-a-licence-and-delius/src/main/resources/application.yml @@ -53,7 +53,9 @@ spring.config.activate.on-profile: [ "dev", "integration-test" ] 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 - ldap.embedded.base-dn: ${spring.ldap.base} + ldap.embedded: + validation.enabled: false + base-dn: ${spring.ldap.base} security.oauth2.resourceserver.jwt.public-key-location: classpath:local-public-key.pub seed.database: true 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 index afe2c8b992..1d9b4113d8 100644 --- 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 @@ -1,6 +1,9 @@ package uk.gov.justice.digital.hmpps.model -enum class DeliusRole(val description: String, val role: String) { +enum class DeliusRole( + override val description: String, + override val mappedRole: String +) : uk.gov.justice.digital.hmpps.ldap.DeliusRole { CTRBT001("Pathfinder CT Probation", "PF_STD_PROBATION"), CTRBT002("Pathfinder CT Approval", "PF_APPROVAL"), CTRBT003("Pathfinder National Reader", "PF_NATIONAL_READER"), @@ -9,6 +12,6 @@ enum class DeliusRole(val description: String, val role: String) { CTRBT006("Pathfinder Admin", "PF_ADMIN"); companion object { - fun from(role: String): DeliusRole? = entries.firstOrNull { it.role == role.uppercase() } + fun from(role: String): DeliusRole? = entries.firstOrNull { it.mappedRole == 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 index 1fbfedf129..00e55e5a78 100644 --- 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 @@ -1,40 +1,15 @@ 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.ldap.addRole +import uk.gov.justice.digital.hmpps.ldap.removeRole 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 addRole(username: String, role: DeliusRole) = ldapTemplate.addRole(username, role) - 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) } + fun removeRole(username: String, role: DeliusRole) = ldapTemplate.removeRole(username, role) }