Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(authz): get a list of all users (reserved to stellio admins) #977

Merged
merged 1 commit into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,9 @@ interface AuthorizationService {
limit: Int,
sub: Option<Sub>
): Either<APIException, Pair<Int, List<JsonLdEntity>>>

suspend fun getUsers(
offset: Int,
limit: Int,
): Either<APIException, Pair<Int, List<JsonLdEntity>>>
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,9 @@ class DisabledAuthorizationService : AuthorizationService {
limit: Int,
sub: Option<Sub>
): Either<APIException, Pair<Int, List<JsonLdEntity>>> = Pair(-1, emptyList<JsonLdEntity>()).right()

override suspend fun getUsers(
offset: Int,
limit: Int,
): Either<APIException, Pair<Int, List<JsonLdEntity>>> = Pair(-1, emptyList<JsonLdEntity>()).right()
}
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,23 @@ class EnabledAuthorizationService(
}
}

override suspend fun getUsers(
offset: Int,
limit: Int
): Either<APIException, Pair<Int, List<JsonLdEntity>>> = either {
val users = subjectReferentialService.getUsers(offset, limit)
val usersCount = subjectReferentialService.getUsersCount().bind()

val jsonLdEntities = users.map {
JsonLdEntity(
it.serializeProperties(),
listOf(AUTHORIZATION_CONTEXT)
)
}

Pair(usersCount, jsonLdEntities)
}

