Skip to content
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
9 changes: 7 additions & 2 deletions api/src/main/scala/za/co/absa/loginsvc/model/User.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@
package za.co.absa.loginsvc.model

case class User(name: String, groups: Seq[String], optionalAttributes: Map[String, Option[AnyRef]]) {
def filterGroupsByPrefixes(prefixes: Set[String]): User = {
val filteredGroups = groups.filter(group => prefixes.exists(group.startsWith))
def filterGroupsByPrefixes(prefixes: Set[String], caseSensitive: Boolean): User = {

val filteredGroups = if (caseSensitive) {
groups.filter(group => prefixes.exists(group.startsWith))
} else {
groups.filter(group => prefixes.map(_.toLowerCase).exists(group.toLowerCase.startsWith))
}

this.copy(groups = filteredGroups)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,19 @@ class TokenController @Autowired()(jwtService: JWTService, experimentalConfigPro
)
@Parameter(in = ParameterIn.QUERY, name = "group-prefixes", schema = new Schema(implementation = classOf[String]), example = "pam-,dehdl-",
description = "Prefixes of groups only to be returned in JWT user object (,-separated)")
@Parameter(in = ParameterIn.QUERY, name = "case-sensitive", schema = new Schema(implementation = classOf[Boolean], defaultValue = "false"), example = "true",
description = "case-sensitivity setting for group-prefixes lookup (default:false)")
@PostMapping(
path = Array("/generate"),
produces = Array(MediaType.APPLICATION_JSON_VALUE)
)
@ResponseStatus(HttpStatus.OK)
@SecurityRequirement(name = "basicAuth")
@SecurityRequirement(name = "negotiate")
def generateToken(authentication: Authentication, @RequestParam("group-prefixes") groupPrefixes: Optional[String]): TokensWrapper = {
def generateToken(authentication: Authentication,
@RequestParam("group-prefixes") groupPrefixes: Optional[String],
@RequestParam(name = "case-sensitive", defaultValue = "false") caseSensitive: Boolean
): TokensWrapper = {

val user: User = authentication.getPrincipal match {
case u: User => u
Expand All @@ -81,7 +86,7 @@ class TokenController @Autowired()(jwtService: JWTService, experimentalConfigPro

val filteredGroupsUser = user.applyIfDefined(groupPrefixesStrScala) { (user: User, prefixesStr: String) =>
val prefixes = prefixesStr.trim.split(',')
user.filterGroupsByPrefixes(prefixes.toSet)
user.filterGroupsByPrefixes(prefixes.toSet, caseSensitive)
}

val accessJwt = jwtService.generateAccessToken(filteredGroupsUser)
Expand All @@ -108,14 +113,19 @@ class TokenController @Autowired()(jwtService: JWTService, experimentalConfigPro
)
@Parameter(in = ParameterIn.QUERY, name = "group-prefixes", schema = new Schema(implementation = classOf[String]), example = "pam-,dehdl-",
description = "Prefixes of groups only to be returned in JWT user object (,-separated)")
@Parameter(in = ParameterIn.QUERY, name = "case-sensitive", schema = new Schema(implementation = classOf[Boolean], defaultValue = "false"), example = "true",
description = "case-sensitivity setting for group-prefixes lookup (default:false)")
@GetMapping(
path = Array("/experimental/get-generate"),
produces = Array(MediaType.APPLICATION_JSON_VALUE)
)
@ResponseStatus(HttpStatus.OK)
@SecurityRequirement(name = "basicAuth")
@SecurityRequirement(name = "negotiate")
def generateTokenExperimentalGet(authentication: Authentication, @RequestParam("group-prefixes") groupPrefixes: Optional[String]): TokensWrapper = {
def caseSensitive(authentication: Authentication,
@RequestParam("group-prefixes") groupPrefixes: Optional[String],
@RequestParam(name = "case-sensitive", defaultValue = "false") caseSensitive: Boolean
): TokensWrapper = {
failIfExperimentalIsNotAllowed()

val user: User = authentication.getPrincipal match {
Expand All @@ -127,7 +137,7 @@ class TokenController @Autowired()(jwtService: JWTService, experimentalConfigPro

val filteredGroupsUser = user.applyIfDefined(groupPrefixesStrScala) { (user: User, prefixesStr: String) =>
val prefixes = prefixesStr.trim.split(',')
user.filterGroupsByPrefixes(prefixes.toSet)
user.filterGroupsByPrefixes(prefixes.toSet, caseSensitive)
}

val accessJwt = jwtService.generateAccessToken(filteredGroupsUser)
Expand Down
14 changes: 10 additions & 4 deletions api/src/test/scala/za/co/absa/loginsvc/model/UserTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,20 @@ class UserTest extends AnyFlatSpec with Matchers {
"blue-123",
"blue-256",
"red-ABC",
"reddish-DEF",
"REDdish-DEF",
"black",
"black-and-white"
), Map.empty[String, Option[AnyRef]])

"User" should "filterGroups by prefixes" in {
testUser.filterGroupsByPrefixes(Set("red", "black", "yellow")) shouldBe
testUser.copy(groups = Seq("red-ABC", "reddish-DEF", "black","black-and-white"))
"User" should "filterGroups by prefixes (case-sensitively)" in {
testUser.filterGroupsByPrefixes(Set("red", "black", "yellow"), caseSensitive = true) shouldBe
testUser.copy(groups = Seq("red-ABC", "black","black-and-white"))
}

it should "filterGroups by prefixes (case-insensitively)" in {
testUser.filterGroupsByPrefixes(Set("red", "BLaCK", "yellow"), caseSensitive = false) shouldBe
testUser.copy(groups = Seq("red-ABC", "REDdish-DEF", "black","black-and-white"))
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,22 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.context.annotation.Import
import org.springframework.http.MediaType
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.{Authentication, GrantedAuthority}
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication
import org.springframework.test.context.TestPropertySource
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.{post, get}
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.{get, post}
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.{content, status}
import za.co.absa.loginsvc.model.User
import za.co.absa.loginsvc.rest.FakeAuthentication.fakeUser
import za.co.absa.loginsvc.rest.config.provider.ConfigProvider
import za.co.absa.loginsvc.rest.model.{AccessToken, RefreshToken}
import za.co.absa.loginsvc.rest.service.jwt.JWTService
import za.co.absa.loginsvc.rest.{AuthManagerConfig, FakeAuthentication, RestResponseEntityExceptionHandler, SecurityConfig}

import java.security.interfaces.RSAPublicKey
import java.util
import java.util.Base64
import scala.concurrent.duration._

Expand Down Expand Up @@ -81,9 +85,21 @@ class TokenControllerTest extends AnyFlatSpec
.andExpect(content.json(expectedJsonBody))
}

it should "return tokens generated by mocked JWTService for the authenticated user with group-prefixes (single)" in {
// aux methods for user-groups testing

def fakeUserWithGroups(groups:Seq[String]): User = User("fakeUser",
groups,
Map("mail" -> Some("[email protected]"), "displayname" -> Some("Fake Name")))

def fakeUserAuthenticationForUser(user: User): Authentication = new UsernamePasswordAuthenticationToken(
user, "fakePassword", new util.ArrayList[GrantedAuthority]()
)

it should "return tokens generated by mocked JWTService for the authenticated user with group-prefixes (single)(case irrelevant)" in {
// `groups-prefixes` fill change the groups in user object passed to the jwtService.generateAccessToken
val fakeUserFilteredGroups = FakeAuthentication.fakeUser.copy(groups = Seq("first-fake-group"))
val fakeUserFilteredGroups = fakeUserWithGroups(groups = Seq("first-fake-group"))
val fakeUserAuth = fakeUserAuthenticationForUser(fakeUserFilteredGroups)

when(jwtService.generateAccessToken(fakeUserFilteredGroups)).thenReturn(fakeAccessJwt)
when(jwtService.generateRefreshToken(fakeUserFilteredGroups)).thenReturn(fakeRefreshJwt)
when(jwtService.getConfiguredRefreshExpDuration).thenReturn(refreshDuration)
Expand All @@ -92,30 +108,64 @@ class TokenControllerTest extends AnyFlatSpec

mockMvc.perform(
post("/token/generate?group-prefixes=first")
.`with`(authentication(FakeAuthentication.fakeUserAuthentication))
.`with`(authentication(fakeUserAuth))
.contentType(MediaType.APPLICATION_JSON)
)
.andExpect(status.isOk)
.andExpect(content.json(expectedJsonBody))
}

it should "return tokens generated by mocked JWTService for the authenticated user with group-prefixes (multiple ,-separated)" in {
val fakeUserFilteredGroups = FakeAuthentication.fakeUser.copy(groups = Seq("second-fake-group", "third-fake-group"))
when(jwtService.generateAccessToken(fakeUserFilteredGroups)).thenReturn(fakeAccessJwt)
when(jwtService.generateRefreshToken(fakeUserFilteredGroups)).thenReturn(fakeRefreshJwt)
it should "return tokens generated by mocked JWTService for the authenticated user with group-prefixes (multiple ,-separated) (case-sensitive)" in {
val userGroups = Seq("second-fake-group", "THIRD-FAKE-GROUP", "FourTH-FaKe-Group")
val authUser = fakeUserWithGroups(userGroups)
val auth = fakeUserAuthenticationForUser(authUser)

val expectedGroups = Seq("second-fake-group", "THIRD-FAKE-GROUP")
val expectedUser = fakeUserWithGroups(expectedGroups)

when(jwtService.generateAccessToken(expectedUser)).thenReturn(fakeAccessJwt)
when(jwtService.generateRefreshToken(expectedUser)).thenReturn(fakeRefreshJwt)
when(jwtService.getConfiguredRefreshExpDuration).thenReturn(refreshDuration)

val expectedJsonBody = s"""{"token": "${fakeAccessJwt.token}", "refresh": "${fakeRefreshJwt.token}"}"""

mockMvc.perform(
post("/token/generate?group-prefixes=second,third,nonexistent")
.`with`(authentication(FakeAuthentication.fakeUserAuthentication))
post("/token/generate?group-prefixes=second,THIRD,fOurth,nonexistent&case-sensitive=true") // fOurth does not match case, so it is not selected for token generation
.`with`(authentication(auth))
.contentType(MediaType.APPLICATION_JSON)
)
.andExpect(status.isOk)
.andExpect(content.json(expectedJsonBody))
}

Seq(
("case-insensitive by default", "/token/generate?group-prefixes=second,THIRD,fOurth,nonexistent"),
("case-insensitive explicitly", "/token/generate?group-prefixes=second,THIRD,fOurth,nonexistent&case-sensitive=false")
).foreach { case (caseName, url) =>
it should s"return tokens generated by mocked JWTService for the authenticated user with group-prefixes (multiple ,-separated) ($caseName)" in {
val userGroups = Seq("second-fake-group", "THIRD-FAKE-GROUP", "FourTH-FaKe-Group", "otherGroup")
val authUser = fakeUserWithGroups(userGroups)
val auth = fakeUserAuthenticationForUser(authUser)

val expectedGroups = Seq("second-fake-group", "THIRD-FAKE-GROUP", "FourTH-FaKe-Group")
val expectedUser = fakeUserWithGroups(expectedGroups)

when(jwtService.generateAccessToken(expectedUser)).thenReturn(fakeAccessJwt)
when(jwtService.generateRefreshToken(expectedUser)).thenReturn(fakeRefreshJwt)
when(jwtService.getConfiguredRefreshExpDuration).thenReturn(refreshDuration)

val expectedJsonBody = s"""{"token": "${fakeAccessJwt.token}", "refresh": "${fakeRefreshJwt.token}"}"""

mockMvc.perform(
post(url) // fOurth gets matched
.`with`(authentication(auth))
.contentType(MediaType.APPLICATION_JSON)
)
.andExpect(status.isOk)
.andExpect(content.json(expectedJsonBody))
}
}

it should "fail for anonymous (not authenticated) user" in {
when(jwtService.generateAccessToken(any[User], any[Boolean])).thenReturn(fakeAccessJwt)

Expand Down