diff --git a/service/src/main/resources/example.application.yaml b/service/src/main/resources/example.application.yaml index b4ee609..94471b7 100644 --- a/service/src/main/resources/example.application.yaml +++ b/service/src/main/resources/example.application.yaml @@ -31,6 +31,7 @@ loginsvc: groups: [] - username: "TestUser" password: "password123" + displayname: "Test User, A.C.E." email: "test@abs.com" groups: - "groupA" diff --git a/service/src/main/scala/za/co/absa/loginsvc/model/User.scala b/service/src/main/scala/za/co/absa/loginsvc/model/User.scala index 303ff3e..cad3ac5 100644 --- a/service/src/main/scala/za/co/absa/loginsvc/model/User.scala +++ b/service/src/main/scala/za/co/absa/loginsvc/model/User.scala @@ -16,4 +16,4 @@ package za.co.absa.loginsvc.model -case class User(name: String, email: Option[String], groups: Seq[String]) +case class User(name: String, email: Option[String], displayName: Option[String], groups: Seq[String]) diff --git a/service/src/main/scala/za/co/absa/loginsvc/rest/config/auth/UsersConfig.scala b/service/src/main/scala/za/co/absa/loginsvc/rest/config/auth/UsersConfig.scala index ad49f18..eb892ad 100644 --- a/service/src/main/scala/za/co/absa/loginsvc/rest/config/auth/UsersConfig.scala +++ b/service/src/main/scala/za/co/absa/loginsvc/rest/config/auth/UsersConfig.scala @@ -59,11 +59,12 @@ case class UsersConfig(knownUsers: Array[UserConfig], order: Int) case class UserConfig(username: String, password: String, email: Option[String], + displayname: Option[String], groups: Array[String] ) extends ConfigValidatable { override def toString: String = { - s"UserConfig($username, $password, ${email.getOrElse("")}, ${Option(groups).map(_.toList)})" + s"UserConfig($username, $password, ${email.getOrElse("")}, ${displayname.getOrElse("")}, ${Option(groups).map(_.toList)})" } override def validate(): ConfigValidationResult = { diff --git a/service/src/main/scala/za/co/absa/loginsvc/rest/provider/ConfigUsersAuthenticationProvider.scala b/service/src/main/scala/za/co/absa/loginsvc/rest/provider/ConfigUsersAuthenticationProvider.scala index 93d0623..cf30a7c 100644 --- a/service/src/main/scala/za/co/absa/loginsvc/rest/provider/ConfigUsersAuthenticationProvider.scala +++ b/service/src/main/scala/za/co/absa/loginsvc/rest/provider/ConfigUsersAuthenticationProvider.scala @@ -39,7 +39,7 @@ class ConfigUsersAuthenticationProvider(usersConfig: UsersConfig) extends Authen usersConfig.knownUsersMap.get(username).map { usersConfig => if (usersConfig.password == password) { logger.info(s"user login: $username - ok") - val principal = User(username, usersConfig.email, usersConfig.groups.toList) + val principal = User(username, usersConfig.email, usersConfig.displayname, usersConfig.groups.toList) new UsernamePasswordAuthenticationToken(principal, password, usersConfig.groups.map(new SimpleGrantedAuthority(_)).toList.asJava) } else { diff --git a/service/src/main/scala/za/co/absa/loginsvc/rest/provider/ad/ldap/ActiveDirectoryLDAPAuthenticationProvider.scala b/service/src/main/scala/za/co/absa/loginsvc/rest/provider/ad/ldap/ActiveDirectoryLDAPAuthenticationProvider.scala index deae4c2..a07f477 100644 --- a/service/src/main/scala/za/co/absa/loginsvc/rest/provider/ad/ldap/ActiveDirectoryLDAPAuthenticationProvider.scala +++ b/service/src/main/scala/za/co/absa/loginsvc/rest/provider/ad/ldap/ActiveDirectoryLDAPAuthenticationProvider.scala @@ -46,11 +46,12 @@ class ActiveDirectoryLDAPAuthenticationProvider(config: ActiveDirectoryLDAPConfi override def authenticate(authentication: Authentication): Authentication = { val fromBase = baseImplementation.authenticate(authentication) - val fromBasePrincipal = fromBase.getPrincipal.asInstanceOf[UserDetailsWithEmail] + val fromBasePrincipal = fromBase.getPrincipal.asInstanceOf[UserDetailsWithExtras] val principal = User( fromBasePrincipal.getUsername, fromBasePrincipal.email, + fromBasePrincipal.displayName, fromBasePrincipal.getAuthorities.asScala.map(_.getAuthority).toSeq ) @@ -60,7 +61,7 @@ class ActiveDirectoryLDAPAuthenticationProvider(config: ActiveDirectoryLDAPConfi override def supports(authentication: Class[_]): Boolean = baseImplementation.supports(authentication) - private case class UserDetailsWithEmail(userDetails: UserDetails, email: Option[String]) extends UserDetails { + private case class UserDetailsWithExtras(userDetails: UserDetails, email: Option[String], displayName: Option[String]) extends UserDetails { override def getAuthorities: util.Collection[_ <: GrantedAuthority] = userDetails.getAuthorities override def getPassword: String = userDetails.getPassword override def getUsername: String = userDetails.getUsername @@ -79,7 +80,9 @@ class ActiveDirectoryLDAPAuthenticationProvider(config: ActiveDirectoryLDAPConfi ): UserDetails = { val fromBase = super.mapUserFromContext(ctx, username, authorities) val email = Option(ctx.getAttributes().get("mail")).map(_.get().toString) - UserDetailsWithEmail(fromBase, email) + val displayName = Option(ctx.getAttributes().get("displayname")).map(_.get().toString) + + UserDetailsWithExtras(fromBase, email, displayName) } } diff --git a/service/src/main/scala/za/co/absa/loginsvc/rest/service/JWTService.scala b/service/src/main/scala/za/co/absa/loginsvc/rest/service/JWTService.scala index 7e2fb57..c50558c 100644 --- a/service/src/main/scala/za/co/absa/loginsvc/rest/service/JWTService.scala +++ b/service/src/main/scala/za/co/absa/loginsvc/rest/service/JWTService.scala @@ -17,11 +17,13 @@ package za.co.absa.loginsvc.rest.service import io.jsonwebtoken.security.Keys -import io.jsonwebtoken.{Jwts, SignatureAlgorithm} +import io.jsonwebtoken.{JwtBuilder, Jwts, SignatureAlgorithm} 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.provider.JwtConfigProvider +import za.co.absa.loginsvc.rest.service.JWTService.JwtBuilderExt +import za.co.absa.loginsvc.utils.OptionExt import java.security.{KeyPair, PublicKey} import java.time.Instant @@ -45,17 +47,14 @@ class JWTService @Autowired()(jwtConfigProvider: JwtConfigProvider) { // needs to be Java List/Array, otherwise incorrect claim is generated val groupsClaim = user.groups.asJava - val jwtBuilderWithoutEmail = Jwts + Jwts .builder() .setSubject(user.name) .setExpiration(expiration) .setIssuedAt(issuedAt) .claim("groups", groupsClaim) - - user - .email - .map(jwtBuilderWithoutEmail.claim("email", _)) - .getOrElse(jwtBuilderWithoutEmail) + .applyIfDefined(user.email, (builder, value: String) => builder.claim("email", value)) + .applyIfDefined(user.displayName, (builder, value: String) => builder.claim("displayname", value)) .signWith(rsaKeyPair.getPrivate) .compact() } @@ -63,3 +62,11 @@ class JWTService @Autowired()(jwtConfigProvider: JwtConfigProvider) { def publicKey: PublicKey = rsaKeyPair.getPublic } + +object JWTService { + implicit class JwtBuilderExt(val jwtBuilder: JwtBuilder) extends AnyVal { + def applyIfDefined[T](opt: Option[T], fn: (JwtBuilder, T) => JwtBuilder): JwtBuilder = { + OptionExt.applyIfDefined(jwtBuilder, opt, fn) + } + } +} diff --git a/service/src/main/scala/za/co/absa/loginsvc/utils/OptionExt.scala b/service/src/main/scala/za/co/absa/loginsvc/utils/OptionExt.scala new file mode 100644 index 0000000..02e11e5 --- /dev/null +++ b/service/src/main/scala/za/co/absa/loginsvc/utils/OptionExt.scala @@ -0,0 +1,38 @@ +/* + * 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.utils + +object OptionExt { + + /** + * For `target`, either return as is (if `optValueToApply` is None) or apply fn `func` + * @param target + * @param optValueToApply + * @param func + * @tparam A + * @tparam B + * @return + */ + def applyIfDefined[A, B](target: A, optValueToApply: Option[B], func: (A, B) => A): A = { + optValueToApply.map { valueToApply => + func(target, valueToApply) + }.getOrElse { + target + } + } + +} diff --git a/service/src/test/resources/application.yaml b/service/src/test/resources/application.yaml index 5c41ab7..f38a574 100644 --- a/service/src/test/resources/application.yaml +++ b/service/src/test/resources/application.yaml @@ -23,6 +23,12 @@ loginsvc: password: "password1" groups: - "group1" + - username: "user2" + password: "password2" + displayname: "User Two" + email: "user@two.org" + groups: + - "group2" # App Config spring: diff --git a/service/src/test/scala/za/co/absa/loginsvc/rest/FakeAuthentication.scala b/service/src/test/scala/za/co/absa/loginsvc/rest/FakeAuthentication.scala index 661127d..15b608d 100644 --- a/service/src/test/scala/za/co/absa/loginsvc/rest/FakeAuthentication.scala +++ b/service/src/test/scala/za/co/absa/loginsvc/rest/FakeAuthentication.scala @@ -26,7 +26,7 @@ import java.util object FakeAuthentication { - val fakeUser: User = User("fakeUser", Some("fake@gmail.com"), Seq.empty) + val fakeUser: User = User("fakeUser", Some("fake@gmail.com"), Some("Fake Name"), Seq.empty) val fakeUserAuthentication: Authentication = new UsernamePasswordAuthenticationToken( fakeUser, "fakePassword", new util.ArrayList[GrantedAuthority]() diff --git a/service/src/test/scala/za/co/absa/loginsvc/rest/config/auth/UsersConfigTest.scala b/service/src/test/scala/za/co/absa/loginsvc/rest/config/auth/UsersConfigTest.scala index 6dd1157..6d18df2 100644 --- a/service/src/test/scala/za/co/absa/loginsvc/rest/config/auth/UsersConfigTest.scala +++ b/service/src/test/scala/za/co/absa/loginsvc/rest/config/auth/UsersConfigTest.scala @@ -23,7 +23,7 @@ import za.co.absa.loginsvc.rest.config.validation.ConfigValidationResult.{Config class UsersConfigTest extends AnyFlatSpec with Matchers { - private val userCfg = UserConfig("user1", "password1", Option("mail@here.tld"), Array("group1", "group2")) + private val userCfg = UserConfig("user1", "password1", Option("mail@here.tld"), Option("Fake Name"), Array("group1", "group2")) "UserConfig" should "validate expected filled content" in { userCfg.validate() shouldBe ConfigValidationSuccess @@ -67,13 +67,13 @@ class UsersConfigTest extends AnyFlatSpec with Matchers { it should "fail on duplicate knownUsers" in { val duplicateValidationResult = UsersConfig(knownUsers = Array( - UserConfig("sameUser", "password1", Option("mail@here.tld"), Array("group1", "group2")), - UserConfig("sameUser", "password2", Option("anotherMail@here.tld"), Array()), + UserConfig("sameUser", "password1", Option("mail@here.tld"), Option("Fake1"), Array("group1", "group2")), + UserConfig("sameUser", "password2", Option("anotherMail@here.tld"), Option("Fake2"), Array()), - UserConfig("sameUser2", "passwordX", Option("abc@def"), Array()), - UserConfig("sameUser2", "passwordA", Option(null), Array()), + UserConfig("sameUser2", "passwordX", Option("abc@def"), Option("Fake1"), Array()), + UserConfig("sameUser2", "passwordA", Option(null), Option(null), Array()), - UserConfig("okUser", "passwordO", Option("ooo@"), Array()) + UserConfig("okUser", "passwordO", Option("ooo@"), Option("Fake1"), Array()) ), 1).validate() duplicateValidationResult shouldBe a[ConfigValidationError] @@ -86,12 +86,12 @@ class UsersConfigTest extends AnyFlatSpec with Matchers { it should "fail multiple errors" in { val multiErrorsResult = UsersConfig(knownUsers = Array( - UserConfig("sameUser", "password1", Option("mail@here.tld"), Array("group1", "group2")), - UserConfig("sameUser", "password2", Option("anotherMail@here.tld"), Array()), + UserConfig("sameUser", "password1", Option("mail@here.tld"), Option("Fake1"), Array("group1", "group2")), + UserConfig("sameUser", "password2", Option("anotherMail@here.tld"), Option("Fake2"), Array()), - UserConfig("userNoPass", null, Option("abc@def"), Array()), - UserConfig("noMailIsFine", "password2", Option(null), Array()), - UserConfig("userNoMissingGroups", "passwordO", Option("ooo@"), null) + UserConfig("userNoPass", null, Option("abc@def"), Option("FakeName"), Array()), + UserConfig("noMailIsFine", "password2", Option(null), Option(null), Array()), + UserConfig("userNoMissingGroups", "passwordO", Option("ooo@"), Option("Fake2"), null) ), 1).validate() multiErrorsResult shouldBe a[ConfigValidationError] diff --git a/service/src/test/scala/za/co/absa/loginsvc/rest/config/provider/ConfigProviderTest.scala b/service/src/test/scala/za/co/absa/loginsvc/rest/config/provider/ConfigProviderTest.scala index 469663c..823cffa 100644 --- a/service/src/test/scala/za/co/absa/loginsvc/rest/config/provider/ConfigProviderTest.scala +++ b/service/src/test/scala/za/co/absa/loginsvc/rest/config/provider/ConfigProviderTest.scala @@ -44,12 +44,20 @@ class ConfigProviderTest extends AnyFlatSpec with Matchers { activeDirectoryLDAPConfig.order == 1) } - "The usersConfig properties" should "Match" in { + "The usersConfig properties" should "be loaded correctly" in { val usersConfig: UsersConfig = configProvider.getUsersConfig + assert(usersConfig.order == 0) + assert(usersConfig.knownUsers(0).groups(0) == "group1" && usersConfig.knownUsers(0).email.isEmpty && + usersConfig.knownUsers(0).displayname.isEmpty && usersConfig.knownUsers(0).password == "password1" && - usersConfig.knownUsers(0).username == "user1" && - usersConfig.order == 0) + usersConfig.knownUsers(0).username == "user1") + + assert(usersConfig.knownUsers(1).groups(0) == "group2" && + usersConfig.knownUsers(1).email == Some("user@two.org") && + usersConfig.knownUsers(1).displayname == Some("User Two") && + usersConfig.knownUsers(1).password == "password2" && + usersConfig.knownUsers(1).username == "user2") } } diff --git a/service/src/test/scala/za/co/absa/loginsvc/rest/provider/ConfigUsersAuthenticationProviderTest.scala b/service/src/test/scala/za/co/absa/loginsvc/rest/provider/ConfigUsersAuthenticationProviderTest.scala index 9b8c9a1..c809e9d 100644 --- a/service/src/test/scala/za/co/absa/loginsvc/rest/provider/ConfigUsersAuthenticationProviderTest.scala +++ b/service/src/test/scala/za/co/absa/loginsvc/rest/provider/ConfigUsersAuthenticationProviderTest.scala @@ -27,7 +27,7 @@ class ConfigUsersAuthenticationProviderTest extends AnyFlatSpec with Matchers { // testing config we are running against val testConfig: UsersConfig = UsersConfig(Array( - UserConfig("testuser", "testpassword", Option("testuser@example.com"), Array()) + UserConfig("testuser", "testpassword", Option("testuser@example.com"), Option("Test User"), Array()) ), 1) val authProvider = new ConfigUsersAuthenticationProvider(testConfig) diff --git a/service/src/test/scala/za/co/absa/loginsvc/rest/service/JWTServiceTest.scala b/service/src/test/scala/za/co/absa/loginsvc/rest/service/JWTServiceTest.scala index f135402..400d1c1 100644 --- a/service/src/test/scala/za/co/absa/loginsvc/rest/service/JWTServiceTest.scala +++ b/service/src/test/scala/za/co/absa/loginsvc/rest/service/JWTServiceTest.scala @@ -32,6 +32,7 @@ class JWTServiceTest extends AnyFlatSpec { private val userWithoutEmailAndGroups: User = User( name = "testUser", email = None, + displayName = None, groups = Seq.empty ) diff --git a/service/src/test/scala/za/co/absa/loginsvc/utils/OptionExtTest.scala b/service/src/test/scala/za/co/absa/loginsvc/utils/OptionExtTest.scala new file mode 100644 index 0000000..4eaf5fe --- /dev/null +++ b/service/src/test/scala/za/co/absa/loginsvc/utils/OptionExtTest.scala @@ -0,0 +1,31 @@ +/* + * 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.utils + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class OptionExtTest extends AnyFlatSpec with Matchers { + + "OptionExt.applyIfDefined" should "apply fn correctly if defined" in { + OptionExt.applyIfDefined(1, Some(2), (a:Int, b: Int) => a + b) shouldBe 3 + } + + "OptionExt.applyIfDefined" should "not apply fn if empty" in { + OptionExt.applyIfDefined(1, None, (a: Int, b: Int) => a + b) shouldBe 1 + } + +}