diff --git a/projects/workforce-allocations-to-delius/deploy/values.yaml b/projects/workforce-allocations-to-delius/deploy/values.yaml index 68099f0d27..cde9935ed8 100644 --- a/projects/workforce-allocations-to-delius/deploy/values.yaml +++ b/projects/workforce-allocations-to-delius/deploy/values.yaml @@ -19,7 +19,7 @@ generic-service: common: SPRING_DATASOURCE_URL: DB_URL SPRING_LDAP_URLS: LDAP_URL - SPRING_LDAP_USERNAME: LDAP_USERNAME + SPRING_LDAP_USERNAME: LDAP_ROOT_USERNAME SPRING_LDAP_PASSWORD: LDAP_PASSWORD workforce-allocations-to-delius-client-credentials: INTEGRATIONS_WORKFORCE-ALLOCATIONS_CLIENT-ID: CLIENT_ID diff --git a/projects/workforce-allocations-to-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/LimitedAccessDataLoader.kt b/projects/workforce-allocations-to-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/LimitedAccessDataLoader.kt index e39bdd1187..b98b91aea8 100644 --- a/projects/workforce-allocations-to-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/LimitedAccessDataLoader.kt +++ b/projects/workforce-allocations-to-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/LimitedAccessDataLoader.kt @@ -1,16 +1,15 @@ package uk.gov.justice.digital.hmpps.data import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty -import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Component import uk.gov.justice.digital.hmpps.data.generator.LimitedAccessGenerator import uk.gov.justice.digital.hmpps.data.generator.LimitedAccessGenerator.generateExclusion import uk.gov.justice.digital.hmpps.data.generator.LimitedAccessGenerator.generateRestriction import uk.gov.justice.digital.hmpps.data.generator.PersonGenerator import uk.gov.justice.digital.hmpps.data.generator.UserGenerator -import uk.gov.justice.digital.hmpps.entity.Exclusion -import uk.gov.justice.digital.hmpps.entity.Restriction import uk.gov.justice.digital.hmpps.integrations.delius.person.PersonRepository +import uk.gov.justice.digital.hmpps.integrations.delius.user.ExclusionRepository +import uk.gov.justice.digital.hmpps.integrations.delius.user.RestrictionRepository import uk.gov.justice.digital.hmpps.user.AuditUserRepository @Component @@ -37,6 +36,3 @@ class LimitedAccessDataLoader( restrictionRepository.save(generateRestriction(person = PersonGenerator.RESTRICTION_EXCLUSION)) } } - -interface ExclusionRepository : JpaRepository -interface RestrictionRepository : JpaRepository diff --git a/projects/workforce-allocations-to-delius/src/dev/resources/schema.ldif b/projects/workforce-allocations-to-delius/src/dev/resources/schema.ldif index 41db670397..c366992763 100644 --- a/projects/workforce-allocations-to-delius/src/dev/resources/schema.ldif +++ b/projects/workforce-allocations-to-delius/src/dev/resources/schema.ldif @@ -9,3 +9,9 @@ objectclass: inetOrgPerson cn: JoeBloggs sn: Bloggs mail: example@example.com + +dn: cn=MAABT001,cn=JoeBloggs,ou=Users,dc=moj,dc=com +objectclass: NDRoleAssociation +objectclass: alias +cn: MAABT001 +aliasedObjectName: cn=MAABT001,cn=NDRoleAssociation,ou=Users,dc=moj,dc=com diff --git a/projects/workforce-allocations-to-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/LimitedAccessIntegrationTest.kt b/projects/workforce-allocations-to-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/UserIntegrationTest.kt similarity index 62% rename from projects/workforce-allocations-to-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/LimitedAccessIntegrationTest.kt rename to projects/workforce-allocations-to-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/UserIntegrationTest.kt index 4ef4b48801..92d8e45c29 100644 --- a/projects/workforce-allocations-to-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/LimitedAccessIntegrationTest.kt +++ b/projects/workforce-allocations-to-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/UserIntegrationTest.kt @@ -2,26 +2,40 @@ package uk.gov.justice.digital.hmpps import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.equalToIgnoringCase 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.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import uk.gov.justice.digital.hmpps.api.model.CaseAccess +import uk.gov.justice.digital.hmpps.api.model.CaseAccessList +import uk.gov.justice.digital.hmpps.api.model.User import uk.gov.justice.digital.hmpps.api.model.UserAccess import uk.gov.justice.digital.hmpps.data.generator.PersonGenerator import uk.gov.justice.digital.hmpps.data.generator.UserGenerator +import uk.gov.justice.digital.hmpps.test.MockMvcExtensions.andExpectJson import uk.gov.justice.digital.hmpps.test.MockMvcExtensions.contentAsJson import uk.gov.justice.digital.hmpps.test.MockMvcExtensions.withJson import uk.gov.justice.digital.hmpps.test.MockMvcExtensions.withToken @AutoConfigureMockMvc @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -class LimitedAccessIntegrationTest { +class UserIntegrationTest { @Autowired lateinit var mockMvc: MockMvc + @Test + fun `get all users`() { + mockMvc.perform(get("/users").withToken()) + .andExpect(status().is2xxSuccessful) + .andExpect(jsonPath("$[0].username", equalToIgnoringCase("JoeBloggs"))) + } + @Test fun `limited access controls are correctly returned`() { val result = mockMvc.perform( @@ -110,4 +124,49 @@ class LimitedAccessIntegrationTest { equalTo(CaseAccess(PersonGenerator.DEFAULT.crn, userExcluded = false, userRestricted = false)) ) } + + @Test + fun `get all access limitations`() { + mockMvc.perform(get("/person/${PersonGenerator.RESTRICTION_EXCLUSION.crn}/limited-access/all").withToken()) + .andExpectJson( + CaseAccessList( + crn = PersonGenerator.RESTRICTION_EXCLUSION.crn, + excludedFrom = listOf(User(UserGenerator.LIMITED_ACCESS_USER.username)), + restrictedTo = listOf(User(UserGenerator.AUDIT_USER.username)), + exclusionMessage = PersonGenerator.RESTRICTION_EXCLUSION.exclusionMessage, + restrictionMessage = PersonGenerator.RESTRICTION_EXCLUSION.restrictionMessage, + ) + ) + } + + @Test + fun `get all access limitations filtered by username`() { + mockMvc.perform( + post("/person/${PersonGenerator.RESTRICTION_EXCLUSION.crn}/limited-access") + .withToken() + .withJson(listOf(UserGenerator.AUDIT_USER.username)) + ).andExpectJson( + CaseAccessList( + crn = PersonGenerator.RESTRICTION_EXCLUSION.crn, + excludedFrom = emptyList(), + restrictedTo = listOf(User(UserGenerator.AUDIT_USER.username)), + exclusionMessage = PersonGenerator.RESTRICTION_EXCLUSION.exclusionMessage, + restrictionMessage = PersonGenerator.RESTRICTION_EXCLUSION.restrictionMessage, + ) + ) + + mockMvc.perform( + post("/person/${PersonGenerator.RESTRICTION_EXCLUSION.crn}/limited-access") + .withToken() + .withJson(listOf("SomeOtherUsername")) + ).andExpectJson( + CaseAccessList( + crn = PersonGenerator.RESTRICTION_EXCLUSION.crn, + excludedFrom = emptyList(), + restrictedTo = emptyList(), + exclusionMessage = PersonGenerator.RESTRICTION_EXCLUSION.exclusionMessage, + restrictionMessage = PersonGenerator.RESTRICTION_EXCLUSION.restrictionMessage, + ) + ) + } } diff --git a/projects/workforce-allocations-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/SharedModels.kt b/projects/workforce-allocations-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/SharedModels.kt index c4ca525de9..6ff77bbefe 100644 --- a/projects/workforce-allocations-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/SharedModels.kt +++ b/projects/workforce-allocations-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/SharedModels.kt @@ -14,6 +14,8 @@ data class Name( data class Event(val number: String, val manager: Manager? = null) data class Sentence(val type: String, val date: LocalDate, val length: String) +data class User(val username: String) + data class StaffMember( val code: String, val name: Name, diff --git a/projects/workforce-allocations-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/UserAccess.kt b/projects/workforce-allocations-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/UserAccess.kt index e1a95462ef..908cae6e13 100644 --- a/projects/workforce-allocations-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/UserAccess.kt +++ b/projects/workforce-allocations-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/UserAccess.kt @@ -9,3 +9,11 @@ data class CaseAccess( val exclusionMessage: String? = null, val restrictionMessage: String? = null ) + +data class CaseAccessList( + val crn: String, + val excludedFrom: List, + val restrictedTo: List, + val exclusionMessage: String? = null, + val restrictionMessage: String? = null, +) diff --git a/projects/workforce-allocations-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/resource/UserResource.kt b/projects/workforce-allocations-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/resource/UserResource.kt index 1a60ca10bd..09f7da79dc 100644 --- a/projects/workforce-allocations-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/resource/UserResource.kt +++ b/projects/workforce-allocations-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/resource/UserResource.kt @@ -3,16 +3,18 @@ package uk.gov.justice.digital.hmpps.api.resource import io.swagger.v3.oas.annotations.Operation import jakarta.validation.constraints.Size import org.springframework.security.access.prepost.PreAuthorize -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestMethod -import org.springframework.web.bind.annotation.RequestParam -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* +import uk.gov.justice.digital.hmpps.api.model.User +import uk.gov.justice.digital.hmpps.service.LdapService import uk.gov.justice.digital.hmpps.service.UserAccessService +import uk.gov.justice.digital.hmpps.service.UserService @RestController -@RequestMapping("/users") -class UserResource(private val userAccessService: UserAccessService) { +class UserResource( + private val userAccessService: UserAccessService, + private val userService: UserService, + private val ldapService: LdapService, +) { @PreAuthorize("hasRole('PROBATION_API__WORKFORCE_ALLOCATIONS__CASE_DETAIL')") @Operation( @@ -27,9 +29,25 @@ class UserResource(private val userAccessService: UserAccessService) { has a restriction in place """ ) - @RequestMapping("/limited-access", method = [RequestMethod.GET, RequestMethod.POST]) + @RequestMapping("/users/limited-access", method = [RequestMethod.GET, RequestMethod.POST]) fun limitedAccessCheck( @Size(min = 1, max = 500, message = "Please provide between 1 and 500 crns") @RequestBody crns: List, @RequestParam(required = false) username: String? ) = username?.let { userAccessService.userAccessFor(it, crns) } ?: userAccessService.checkLimitedAccessFor(crns) + + @GetMapping("/users") + @Operation(summary = "Returns all users with the Delius `MAABT001` role") + fun allUsers() = ldapService.findAllUsersWithRole().map { User(it) } + + @GetMapping("/person/{crn}/limited-access/all") + @Operation(summary = "Returns all limited access information (restrictions and exclusions) for a Delius CRN") + fun allAccessLimitationsForCrn(@PathVariable crn: String) = userService.getAllAccessLimitations(crn) + + @PostMapping("/person/{crn}/limited-access") + @Operation(summary = "Returns limited access information (restrictions and exclusions) for a Delius CRN, given a list of staff codes") + fun allAccessLimitationsForCrnAndUserList( + @PathVariable crn: String, + @Size(min = 0, max = 500, message = "Please provide up to 500 usernames to filter by") + @RequestBody(required = false) usernames: List? = null, + ) = userService.getAllAccessLimitations(crn, usernames) } diff --git a/projects/workforce-allocations-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/user/LimitedAccess.kt b/projects/workforce-allocations-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/user/LimitedAccess.kt new file mode 100644 index 0000000000..0cec0467e3 --- /dev/null +++ b/projects/workforce-allocations-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/user/LimitedAccess.kt @@ -0,0 +1,16 @@ +package uk.gov.justice.digital.hmpps.integrations.delius.user + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import uk.gov.justice.digital.hmpps.entity.Exclusion +import uk.gov.justice.digital.hmpps.entity.Restriction + +interface ExclusionRepository : JpaRepository { + @Query("select e from Exclusion e where e.person.id = :personId and (e.end is null or e.end > current_date)") + fun findByPersonId(personId: Long): List +} + +interface RestrictionRepository : JpaRepository { + @Query("select r from Restriction r where r.person.id = :personId and (r.end is null or r.end > current_date)") + fun findByPersonId(personId: Long): List +} \ No newline at end of file diff --git a/projects/workforce-allocations-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/LdapService.kt b/projects/workforce-allocations-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/LdapService.kt index 2cc51f9f8f..25b902bb13 100644 --- a/projects/workforce-allocations-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/LdapService.kt +++ b/projects/workforce-allocations-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/LdapService.kt @@ -2,11 +2,13 @@ package uk.gov.justice.digital.hmpps.service 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.filter.EqualsFilter import org.springframework.ldap.filter.OrFilter import org.springframework.ldap.query.LdapQueryBuilder import org.springframework.ldap.query.SearchScope +import org.springframework.ldap.support.LdapUtils import org.springframework.stereotype.Service import uk.gov.justice.digital.hmpps.integrations.delius.provider.StaffWithUser import uk.gov.justice.digital.hmpps.integrations.delius.user.LdapUser @@ -23,16 +25,28 @@ class LdapService(private val ldapTemplate: LdapTemplate) { staff?.user?.username?.let { ldapTemplate.findEmailByUsername(it) } @WithSpan - fun findEmailsForStaffIn(@SpanAttribute staff: List) = staff.mapNotNull { it.user?.username } - .distinct() - .chunked(LDAP_MAX_RESULTS_PER_QUERY) - .flatMap { - val filter = it.map { username -> EqualsFilter("cn", username) }.fold(OrFilter()) { a, b -> a.or(b) } - val query = LdapQueryBuilder.query() - .attributes("mail") - .searchScope(SearchScope.ONELEVEL) - .filter(filter) - ldapTemplate.find(query, LdapUser::class.java) + fun findEmailsForStaffIn(@SpanAttribute staff: List) = + staff.asSequence().mapNotNull { it.user?.username } + .distinct() + .chunked(LDAP_MAX_RESULTS_PER_QUERY) + .flatMap { + val filter = it.map { username -> EqualsFilter("cn", username) }.fold(OrFilter()) { a, b -> a.or(b) } + val query = LdapQueryBuilder.query() + .attributes("mail") + .searchScope(SearchScope.ONELEVEL) + .filter(filter) + ldapTemplate.find(query, LdapUser::class.java) + } + .associate { it.username to it.email } + + @WithSpan + fun findAllUsersWithRole(role: String = "MAABT001"): List = ldapTemplate.search(LdapQueryBuilder.query() + .attributes("entryDN") + .searchScope(SearchScope.SUBTREE) + .where("cn").`is`(role) + .and("objectclass").`is`("NDRoleAssociation"), + AttributesMapper { + LdapUtils.getStringValue(LdapUtils.newLdapName(it["entryDN"]?.get()?.toString()), 3) } - .associate { it.username to it.email } + ) } diff --git a/projects/workforce-allocations-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/UserService.kt b/projects/workforce-allocations-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/UserService.kt new file mode 100644 index 0000000000..e95c340fb4 --- /dev/null +++ b/projects/workforce-allocations-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/UserService.kt @@ -0,0 +1,32 @@ +package uk.gov.justice.digital.hmpps.service + +import org.springframework.stereotype.Service +import uk.gov.justice.digital.hmpps.api.model.CaseAccessList +import uk.gov.justice.digital.hmpps.api.model.User +import uk.gov.justice.digital.hmpps.exception.NotFoundException +import uk.gov.justice.digital.hmpps.integrations.delius.person.PersonRepository +import uk.gov.justice.digital.hmpps.integrations.delius.user.ExclusionRepository +import uk.gov.justice.digital.hmpps.integrations.delius.user.RestrictionRepository + +@Service +class UserService( + private val personRepository: PersonRepository, + private val exclusionRepository: ExclusionRepository, + private val restrictionRepository: RestrictionRepository +) { + fun getAllAccessLimitations(crn: String, usernamesFilter: List? = null): CaseAccessList { + val person = personRepository.findByCrnAndSoftDeletedFalse(crn) ?: throw NotFoundException("Person", "crn", crn) + return CaseAccessList( + crn = crn, + exclusionMessage = person.exclusionMessage, + restrictionMessage = person.restrictionMessage, + excludedFrom = exclusionRepository.findByPersonId(person.id).map { it.user.username } + .filter { usernamesFilter == null || it in usernamesFilter } + .map { User(it) }, + restrictedTo = restrictionRepository.findByPersonId(person.id).map { it.user.username } + .filter { usernamesFilter == null || it in usernamesFilter } + .map { User(it) }, + ) + } +} + diff --git a/projects/workforce-allocations-to-delius/src/main/resources/application.yml b/projects/workforce-allocations-to-delius/src/main/resources/application.yml index ecbbb4bbfb..697585dd63 100644 --- a/projects/workforce-allocations-to-delius/src/main/resources/application.yml +++ b/projects/workforce-allocations-to-delius/src/main/resources/application.yml @@ -57,7 +57,9 @@ server.shutdown: immediate 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: + base-dn: ${spring.ldap.base} + validation.enabled: false seed.database: true context.initializer.classes: uk.gov.justice.digital.hmpps.wiremock.WireMockInitialiser