Skip to content

Commit

Permalink
PI-1568 add cvl user onboarding endpoints (#2469)
Browse files Browse the repository at this point in the history
* PI-1568 add cvl user onboarding endpoints

* PI-1568 unit tests
  • Loading branch information
anthony-britton-moj authored Oct 31, 2023
1 parent f153cc3 commit 518599d
Show file tree
Hide file tree
Showing 11 changed files with 256 additions and 37 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package uk.gov.justice.digital.hmpps.ldap

interface DeliusRole {
val description: String
val mappedRole: String
val name: String
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 <reified T> LdapTemplate.findByUsername(@SpanAttribute username: String) = find(query().byUsername(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(
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<Any>.asAttribute(key: String): BasicAttribute =
BasicAttribute(key).apply { forEach(this::add) }
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,31 @@ 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)
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", "[email protected]")
Expand All @@ -38,4 +47,65 @@ class LdapTemplateExtensionsTest {

assertThat(email, equalTo("[email protected]"))
}

@Test
fun `add role successfully`() {
whenever(ldapTemplate.lookupContext(any<LdapName>()))
.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<LdapName>()
val attributeCapture = argumentCaptor<Attributes>()
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<LdapName>()))
.thenReturn(null)

val res = assertThrows<NotFoundException> {
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<LdapName>()
verify(ldapTemplate).unbind(nameCapture.capture())

assertThat(nameCapture.firstValue.toString(), equalTo("cn=ROLE1,cn=john-smith,ou=Users"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,22 @@ objectclass: top
objectclass: inetOrgPerson
cn: bob-smith
sn: Staff
mail: [email protected]
mail: [email protected]

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
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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<NameNotFoundException> {
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.]"))
}
}
Original file line number Diff line number Diff line change
@@ -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() }
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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"),
Expand All @@ -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() }
}
}
Original file line number Diff line number Diff line change
@@ -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<Any>.asAttribute(key: String): BasicAttribute =
BasicAttribute(key).apply { forEach(this::add) }
fun removeRole(username: String, role: DeliusRole) = ldapTemplate.removeRole(username, role)
}

0 comments on commit 518599d

Please sign in to comment.