Skip to content

Commit

Permalink
Addressing Comments
Browse files Browse the repository at this point in the history
  • Loading branch information
TheLydonKing committed Feb 29, 2024
1 parent 86e6baf commit 1751749
Show file tree
Hide file tree
Showing 17 changed files with 221 additions and 121 deletions.
4 changes: 2 additions & 2 deletions api/src/main/resources/example.application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,13 @@ loginsvc:
# Set the order of the protocol starting from 1
# Set to 0 to disable or simply exclude the ldap tag from config
# NOTE: At least 1 auth protocol needs to be enabled
order: 2
order: 0
domain: "some.domain.com"
url: "ldaps://some.domain.com:636/"
search-filter: "(samaccountname={1})"
service-account:
# The account-pattern is used to format the username of the user to be used to authenticate with the ldap server.
account-pattern: "CN=%s,OU=Users,OU=CORP Accounts,DC=corp,DC=dsarena,DC=com"
account-pattern: "CN=%s,OU=Users,OU=Organization1,DC=my,DC=domain,DC=com"
# The in-config-account is used to authenticate with the ldap server.
in-config-account:
username: "svc-ldap"
Expand Down
20 changes: 10 additions & 10 deletions api/src/main/scala/za/co/absa/loginsvc/rest/AuthManagerConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,34 +33,34 @@ import scala.collection.immutable.SortedMap
@Configuration
class AuthManagerConfig @Autowired()(authConfigProvider: AuthConfigProvider){

private val usersConfig: UsersConfig = authConfigProvider.getUsersConfig
private val adLDAPConfig: ActiveDirectoryLDAPConfig = authConfigProvider.getLdapConfig
private val usersConfig: Option[UsersConfig] = authConfigProvider.getUsersConfig
private val adLDAPConfig: Option[ActiveDirectoryLDAPConfig] = authConfigProvider.getLdapConfig

private val logger = LoggerFactory.getLogger(classOf[AuthManagerConfig])

@Bean
def authManager(http: HttpSecurity): AuthenticationManager = {

val authenticationManagerBuilder = http.getSharedObject(classOf[AuthenticationManagerBuilder]).parentAuthenticationManager(null)
val configs: Array[DynamicAuthOrder] = Array(usersConfig, adLDAPConfig)
val configs: Array[DynamicAuthOrder] = Array(usersConfig, adLDAPConfig).flatten
val orderedProviders = createProviders(configs)

if(orderedProviders.isEmpty)
throw ConfigValidationException("No authentication method enabled in config")

orderedProviders.foreach(
orderedProviders.zipWithIndex.foreach(
auth => {
logger.info(s"Authentication method ${auth._2.getClass.getSimpleName} has been initialized at order ${auth._1}")
authenticationManagerBuilder.authenticationProvider(auth._2)
logger.info(s"Authentication method ${auth._1.getClass.getSimpleName} has been initialized at order ${auth._2 + 1}")
authenticationManagerBuilder.authenticationProvider(auth._1)
})
authenticationManagerBuilder.build
}

private def createProviders(configs: Array[DynamicAuthOrder]): SortedMap[Int, AuthenticationProvider] = {
SortedMap.empty[Int, AuthenticationProvider] ++ configs.filter(_.order > 0)
private def createProviders(configs: Array[DynamicAuthOrder]): Array[AuthenticationProvider] = {
Array.empty[AuthenticationProvider] ++ configs.filter(_.order > 0).sortBy(_.order)
.map {
case c: UsersConfig => (c.order, new ConfigUsersAuthenticationProvider(c))
case c: ActiveDirectoryLDAPConfig => (c.order, new ActiveDirectoryLDAPAuthenticationProvider(c))
case c: UsersConfig => new ConfigUsersAuthenticationProvider(c)
case c: ActiveDirectoryLDAPConfig => new ActiveDirectoryLDAPAuthenticationProvider(c)
case other => throw new IllegalStateException(s"unsupported config $other")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,30 +74,39 @@ case class ActiveDirectoryLDAPConfig(domain: String,
}

case class ServiceAccountConfig(private val accountPattern: String,
private val inConfigAccount: Option[IntegratedLdapUserConfig],
private val inConfigAccount: Option[LdapUserCredentialsConfig],
private val awsSecretsManagerAccount: Option[AwsSecretsLdapUserConfig])
{
private val ldapUserDetails: LdapUser = (inConfigAccount, awsSecretsManagerAccount) match {
case (Some(_), Some(_)) =>
throw ConfigValidationException("Both integratedLdapUserConfig and awsSecretsLdapUserConfig exist. Please choose only one.")

case (None, None) =>
throw ConfigValidationException("Neither integratedLdapUserConfig nor awsSecretsLdapUserConfig exists. One of them should exist.")
throw ConfigValidationException("Neither integratedLdapUserConfig nor awsSecretsLdapUserConfig exists. Exactly one of them should be present.")

case _ =>
val ldapConfig: LdapUser = inConfigAccount.orElse(awsSecretsManagerAccount).get
ldapConfig.throwErrors()
case (inConfig@Some(_), None) =>
val ldapConfig: LdapUser = inConfig.get
ldapConfig.throwOnErrors()

ldapConfig

case (None, awsConfig@Some(_)) =>
val ldapConfig: LdapUser = awsConfig.get
ldapConfig.throwOnErrors()

ldapConfig

case _ =>
throw ConfigValidationException("Error with current config concerning integratedLdapUserConfig or awsSecretsLdapUserConfig")
}

val username: String = String.format(accountPattern, ldapUserDetails.username)
val password: String = ldapUserDetails.password
}

case class IntegratedLdapUserConfig(username: String, password: String) extends LdapUser
case class LdapUserCredentialsConfig (username: String, password: String) extends LdapUser
{
def throwErrors(): Unit = this.validate().throwOnErrors()
def throwOnErrors(): Unit = this.validate().throwOnErrors()
}

case class AwsSecretsLdapUserConfig(private val secretName: String,
Expand All @@ -108,7 +117,7 @@ case class AwsSecretsLdapUserConfig(private val secretName: String,
private val logger = LoggerFactory.getLogger(classOf[LdapUser])

val (username, password) = getUsernameAndPasswordFromSecret
def throwErrors(): Unit = this.validate().throwOnErrors()
def throwOnErrors(): Unit = this.validate().throwOnErrors()
override def validate(): ConfigValidationResult = {
val results = Seq(
Option(secretName)
Expand Down Expand Up @@ -163,8 +172,7 @@ case class AwsSecretsLdapUserConfig(private val secretName: String,
trait LdapUser extends ConfigValidatable {
def username: String
def password: String

def throwErrors(): Unit
def throwOnErrors(): Unit

override def validate(): ConfigValidationResult = {
val results = Seq(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ package za.co.absa.loginsvc.rest.config.provider
import za.co.absa.loginsvc.rest.config.auth._

trait AuthConfigProvider {
def getLdapConfig : ActiveDirectoryLDAPConfig
def getUsersConfig : UsersConfig
def getLdapConfig : Option[ActiveDirectoryLDAPConfig]
def getUsersConfig : Option[UsersConfig]
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,36 +58,37 @@ class ConfigProvider(@Value("${spring.config.location}") yamlPath: String)
case (None, None) =>
throw ConfigValidationException("Neither inMemoryKeyConfig nor awsSecretsManagerKeyConfig exists. One of them should exist.")

case _ =>
val keyConfig: KeyConfig = inMemoryKeyConfig.orElse(awsSecretsManagerKeyConfig).get
case (inMemoryKeyConfig@Some(_), None) =>
val keyConfig: KeyConfig = inMemoryKeyConfig.get
keyConfig.throwErrors()

keyConfig

case (None, awsSecretsManagerKeyConfig@Some(_)) =>
val keyConfig: KeyConfig = awsSecretsManagerKeyConfig.get
keyConfig.throwErrors()

keyConfig

case _ =>
throw ConfigValidationException("Error with current config concerning inMemoryKeyConfig or awsSecretsManagerKeyConfig")
}
}

def getLdapConfig : ActiveDirectoryLDAPConfig = {
def getLdapConfig : Option[ActiveDirectoryLDAPConfig] = {
val ldapConfigOption = createConfigClass[ActiveDirectoryLDAPConfig]("loginsvc.rest.auth.provider.ldap")
if(ldapConfigOption.nonEmpty)
{
val ldapConfig = ldapConfigOption.get
ldapConfig.throwErrors()
ldapConfig
}
else{
val emptyServiceAccount = ServiceAccountConfig("", Option(IntegratedLdapUserConfig("","")), None)
ActiveDirectoryLDAPConfig("", "", "", 0, emptyServiceAccount, None)
}
ldapConfigOption.get.throwErrors()

ldapConfigOption
}

def getUsersConfig : UsersConfig = {
def getUsersConfig : Option[UsersConfig] = {
val userConfigOption = createConfigClass[UsersConfig]("loginsvc.rest.auth.provider.users")
if (userConfigOption.nonEmpty) {
val userConfig = userConfigOption.get
userConfig.throwErrors()
userConfig
}
else UsersConfig(Array.empty[UserConfig], 0)
if (userConfigOption.nonEmpty)
userConfigOption.get.throwErrors()

userConfigOption
}

private def getGitConfig: GitConfig = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import org.springframework.security.core.Authentication
import org.springframework.web.bind.annotation._
import za.co.absa.loginsvc.model.User
import za.co.absa.loginsvc.rest.model.{PublicKey, TokensWrapper}
import za.co.absa.loginsvc.rest.service.JWTService
import za.co.absa.loginsvc.rest.service.jwt.JWTService
import za.co.absa.loginsvc.utils.OptionUtils.ImplicitBuilderExt

import java.util.concurrent.CompletableFuture
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

package za.co.absa.loginsvc.rest.service
package za.co.absa.loginsvc.rest.service.jwt

import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.jose.jwk.{JWKSet, KeyUse, RSAKey}
Expand All @@ -25,7 +25,8 @@ import org.springframework.stereotype.Service
import za.co.absa.loginsvc.model.User
import za.co.absa.loginsvc.rest.config.provider.JwtConfigProvider
import za.co.absa.loginsvc.rest.model.{AccessToken, RefreshToken, Token}
import za.co.absa.loginsvc.rest.service.JWTService.extractUserFrom
import za.co.absa.loginsvc.rest.service.jwt.JWTService.extractUserFrom
import za.co.absa.loginsvc.rest.service.search.AuthSearchService

import java.security.interfaces.RSAPublicKey
import java.security.{KeyPair, PublicKey}
Expand Down Expand Up @@ -127,7 +128,6 @@ class JWTService @Autowired()(jwtConfigProvider: JwtConfigProvider, authSearchSe
try {
val searchedUser = authSearchService.searchUser(userFromOldAccessToken.name)
val prefixedGroups = searchedUser.groups.intersect(userFromOldAccessToken.groups) // only keep groups that were in old token

User(searchedUser.name, prefixedGroups, searchedUser.optionalAttributes)
} catch {
case _: Throwable => throw new UnsupportedJwtException(s"User ${userFromOldAccessToken.name} not found")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2023 ABSA Group Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package za.co.absa.loginsvc.rest.service.search

import za.co.absa.loginsvc.model.User

trait AuthSearchProvider {
def searchForUser(username: String): Option[User]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright 2023 ABSA Group Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package za.co.absa.loginsvc.rest.service.search

import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import za.co.absa.loginsvc.model.User
import za.co.absa.loginsvc.rest.config.auth.{ActiveDirectoryLDAPConfig, DynamicAuthOrder, UsersConfig}
import za.co.absa.loginsvc.rest.config.provider.AuthConfigProvider
import za.co.absa.loginsvc.rest.config.validation.ConfigValidationException

@Service
class AuthSearchService @Autowired()(authConfigProvider: AuthConfigProvider) {

private val logger = LoggerFactory.getLogger(classOf[AuthSearchService])

private val usersConfig: Option[DynamicAuthOrder] = authConfigProvider.getUsersConfig
private val adLDAPConfig: Option[DynamicAuthOrder] = authConfigProvider.getLdapConfig

private val configs: Array[DynamicAuthOrder] = Array(usersConfig, adLDAPConfig).flatten.filter(_.order != 0).sortBy(_.order)
private val orderedProviders = createProviders(configs)

if (orderedProviders.isEmpty)
throw ConfigValidationException("No authentication method enabled in config")

def searchUser(username: String): User = {
orderedProviders.foreach { provider =>
val user = provider.searchForUser(username)
if (user.isDefined) {
return user.get
}
}
throw new NoSuchElementException(s"Value not found in any object.")
}

private def createProviders(configs: Array[DynamicAuthOrder]): Array[AuthSearchProvider] = {
Array.empty[AuthSearchProvider] ++ configs.filter(_.order > 0).sortBy(_.order)
.map {
case c: UsersConfig => new ConfigSearchProvider(c)
case c: ActiveDirectoryLDAPConfig => new LdapSearchProvider(c)
case other => throw new IllegalStateException(s"unsupported config $other")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2023 ABSA Group Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package za.co.absa.loginsvc.rest.service.search

import org.slf4j.LoggerFactory
import za.co.absa.loginsvc.model.User
import za.co.absa.loginsvc.rest.config.auth.UsersConfig

class ConfigSearchProvider(usersConfig: UsersConfig)
extends AuthSearchProvider {

private val logger = LoggerFactory.getLogger(classOf[ConfigSearchProvider])
def searchForUser(username: String): Option[User] = {
logger.info(s"Searching for user in config: $username")
usersConfig.knownUsersMap.get(username).flatMap { userConfig =>
val optionalAttributes: Map[String, Option[AnyRef]] = userConfig.attributes.getOrElse(Map.empty).map {
case (k, v) => (k, Some(v))
}
logger.info(s"User found in config: $username")
Option(User(userConfig.username, userConfig.groups.toList, optionalAttributes))
}
}
}
Loading

0 comments on commit 1751749

Please sign in to comment.