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

100 using spnego similarly like in enceladus to negotiate the auth #103

Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
88246fa
Initial Commit
TheLydonKing Apr 24, 2024
664a37c
Added Kerberos to Config
TheLydonKing Apr 25, 2024
7c00778
Added config to required class
TheLydonKing Apr 26, 2024
69cab58
Implement Kerberos Search
TheLydonKing Jun 10, 2024
0f11ae1
Added Test LdapProvider just to test kerberosLdapSearch
TheLydonKing Jun 11, 2024
0b2621e
Trying to use system property sets
TheLydonKing Jun 12, 2024
892d391
Simplify code for better debugging
TheLydonKing Jul 3, 2024
9fb0e2b
Split up config among new files for better readability
TheLydonKing Jul 22, 2024
1693d3b
Added Missing License
TheLydonKing Jul 22, 2024
3ef9aad
Fixed Tests
TheLydonKing Jul 22, 2024
864f79d
Fixed Tests part 2
TheLydonKing Jul 22, 2024
fb64065
Added Changes to Dockerfile to include krb5 and keytab
TheLydonKing Jul 31, 2024
d26bf94
Revert Dockerfile change
TheLydonKing Aug 2, 2024
98731ac
Add Ldap required functions for future testing once Dummy Service is …
TheLydonKing Aug 5, 2024
36c75f4
Fix krb5 property
TheLydonKing Aug 12, 2024
0cf9586
Set AfterProperties Set for Properties to ensure KRB5 is read
TheLydonKing Aug 13, 2024
9e35e25
Remove Dummy User Service and enable LdapUserDetailsService
TheLydonKing Aug 14, 2024
3866b40
Minor Fix
TheLydonKing Aug 14, 2024
6d6fb83
Test Just Authentication Provider Only
TheLydonKing Aug 14, 2024
a487173
Test Just Service Authentication Provider Only
TheLydonKing Aug 14, 2024
9e864c3
Minor Change
TheLydonKing Aug 14, 2024
7d8f197
Add new UserDetailsType
TheLydonKing Aug 14, 2024
f1e665f
Slight Cleanup of unused code
TheLydonKing Aug 14, 2024
e6dca19
Disable Cache
TheLydonKing Aug 14, 2024
85dac05
Disable Cache attempt 2
TheLydonKing Aug 15, 2024
8249377
Add UserProvider
TheLydonKing Aug 15, 2024
6656966
Clean up code
TheLydonKing Aug 15, 2024
821ba5a
Add Missing License
TheLydonKing Aug 15, 2024
89df77b
Fix Logging
TheLydonKing Aug 15, 2024
fdb43a3
Add Testing
TheLydonKing Aug 15, 2024
a4aab77
Fix example.application.yaml
TheLydonKing Aug 15, 2024
be8c91b
Remove Unused Code
TheLydonKing Aug 15, 2024
68d8a2b
Amend Test Account Pattern
TheLydonKing Aug 20, 2024
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
5 changes: 5 additions & 0 deletions api/src/main/resources/example.application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ loginsvc:
#region: "region"
#username-field-name: "username"
#password-field-name: "password"
#enable-kerberos:
#krb-file-location: "/etc/krb5.conf"
#keytab-file-location: "/etc/keytab"
#spn: "HTTP/Host"
#debug: true
attributes:
# The FieldName is the key used to search ldap and the value is the value used to name the JWT claim.
# ldapFieldName: claimFieldName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,21 @@

package za.co.absa.loginsvc.rest

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.{Bean, Configuration}
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter
import za.co.absa.loginsvc.rest.config.provider.AuthConfigProvider
import za.co.absa.loginsvc.rest.provider.kerberos.KerberosSPNEGOAuthenticationProvider

