Skip to content

Commit fc8ac45

Browse files
authored
Feature/146 groups case sensitivity (#149)
* #146 instill current state in tests (prefixes are case sensitive) * #146 case sensitivity added for group-prefixes, swagger update, tests updated
1 parent 277b052 commit fc8ac45

File tree

4 files changed

+91
-20
lines changed

4 files changed

+91
-20
lines changed

api/src/main/scala/za/co/absa/loginsvc/model/User.scala

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,13 @@
1717
package za.co.absa.loginsvc.model
1818

1919
case class User(name: String, groups: Seq[String], optionalAttributes: Map[String, Option[AnyRef]]) {
20-
def filterGroupsByPrefixes(prefixes: Set[String]): User = {
21-
val filteredGroups = groups.filter(group => prefixes.exists(group.startsWith))
20+
def filterGroupsByPrefixes(prefixes: Set[String], caseSensitive: Boolean): User = {
21+
22+
val filteredGroups = if (caseSensitive) {
23+
groups.filter(group => prefixes.exists(group.startsWith))
24+
} else {
25+
groups.filter(group => prefixes.map(_.toLowerCase).exists(group.toLowerCase.startsWith))
26+
}
2227

2328
this.copy(groups = filteredGroups)
2429
}

api/src/main/scala/za/co/absa/loginsvc/rest/controller/TokenController.scala

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,19 @@ class TokenController @Autowired()(jwtService: JWTService, experimentalConfigPro
6363
)
6464
@Parameter(in = ParameterIn.QUERY, name = "group-prefixes", schema = new Schema(implementation = classOf[String]), example = "pam-,dehdl-",
6565
description = "Prefixes of groups only to be returned in JWT user object (,-separated)")
66+
@Parameter(in = ParameterIn.QUERY, name = "case-sensitive", schema = new Schema(implementation = classOf[Boolean], defaultValue = "false"), example = "true",
67+
description = "case-sensitivity setting for group-prefixes lookup (default:false)")
6668
@PostMapping(
6769
path = Array("/generate"),
6870
produces = Array(MediaType.APPLICATION_JSON_VALUE)
6971
)
7072
@ResponseStatus(HttpStatus.OK)
7173
@SecurityRequirement(name = "basicAuth")
7274
@SecurityRequirement(name = "negotiate")
73-
def generateToken(authentication: Authentication, @RequestParam("group-prefixes") groupPrefixes: Optional[String]): TokensWrapper = {
75+
def generateToken(authentication: Authentication,
76+
@RequestParam("group-prefixes") groupPrefixes: Optional[String],
77+
@RequestParam(name = "case-sensitive", defaultValue = "false") caseSensitive: Boolean
78+
): TokensWrapper = {
7479

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

8287
val filteredGroupsUser = user.applyIfDefined(groupPrefixesStrScala) { (user: User, prefixesStr: String) =>
8388
val prefixes = prefixesStr.trim.split(',')
84-
user.filterGroupsByPrefixes(prefixes.toSet)
89+
user.filterGroupsByPrefixes(prefixes.toSet, caseSensitive)
8590
}
8691

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

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

128138
val filteredGroupsUser = user.applyIfDefined(groupPrefixesStrScala) { (user: User, prefixesStr: String) =>
129139
val prefixes = prefixesStr.trim.split(',')
130-
user.filterGroupsByPrefixes(prefixes.toSet)
140+
user.filterGroupsByPrefixes(prefixes.toSet, caseSensitive)
131141
}
132142

133143
val accessJwt = jwtService.generateAccessToken(filteredGroupsUser)

api/src/test/scala/za/co/absa/loginsvc/model/UserTest.scala

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,20 @@ class UserTest extends AnyFlatSpec with Matchers {
2525
"blue-123",
2626
"blue-256",
2727
"red-ABC",
28-
"reddish-DEF",
28+
"REDdish-DEF",
2929
"black",
3030
"black-and-white"
3131
), Map.empty[String, Option[AnyRef]])
3232

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

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

api/src/test/scala/za/co/absa/loginsvc/rest/controller/TokenControllerTest.scala

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,22 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
2727
import org.springframework.boot.test.mock.mockito.MockBean
2828
import org.springframework.context.annotation.Import
2929
import org.springframework.http.MediaType
30+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
31+
import org.springframework.security.core.{Authentication, GrantedAuthority}
3032
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication
3133
import org.springframework.test.context.TestPropertySource
3234
import org.springframework.test.web.servlet.MockMvc
33-
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.{post, get}
35+
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.{get, post}
3436
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.{content, status}
3537
import za.co.absa.loginsvc.model.User
38+
import za.co.absa.loginsvc.rest.FakeAuthentication.fakeUser
3639
import za.co.absa.loginsvc.rest.config.provider.ConfigProvider
3740
import za.co.absa.loginsvc.rest.model.{AccessToken, RefreshToken}
3841
import za.co.absa.loginsvc.rest.service.jwt.JWTService
3942
import za.co.absa.loginsvc.rest.{AuthManagerConfig, FakeAuthentication, RestResponseEntityExceptionHandler, SecurityConfig}
4043

4144
import java.security.interfaces.RSAPublicKey
45+
import java.util
4246
import java.util.Base64
4347
import scala.concurrent.duration._
4448

@@ -81,9 +85,21 @@ class TokenControllerTest extends AnyFlatSpec
8185
.andExpect(content.json(expectedJsonBody))
8286
}
8387

84-
it should "return tokens generated by mocked JWTService for the authenticated user with group-prefixes (single)" in {
88+
// aux methods for user-groups testing
89+
90+
def fakeUserWithGroups(groups:Seq[String]): User = User("fakeUser",
91+
groups,
92+
Map("mail" -> Some("[email protected]"), "displayname" -> Some("Fake Name")))
93+
94+
def fakeUserAuthenticationForUser(user: User): Authentication = new UsernamePasswordAuthenticationToken(
95+
user, "fakePassword", new util.ArrayList[GrantedAuthority]()
96+
)
97+
98+
it should "return tokens generated by mocked JWTService for the authenticated user with group-prefixes (single)(case irrelevant)" in {
8599
// `groups-prefixes` fill change the groups in user object passed to the jwtService.generateAccessToken
86-
val fakeUserFilteredGroups = FakeAuthentication.fakeUser.copy(groups = Seq("first-fake-group"))
100+
val fakeUserFilteredGroups = fakeUserWithGroups(groups = Seq("first-fake-group"))
101+
val fakeUserAuth = fakeUserAuthenticationForUser(fakeUserFilteredGroups)
102+
87103
when(jwtService.generateAccessToken(fakeUserFilteredGroups)).thenReturn(fakeAccessJwt)
88104
when(jwtService.generateRefreshToken(fakeUserFilteredGroups)).thenReturn(fakeRefreshJwt)
89105
when(jwtService.getConfiguredRefreshExpDuration).thenReturn(refreshDuration)
@@ -92,30 +108,64 @@ class TokenControllerTest extends AnyFlatSpec
92108

93109
mockMvc.perform(
94110
post("/token/generate?group-prefixes=first")
95-
.`with`(authentication(FakeAuthentication.fakeUserAuthentication))
111+
.`with`(authentication(fakeUserAuth))
96112
.contentType(MediaType.APPLICATION_JSON)
97113
)
98114
.andExpect(status.isOk)
99115
.andExpect(content.json(expectedJsonBody))
100116
}
101117

102-
it should "return tokens generated by mocked JWTService for the authenticated user with group-prefixes (multiple ,-separated)" in {
103-
val fakeUserFilteredGroups = FakeAuthentication.fakeUser.copy(groups = Seq("second-fake-group", "third-fake-group"))
104-
when(jwtService.generateAccessToken(fakeUserFilteredGroups)).thenReturn(fakeAccessJwt)
105-
when(jwtService.generateRefreshToken(fakeUserFilteredGroups)).thenReturn(fakeRefreshJwt)
118+
it should "return tokens generated by mocked JWTService for the authenticated user with group-prefixes (multiple ,-separated) (case-sensitive)" in {
119+
val userGroups = Seq("second-fake-group", "THIRD-FAKE-GROUP", "FourTH-FaKe-Group")
120+
val authUser = fakeUserWithGroups(userGroups)
121+
val auth = fakeUserAuthenticationForUser(authUser)
122+
123+
val expectedGroups = Seq("second-fake-group", "THIRD-FAKE-GROUP")
124+
val expectedUser = fakeUserWithGroups(expectedGroups)
125+
126+
when(jwtService.generateAccessToken(expectedUser)).thenReturn(fakeAccessJwt)
127+
when(jwtService.generateRefreshToken(expectedUser)).thenReturn(fakeRefreshJwt)
106128
when(jwtService.getConfiguredRefreshExpDuration).thenReturn(refreshDuration)
107129

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

110132
mockMvc.perform(
111-
post("/token/generate?group-prefixes=second,third,nonexistent")
112-
.`with`(authentication(FakeAuthentication.fakeUserAuthentication))
133+
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
134+
.`with`(authentication(auth))
113135
.contentType(MediaType.APPLICATION_JSON)
114136
)
115137
.andExpect(status.isOk)
116138
.andExpect(content.json(expectedJsonBody))
117139
}
118140

141+
Seq(
142+
("case-insensitive by default", "/token/generate?group-prefixes=second,THIRD,fOurth,nonexistent"),
143+
("case-insensitive explicitly", "/token/generate?group-prefixes=second,THIRD,fOurth,nonexistent&case-sensitive=false")
144+
).foreach { case (caseName, url) =>
145+
it should s"return tokens generated by mocked JWTService for the authenticated user with group-prefixes (multiple ,-separated) ($caseName)" in {
146+
val userGroups = Seq("second-fake-group", "THIRD-FAKE-GROUP", "FourTH-FaKe-Group", "otherGroup")
147+
val authUser = fakeUserWithGroups(userGroups)
148+
val auth = fakeUserAuthenticationForUser(authUser)
149+
150+
val expectedGroups = Seq("second-fake-group", "THIRD-FAKE-GROUP", "FourTH-FaKe-Group")
151+
val expectedUser = fakeUserWithGroups(expectedGroups)
152+
153+
when(jwtService.generateAccessToken(expectedUser)).thenReturn(fakeAccessJwt)
154+
when(jwtService.generateRefreshToken(expectedUser)).thenReturn(fakeRefreshJwt)
155+
when(jwtService.getConfiguredRefreshExpDuration).thenReturn(refreshDuration)
156+
157+
val expectedJsonBody = s"""{"token": "${fakeAccessJwt.token}", "refresh": "${fakeRefreshJwt.token}"}"""
158+
159+
mockMvc.perform(
160+
post(url) // fOurth gets matched
161+
.`with`(authentication(auth))
162+
.contentType(MediaType.APPLICATION_JSON)
163+
)
164+
.andExpect(status.isOk)
165+
.andExpect(content.json(expectedJsonBody))
166+
}
167+
}
168+
119169
it should "fail for anonymous (not authenticated) user" in {
120170
when(jwtService.generateAccessToken(any[User], any[Boolean])).thenReturn(fakeAccessJwt)
121171

0 commit comments

Comments
 (0)