Skip to content

Commit

Permalink
PI-1568 add cvl user onboarding endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
anthony-britton-moj committed Oct 31, 2023
1 parent f153cc3 commit b3f9d5c
Show file tree
Hide file tree
Showing 10 changed files with 184 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,53 @@ 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()

fun LdapTemplate.addRole(username: String, 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)
}

fun LdapTemplate.removeRole(username: String, role: DeliusRole) =
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) }
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 b3f9d5c

Please sign in to comment.