@Configuration
@EnableWebSecurity
class SecurityConfig {
class SecurityConfig @Autowired()(authConfigsProvider: AuthConfigProvider) {

private val ldapConfig = authConfigsProvider.getLdapConfig.orNull

@Bean
def filterChain(http: HttpSecurity): SecurityFilterChain = {
Expand All @@ -49,9 +55,19 @@ class SecurityConfig {
.and()
.httpBasic()

http.build()
}
if(ldapConfig != null)
{
if(ldapConfig.enableKerberos.isDefined)
{
val kerberos = new KerberosSPNEGOAuthenticationProvider(ldapConfig)

http.addFilterBefore(
kerberos.spnegoAuthenticationProcessingFilter,
classOf[BasicAuthenticationFilter])
}
}

http.build()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ case class ActiveDirectoryLDAPConfig(domain: String,
searchFilter: String,
order: Int,
serviceAccount: ServiceAccountConfig,
enableKerberos: Option[KerberosConfig],
attributes: Option[Map[String, String]])
extends ConfigValidatable with ConfigOrdering
{
Expand Down Expand Up @@ -63,89 +64,17 @@ case class ActiveDirectoryLDAPConfig(domain: String,
.getOrElse(ConfigValidationError(ConfigValidationException("searchFilter is empty")))
)

results.foldLeft[ConfigValidationResult](ConfigValidationSuccess)(ConfigValidationResult.merge)
val requiredResults = results.foldLeft[ConfigValidationResult](ConfigValidationSuccess)(ConfigValidationResult.merge)
val kerberosResults = enableKerberos match {
case Some(x) => x.validate()
case None => ConfigValidationSuccess
}
requiredResults.merge(kerberosResults)
}
else ConfigValidationSuccess
}
}

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

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

case (Some(inConfig), None) =>
inConfig.throwOnErrors()
inConfig

case (None, Some(awsConfig)) =>
awsConfig.throwOnErrors()
awsConfig

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

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

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

case class AwsSecretsLdapUserConfig(private val secretName: String,
private val region: String,
private val usernameFieldName: String,
private val passwordFieldName: String) extends LdapUser {

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

val (username, password) = getUsernameAndPasswordFromSecret
def throwOnErrors(): Unit = this.validate().throwOnErrors()
override def validate(): ConfigValidationResult = {
val results = Seq(
Option(secretName)
.map(_ => ConfigValidationSuccess)
.getOrElse(ConfigValidationError(ConfigValidationException("secretName is empty"))),

Option(region)
.map(_ => ConfigValidationSuccess)
.getOrElse(ConfigValidationError(ConfigValidationException("region is empty"))),

Option(usernameFieldName)
.map(_ => ConfigValidationSuccess)
.getOrElse(ConfigValidationError(ConfigValidationException("usernameFieldName is empty"))),

Option(passwordFieldName)
.map(_ => ConfigValidationSuccess)
.getOrElse(ConfigValidationError(ConfigValidationException("passwordFieldName is empty")))
)

val awsSecretsResultsMerge = results.foldLeft[ConfigValidationResult](ConfigValidationSuccess)(ConfigValidationResult.merge)
awsSecretsResultsMerge.merge(super.validate())
}

private def getUsernameAndPasswordFromSecret: (String, String) = {
try {
val secrets = AwsSecretsUtils.fetchSecret(secretName, region, Array(usernameFieldName, passwordFieldName))
(secrets(usernameFieldName), secrets(passwordFieldName))
}
catch {
case e: Throwable =>
logger.error(s"Error occurred retrieving account data from AWS Secrets Manager", e)
throw e
}
}
}

trait LdapUser extends ConfigValidatable {
def username: String
def password: String
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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.config.auth

import za.co.absa.loginsvc.rest.config.validation.{ConfigValidatable, ConfigValidationException, ConfigValidationResult}
import za.co.absa.loginsvc.rest.config.validation.ConfigValidationResult.{ConfigValidationError, ConfigValidationSuccess}

case class KerberosConfig(
krbFileLocation: String,
keytabFileLocation: String,
spn: String,
debug: Option[Boolean]) extends ConfigValidatable {

override def validate(): ConfigValidationResult = {
val results = Seq(
Option(krbFileLocation)
.map(_ => ConfigValidationSuccess)
.getOrElse(ConfigValidationError(ConfigValidationException("krbFileLocation is empty"))),
Option(keytabFileLocation)
.map(_ => ConfigValidationSuccess)
.getOrElse(ConfigValidationError(ConfigValidationException("keytabFileLocation is empty"))),
Option(spn)
.map(_ => ConfigValidationSuccess)
.getOrElse(ConfigValidationError(ConfigValidationException("spn is empty")))
)
results.foldLeft[ConfigValidationResult](ConfigValidationSuccess)(ConfigValidationResult.merge)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* 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.config.auth

import org.slf4j.LoggerFactory
import za.co.absa.loginsvc.rest.config.validation.{ConfigValidationException, ConfigValidationResult}
import za.co.absa.loginsvc.rest.config.validation.ConfigValidationResult.{ConfigValidationError, ConfigValidationSuccess}
import za.co.absa.loginsvc.utils.AwsSecretsUtils

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

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

case (Some(inConfig), None) =>
inConfig.throwOnErrors()
inConfig

case (None, Some(awsConfig)) =>
awsConfig.throwOnErrors()
awsConfig

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

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

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

case class AwsSecretsLdapUserConfig(private val secretName: String,
private val region: String,
private val usernameFieldName: String,
private val passwordFieldName: String) extends LdapUser
{

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

val (username, password) = getUsernameAndPasswordFromSecret
def throwOnErrors(): Unit = this.validate().throwOnErrors()
override def validate(): ConfigValidationResult = {
val results = Seq(
Option(secretName)
.map(_ => ConfigValidationSuccess)
.getOrElse(ConfigValidationError(ConfigValidationException("secretName is empty"))),

Option(region)
.map(_ => ConfigValidationSuccess)
.getOrElse(ConfigValidationError(ConfigValidationException("region is empty"))),

Option(usernameFieldName)
.map(_ => ConfigValidationSuccess)
.getOrElse(ConfigValidationError(ConfigValidationException("usernameFieldName is empty"))),

Option(passwordFieldName)
.map(_ => ConfigValidationSuccess)
.getOrElse(ConfigValidationError(ConfigValidationException("passwordFieldName is empty")))
)

val awsSecretsResultsMerge = results.foldLeft[ConfigValidationResult](ConfigValidationSuccess)(ConfigValidationResult.merge)
awsSecretsResultsMerge.merge(super.validate())
}

private def getUsernameAndPasswordFromSecret: (String, String) = {
try {
val secrets = AwsSecretsUtils.fetchSecret(secretName, region, Array(usernameFieldName, passwordFieldName))
(secrets(usernameFieldName), secrets(passwordFieldName))
}
catch {
case e: Throwable =>
logger.error(s"Error occurred retrieving account data from AWS Secrets Manager", e)
throw e
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.{HttpStatus, MediaType}
import org.springframework.security.core.Authentication
import org.springframework.web.bind.annotation._
import org.springframework.web.server.ResponseStatusException
import za.co.absa.loginsvc.model.User
import za.co.absa.loginsvc.rest.model.{PublicKey, TokensWrapper}
import za.co.absa.loginsvc.rest.model.{KerberosUserDetails, PublicKey, TokensWrapper}
import za.co.absa.loginsvc.rest.service.jwt.JWTService
import za.co.absa.loginsvc.utils.OptionUtils.ImplicitBuilderExt

Expand Down Expand Up @@ -71,7 +72,12 @@ class TokenController @Autowired()(jwtService: JWTService) {
@ResponseStatus(HttpStatus.OK)
@SecurityRequirement(name = "basicAuth")
def generateToken(authentication: Authentication, @RequestParam("group-prefixes") groupPrefixes: Optional[String]): CompletableFuture[TokensWrapper] = {
val user = authentication.getPrincipal.asInstanceOf[User]

val user: User = authentication.getPrincipal match {
case u: User => u
case k: KerberosUserDetails => k.getUser
case _ => throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not authenticated or unknown principal type")
}
val groupPrefixesStrScala = groupPrefixes.toScalaOption

val filteredGroupsUser = user.applyIfDefined(groupPrefixesStrScala) { (user: User, prefixesStr: String) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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.model

import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.UserDetails
import za.co.absa.loginsvc.model.User

import java.util
import scala.collection.JavaConverters._

case class KerberosUserDetails(user: User)
extends UserDetails
{
override def getAuthorities: util.Collection[_ <: GrantedAuthority] =
user.groups.map(new SimpleGrantedAuthority(_)).toList.asJava

override def getPassword: String = ""

override def getUsername: String = user.name

override def isAccountNonExpired: Boolean = true

override def isAccountNonLocked: Boolean = true

override def isCredentialsNonExpired: Boolean = true

override def isEnabled: Boolean = true

def getUser: User = user
}
Loading
Loading