override suspend fun computeAccessRightFilter(sub: Option<Sub>): () -> String? =
subjectReferentialService.getSubjectAndGroupsUUID(sub).map { uuids ->
if (subjectReferentialService.hasStellioAdminRole(uuids).getOrElse { false }) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,42 @@ class SubjectReferentialService(
)
.oneToResult { toInt(it["count"]) }

suspend fun getUsers(offset: Int, limit: Int): List<User> =
databaseClient
.sql(
"""
SELECT subject_id AS user_id, (subject_info->'value'->>'username') AS username,
(subject_info->'value'->>'givenName') AS givenName,
(subject_info->'value'->>'familyName') AS familyName
FROM subject_referential
WHERE subject_type = '${SubjectType.USER.name}'
ORDER BY username
LIMIT :limit
OFFSET :offset
""".trimIndent()
)
.bind("limit", limit)
.bind("offset", offset)
.allToMappedList {
User(
id = it["user_id"] as String,
username = it["username"] as String,
givenName = it["givenName"] as? String,
familyName = it["familyName"] as? String
)
}

suspend fun getUsersCount(): Either<APIException, Int> =
databaseClient
.sql(
"""
SELECT count(*) as count
FROM subject_referential
WHERE subject_type = '${SubjectType.USER.name}'
""".trimIndent()
)
.oneToResult { toInt(it["count"]) }

suspend fun hasStellioAdminRole(uuids: List<Sub>): Either<APIException, Boolean> =
hasOneOfGlobalRoles(uuids, ADMIN_ROLES)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.egm.stellio.search.authorization

import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_FAMILY_NAME
import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_GIVEN_NAME
import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_USERNAME
import com.egm.stellio.shared.util.AuthContextModel.USER_ENTITY_PREFIX
import com.egm.stellio.shared.util.AuthContextModel.USER_TYPE
import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_ID
import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_TYPE
import com.egm.stellio.shared.util.JsonLdUtils.buildExpandedProperty

data class User(
val id: String,
val type: String = USER_TYPE,
val username: String,
val givenName: String? = null,
val familyName: String? = null
) {
fun serializeProperties(): Map<String, Any> {
val resultEntity = mutableMapOf<String, Any>()
resultEntity[JSONLD_ID] = USER_ENTITY_PREFIX + id
resultEntity[JSONLD_TYPE] = listOf(type)

resultEntity[AUTH_PROP_USERNAME] = buildExpandedProperty(username)

givenName.run {
resultEntity[AUTH_PROP_GIVEN_NAME] = buildExpandedProperty(givenName.toString())
}

familyName.run {
resultEntity[AUTH_PROP_FAMILY_NAME] = buildExpandedProperty(familyName.toString())
}

return resultEntity
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,51 @@ class EntityAccessControlHandler(
{ it }
)

@GetMapping("/users", produces = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE])
suspend fun getUsers(
@RequestHeader httpHeaders: HttpHeaders,
@RequestParam params: MultiValueMap<String, String>
): ResponseEntity<*> = either {
val sub = getSubFromSecurityContext()

authorizationService.userIsAdmin(sub).bind()

val contextLink = getContextFromLinkHeaderOrDefault(httpHeaders).bind().addAuthzContextIfNeeded()
val mediaType = getApplicableMediaType(httpHeaders)
val queryParams = parseQueryParams(
Pair(applicationProperties.pagination.limitDefault, applicationProperties.pagination.limitMax),
params,
contextLink
).bind()

val countAndUserEntities =
authorizationService.getUsers(queryParams.offset, queryParams.limit).bind()

if (countAndUserEntities.first == -1) {
return@either ResponseEntity.status(HttpStatus.NO_CONTENT).build<String>()
}

val compactedEntities = JsonLdUtils.compactEntities(
countAndUserEntities.second,
queryParams.useSimplifiedRepresentation,
contextLink,
mediaType
)

buildQueryResponse(
compactedEntities,
countAndUserEntities.first,
"/ngsi-ld/v1/entityAccessControl/users",
queryParams,
params,
mediaType,
contextLink
)
}.fold(
{ it.toErrorResponse() },
{ it }
)

@PostMapping("/{subjectId}/attrs", consumes = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE])
suspend fun addRightsOnEntities(
@RequestHeader httpHeaders: HttpHeaders,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ package com.egm.stellio.search.authorization

import arrow.core.None
import com.egm.stellio.shared.model.QueryParams
import com.egm.stellio.shared.util.*
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_CORE_CONTEXT
import com.egm.stellio.shared.util.shouldSucceedWith
import com.egm.stellio.shared.util.toUri
import io.mockk.spyk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
Expand Down Expand Up @@ -49,4 +50,15 @@ class AuthorizationServiceTests {
assertEquals(0, it.second.size)
}
}

@Test
fun `get users should return a count of -1 if authentication is not enabled`() = runTest {
authorizationService.getUsers(
0,
0
).shouldSucceedWith {
assertEquals(-1, it.first)
assertEquals(0, it.second.size)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ import com.egm.stellio.search.authorization.EntityAccessRights.SubjectRightInfo
import com.egm.stellio.shared.model.AccessDeniedException
import com.egm.stellio.shared.model.QueryParams
import com.egm.stellio.shared.util.*
import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_USERNAME
import com.egm.stellio.shared.util.AuthContextModel.AUTH_REL_CAN_WRITE
import com.egm.stellio.shared.util.AuthContextModel.GROUP_ENTITY_PREFIX
import com.egm.stellio.shared.util.AuthContextModel.GROUP_TYPE
import com.egm.stellio.shared.util.AuthContextModel.SpecificAccessPolicy
import com.egm.stellio.shared.util.AuthContextModel.USER_ENTITY_PREFIX
import com.egm.stellio.shared.util.AuthContextModel.USER_TYPE
import com.ninjasquad.springmockk.MockkBean
import io.mockk.coEvery
import io.mockk.coVerify
Expand Down Expand Up @@ -287,6 +290,36 @@ class EnabledAuthorizationServiceTests {
}
}

@Test
fun `it should return serialized users along with a count for an admin`() = runTest {
coEvery {
subjectReferentialService.getUsers(any(), any())
} returns listOf(
User(
id = UUID.randomUUID().toString(),
username = "Username 1",
givenName = "Given Name 1",
familyName = "Family Name 1"
),
User(
id = UUID.randomUUID().toString(),
username = "Username 2"
)
)
coEvery { subjectReferentialService.getUsersCount() } returns Either.Right(2)

enabledAuthorizationService.getUsers(0, 2)
.shouldSucceedWith {
assertEquals(2, it.first)
it.second.forEach { jsonLdEntity ->
assertEquals(1, jsonLdEntity.types.size)
assertEquals(USER_TYPE, jsonLdEntity.types[0])
assertTrue(jsonLdEntity.id.startsWith(USER_ENTITY_PREFIX))
assertTrue(jsonLdEntity.members.containsKey(AUTH_PROP_USERNAME))
}
}
}

@Test
fun `it should returned serialized access control entities with a count`() = runTest {
coEvery {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.egm.stellio.shared.model.AccessDeniedException
import com.egm.stellio.shared.util.*
import com.egm.stellio.shared.util.GlobalRole.STELLIO_ADMIN
import com.egm.stellio.shared.util.GlobalRole.STELLIO_CREATOR
import io.r2dbc.postgresql.codec.Json
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.assertj.core.api.Assertions.assertThat
Expand Down Expand Up @@ -244,6 +245,62 @@ class SubjectReferentialServiceTests : WithTimescaleContainer {
.shouldSucceedWith { assertEquals(3, it) }
}

@Test
fun `it should get all users`() = runTest {
val allUsersUuids = List(3) {
val userUuid = UUID.randomUUID().toString()
subjectReferentialService.create(
SubjectReferential(
subjectId = userUuid,
subjectType = SubjectType.USER,
subjectInfo = getSubjectInfoForUser(userUuid)
)
)
userUuid
}

val users = subjectReferentialService.getUsers(0, 10)

assertEquals(3, users.size)
users.forEach {
assertTrue(it.id in allUsersUuids)
assertNotNull(it.username)
assertNull(it.givenName)
assertNull(it.familyName)
}

subjectReferentialService.getUsersCount()
.shouldSucceedWith { assertEquals(3, it) }
}

@Test
fun `it should get an user with all available information`() = runTest {
val userUuid = UUID.randomUUID().toString()
subjectReferentialService.create(
SubjectReferential(
subjectId = userUuid,
subjectType = SubjectType.USER,
subjectInfo = Json.of(
"""
{
"type": "Property",
"value": { "username": "username", "givenName": "givenName", "familyName": "familyName" }
}
""".trimIndent()
)
)
)

val users = subjectReferentialService.getUsers(0, 10)

assertEquals(1, users.size)
val user = users[0]
assertEquals(userUuid, user.id)
assertEquals("username", user.username)
assertEquals("givenName", user.givenName)
assertEquals("familyName", user.familyName)
}

@Test
fun `it should update the global role of a subject`() = runTest {
val subjectReferential = SubjectReferential(
Expand Down
Loading