From 5fea4c7846c3952905b1b038efc82a0b0fef7387 Mon Sep 17 00:00:00 2001 From: dzikoysk Date: Tue, 11 Jul 2023 16:39:06 +0200 Subject: [PATCH] GH-1872 Use filterArgs in LDAP impl & refactor it with more descriptive properties --- .../com/reposilite/auth/LdapAuthenticator.kt | 138 +++++++++++++----- .../extensions/ExpressibleExtensions.kt | 4 + 2 files changed, 105 insertions(+), 37 deletions(-) create mode 100644 reposilite-backend/src/main/kotlin/com/reposilite/shared/extensions/ExpressibleExtensions.kt diff --git a/reposilite-backend/src/main/kotlin/com/reposilite/auth/LdapAuthenticator.kt b/reposilite-backend/src/main/kotlin/com/reposilite/auth/LdapAuthenticator.kt index 0003847c8..a50097acb 100644 --- a/reposilite-backend/src/main/kotlin/com/reposilite/auth/LdapAuthenticator.kt +++ b/reposilite-backend/src/main/kotlin/com/reposilite/auth/LdapAuthenticator.kt @@ -22,6 +22,7 @@ import com.reposilite.journalist.Channel.DEBUG import com.reposilite.shared.ErrorResponse import com.reposilite.shared.badRequest import com.reposilite.shared.badRequestError +import com.reposilite.shared.extensions.accept import com.reposilite.shared.internalServerError import com.reposilite.shared.notFoundError import com.reposilite.shared.unauthorized @@ -48,7 +49,11 @@ import javax.naming.directory.SearchResult typealias Attributes = List typealias AttributesMap = Map -typealias SearchEntry = Pair + +data class SearchEntry( + val fullName: String, + val attributes: AttributesMap +) internal class LdapAuthenticator( private val ldapSettings: Reference, @@ -66,43 +71,69 @@ internal class LdapAuthenticator( createSearchContext() .flatMap { it.search( - "(&(objectClass=person)($userAttribute=${credentials.name}))", // find user entry with search user - userAttribute + ldapFilterQuery = "(&(objectClass=person)($userAttribute={0}))", // find user entry with search user, + ldapFilterQueryArguments = arrayOf(credentials.name), + requestedAttributes = arrayOf(userAttribute) + ) + } + .filter { users -> + when { + users.isEmpty() -> badRequest("User not found") + users.size > 1 -> badRequest("Could not identify one specific result") + else -> accept() // only one search result allowed + } + } + .map { users -> users.first() } + .flatMap { matchedUserObject -> + // try to authenticate user with matched domain namespace + createDirContext( + user = matchedUserObject.fullName, + password = credentials.secret ) } - .filter({ it.size == 1 }, { badRequest("Could not identify one specific result") }) // only one search result allowed - .map { it.first() } - .flatMap { createContext(user = it.first, password = credentials.secret) } // try to authenticate user with matched domain namespace .flatMap { it.search( - "(&(objectClass=person)($userAttribute=${credentials.name})$userFilter)", // filter result with user-filter from configuration - userAttribute + ldapFilterQuery = "(&(objectClass=person)($userAttribute={0})$userFilter)", // filter result with user-filter from configuration + ldapFilterQueryArguments = arrayOf(credentials.name), + requestedAttributes = arrayOf(userAttribute) ) } - .filter({ it.size == 1 }, { badRequest("Could not identify one specific result as user") }) // only one search result allowed - .map { it.first() } - .map { (_, attributes) -> attributes[userAttribute]!! } // search returns only lists with values - .filter({ it.size == 1 }, { badRequest("Could not identify one specific attribute") }) // only one attribute value is allowed - .map { it.first() } - .filter( - { credentials.name == it }, // make sure requested name matches required attribute - { unauthorized("LDAP user does not match required attribute") } - ) - .map { name -> accessTokenFacade.getAccessToken(name) - ?: accessTokenFacade.createAccessToken( - CreateAccessTokenRequest( - type = ldapSettings.map { it.userType }, - name = name, - secret = credentials.secret - ) - ).accessToken + .filter { filterResults -> + when { + filterResults.isEmpty() -> badRequest("User matching filter not found") + filterResults.size > 1 -> badRequest("Could not identify one specific result with specified user-filter") + else -> accept() // only one search result allowed + } + } + .map { filterResults -> filterResults.first() } + .map { it.attributes[userAttribute]!! } // search returns only lists with values + .filter { usernameAttributeObject -> + when { + usernameAttributeObject.isEmpty() -> badRequest("Username attribute not found") + usernameAttributeObject.size > 1 -> badRequest("Could not identify one specific username attribute: ${usernameAttributeObject.joinToString()}") + else -> accept() // only one attribute value is allowed + } + } + .map { attributes -> attributes.first() } + .filter { username -> + when { + username != credentials.name -> unauthorized("LDAP user does not match required attribute") + else -> accept() + } + } + .map { username -> + accessTokenFacade.getAccessToken(username) + ?: accessTokenFacade.createAccessToken( + CreateAccessTokenRequest( + type = ldapSettings.map { it.userType }, + name = username, + secret = credentials.secret + ) + ).accessToken } } - private fun createSearchContext(): Result = - ldapSettings.map { createContext(user = it.searchUserDn, password = it.searchUserPassword) } - - private fun createContext(user: String, password: String): Result = + private fun createDirContext(user: String, password: String): Result = Hashtable() .also { it[INITIAL_CONTEXT_FACTORY] = "com.sun.jndi.ldap.LdapCtxFactory" @@ -117,20 +148,53 @@ internal class LdapAuthenticator( unauthorized("Unauthorized LDAP access") } - fun search(ldapFilterQuery: String, vararg requestedAttributes: String): Result, ErrorResponse> = - createSearchContext() - .flatMap { it.search(ldapFilterQuery, *requestedAttributes) } + fun search( + ldapFilterQuery: String, + ldapFilterQueryArguments: Array, + vararg requestedAttributes: String + ): Result, ErrorResponse> = + createSearchContext().flatMap { + it.search( + ldapFilterQuery = ldapFilterQuery, + ldapFilterQueryArguments = ldapFilterQueryArguments, + requestedAttributes = requestedAttributes + ) + } - private fun DirContext.search(ldapFilterQuery: String, vararg requestedAttributes: String): Result, ErrorResponse> = + private fun createSearchContext(): Result = + ldapSettings.map { + createDirContext( + user = it.searchUserDn, + password = it.searchUserPassword + ) + } + + private fun DirContext.search( + ldapFilterQuery: String, + ldapFilterQueryArguments: Array, + requestedAttributes: Array + ): Result, ErrorResponse> = try { SearchControls() .also { - it.returningAttributes = requestedAttributes it.searchScope = SearchControls.SUBTREE_SCOPE + it.returningAttributes = requestedAttributes + } + .let { + this.search( + ldapSettings.get().baseDn, + ldapFilterQuery, + ldapFilterQueryArguments, + it + ) } - .let { controls -> search(ldapSettings.map { it.baseDn }, ldapFilterQuery, controls) } .asSequence() - .map { it.nameInNamespace to it.attributesMap(*requestedAttributes) } + .map { + SearchEntry( + fullName = it.nameInNamespace, + attributes = it.attributesMap(requestedAttributes) + ) + } .toList() .takeIf { it.isNotEmpty() } ?.asSuccess() @@ -145,7 +209,7 @@ internal class LdapAuthenticator( internalServerError(exception.toString()) } - private fun SearchResult.attributesMap(vararg requestedAttributes: String): AttributesMap = + private fun SearchResult.attributesMap(requestedAttributes: Array): AttributesMap = requestedAttributes.associate { attribute -> attributes.get(attribute) .all diff --git a/reposilite-backend/src/main/kotlin/com/reposilite/shared/extensions/ExpressibleExtensions.kt b/reposilite-backend/src/main/kotlin/com/reposilite/shared/extensions/ExpressibleExtensions.kt new file mode 100644 index 000000000..8e6f6d504 --- /dev/null +++ b/reposilite-backend/src/main/kotlin/com/reposilite/shared/extensions/ExpressibleExtensions.kt @@ -0,0 +1,4 @@ +package com.reposilite.shared.extensions + +fun accept(): T? = + null \ No newline at end of file