From ac9770dc0c288c30b1609e8a43d0aafa99c068f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Fri, 7 Nov 2025 20:23:05 +0100 Subject: [PATCH 01/15] qs --- .../blocks/ImpersonationWarning.scala | 6 + .../blocks/rules/auth/JwtAuthRule.scala | 234 +----------------- .../rules/auth/JwtAuthenticationRule.scala | 68 +++++ .../rules/auth/JwtAuthorizationRule.scala | 93 +++++++ .../blocks/rules/auth/base/BaseJwtRule.scala | 195 +++++++++++++++ .../blocks/users/LocalUsersContext.scala | 2 + .../variables/runtime/VariableContext.scala | 2 + .../rules/auth/JwtAuthRuleDecoder.scala | 58 +++-- .../blocks/rules/auth/JwtAuthRuleTests.scala | 38 +-- .../rules/auth/JwtAuthRuleSettingsTests.scala | 225 +++++++++-------- 10 files changed, 546 insertions(+), 375 deletions(-) create mode 100644 core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthenticationRule.scala create mode 100644 core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthorizationRule.scala create mode 100644 core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/base/BaseJwtRule.scala diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/ImpersonationWarning.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/ImpersonationWarning.scala index b3bacf1c5c..cbeb962dc6 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/ImpersonationWarning.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/ImpersonationWarning.scala @@ -135,6 +135,12 @@ object ImpersonationWarning { implicit val jwtAuthRule: ImpersonationWarningExtractor[JwtAuthRule] = ImpersonationWarningExtractor[JwtAuthRule] { (rule, blockName, _) => Some(impersonationNotSupportedWarning(rule, blockName)) } + implicit val jwtAuthenticationRule: ImpersonationWarningExtractor[JwtAuthenticationRule] = ImpersonationWarningExtractor[JwtAuthenticationRule] { (rule, blockName, _) => + Some(impersonationNotSupportedWarning(rule, blockName)) + } + implicit val jwtAuthorizationRule: ImpersonationWarningExtractor[JwtAuthorizationRule] = ImpersonationWarningExtractor[JwtAuthorizationRule] { (rule, blockName, _) => + Some(impersonationNotSupportedWarning(rule, blockName)) + } implicit val ldapAuthenticationRule: ImpersonationWarningExtractor[LdapAuthenticationRule] = ImpersonationWarningExtractor[LdapAuthenticationRule] { (rule, blockName, requestId) => ldapWarning(rule.name, blockName, rule.settings.ldap.id, rule.impersonation)(requestId) } diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthRule.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthRule.scala index 803ff69b99..74c1a0ef80 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthRule.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthRule.scala @@ -16,246 +16,24 @@ */ package tech.beshu.ror.accesscontrol.blocks.rules.auth -import io.jsonwebtoken.Jwts -import io.jsonwebtoken.security.Keys -import monix.eval.Task -import org.apache.logging.log4j.scala.Logging -import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef -import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef.SignatureCheckMethod.* import tech.beshu.ror.accesscontrol.blocks.rules.Rule import tech.beshu.ror.accesscontrol.blocks.rules.Rule.AuthenticationRule.EligibleUsersSupport -import tech.beshu.ror.accesscontrol.blocks.rules.Rule.RuleResult.{Fulfilled, Rejected} -import tech.beshu.ror.accesscontrol.blocks.rules.Rule.{AuthRule, RuleName, RuleResult} -import tech.beshu.ror.accesscontrol.blocks.rules.auth.JwtAuthRule.Groups -import tech.beshu.ror.accesscontrol.blocks.rules.auth.base.impersonation.{AuthenticationImpersonationCustomSupport, AuthorizationImpersonationCustomSupport} -import tech.beshu.ror.accesscontrol.blocks.{BlockContext, BlockContextUpdater} -import tech.beshu.ror.accesscontrol.domain.LoggedUser.DirectlyLoggedUser +import tech.beshu.ror.accesscontrol.blocks.rules.Rule.RuleName +import tech.beshu.ror.accesscontrol.blocks.rules.auth.base.BaseComposedAuthenticationAndAuthorizationRule import tech.beshu.ror.accesscontrol.domain.* -import tech.beshu.ror.accesscontrol.request.RequestContext -import tech.beshu.ror.accesscontrol.request.RequestContextOps.* -import tech.beshu.ror.accesscontrol.utils.ClaimsOps.* -import tech.beshu.ror.accesscontrol.utils.ClaimsOps.ClaimSearchResult.{Found, NotFound} -import tech.beshu.ror.implicits.* -import tech.beshu.ror.utils.RefinedUtils.* -import tech.beshu.ror.utils.uniquelist.{UniqueList, UniqueNonEmptyList} -import scala.util.Try - -final class JwtAuthRule(val settings: JwtAuthRule.Settings, - override val userIdCaseSensitivity: CaseSensitivity) - extends AuthRule - with AuthenticationImpersonationCustomSupport - with AuthorizationImpersonationCustomSupport - with Logging { +final class JwtAuthRule(val authentication: JwtAuthenticationRule, + val authorization: JwtAuthorizationRule) + extends BaseComposedAuthenticationAndAuthorizationRule(authentication, authorization) { override val name: Rule.Name = JwtAuthRule.Name.name override val eligibleUsers: EligibleUsersSupport = EligibleUsersSupport.NotAvailable - - private val parser = - settings.jwt.checkMethod match { - case NoCheck(_) => Jwts.parser().unsecured().build() - case Hmac(rawKey) => Jwts.parser().verifyWith(Keys.hmacShaKeyFor(rawKey)).build() - case Rsa(pubKey) => Jwts.parser().verifyWith(pubKey).build() - case Ec(pubKey) => Jwts.parser().verifyWith(pubKey).build() - } - - override protected[rules] def authenticate[B <: BlockContext : BlockContextUpdater](blockContext: B): Task[RuleResult[B]] = - Task.now(RuleResult.Fulfilled(blockContext)) - - override protected[rules] def authorize[B <: BlockContext : BlockContextUpdater](blockContext: B): Task[RuleResult[B]] = - Task - .unit - .flatMap { _ => - settings.permittedGroups match { - case Groups.NotDefined => - authorizeUsingJwtToken(blockContext) - case Groups.Defined(groupsLogic) if blockContext.isCurrentGroupPotentiallyEligible(groupsLogic) => - authorizeUsingJwtToken(blockContext) - case Groups.Defined(_) => - Task.now(RuleResult.Rejected()) - } - } - - private def authorizeUsingJwtToken[B <: BlockContext : BlockContextUpdater](blockContext: B): Task[RuleResult[B]] = { - jwtTokenFrom(blockContext.requestContext) match { - case None => - logger.debug(s"[${blockContext.requestContext.id.show}] Authorization header '${settings.jwt.authorizationTokenDef.headerName.show}' is missing or does not contain a JWT token") - Task.now(Rejected()) - case Some(token) => - process(token, blockContext) - } - } - - private def jwtTokenFrom(requestContext: RequestContext) = { - requestContext - .authorizationToken(settings.jwt.authorizationTokenDef) - .map(t => Jwt.Token(t.value)) - } - - private def process[B <: BlockContext : BlockContextUpdater](token: Jwt.Token, - blockContext: B): Task[RuleResult[B]] = { - implicit val requestId: RequestId = blockContext.requestContext.id.toRequestId - userAndGroupsFromJwtToken(token) match { - case Left(_) => - Task.now(Rejected()) - case Right((tokenPayload, user, groups)) => - if (logger.delegate.isDebugEnabled) { - logClaimSearchResults(user, groups)(blockContext.requestContext.id.toRequestId) - } - val claimProcessingResult = for { - newBlockContext <- handleUserClaimSearchResult(blockContext, user) - finalBlockContext <- handleGroupsClaimSearchResult(newBlockContext, groups) - } yield finalBlockContext.withUserMetadata(_.withJwtToken(tokenPayload)) - claimProcessingResult match { - case Left(_) => - Task.now(Rejected()) - case Right(modifiedBlockContext) => - settings.jwt.checkMethod match { - case NoCheck(service) => - implicit val requestId: RequestId = blockContext.requestContext.id.toRequestId - service - .authenticate(Credentials(User.Id(nes("jwt")), PlainTextSecret(token.value))) - .map(RuleResult.resultBasedOnCondition(modifiedBlockContext)(_)) - case Hmac(_) | Rsa(_) | Ec(_) => - Task.now(Fulfilled(modifiedBlockContext)) - } - } - } - } - - private def logClaimSearchResults(user: Option[ClaimSearchResult[User.Id]], - groups: Option[ClaimSearchResult[UniqueList[Group]]]) - (implicit requestId: RequestId): Unit = { - (settings.jwt.userClaim, user) match { - case (Some(userClaim), Some(u)) => - logger.debug(s"[${requestId.show}] JWT resolved user for claim ${userClaim.name.rawPath}: ${u.show}") - case _ => - } - (settings.jwt.groupsConfig, groups) match { - case (Some(groupsConfig), Some(g)) => - val claimsDescription = groupsConfig.namesClaim match { - case Some(namesClaim) => s"claims (id:'${groupsConfig.idsClaim.name.show}',name:'${namesClaim.name.show}')" - case None => s"claim '${groupsConfig.idsClaim.name.show}'" - } - logger.debug(s"[${requestId.show}] JWT resolved groups for ${claimsDescription.show}: ${g.show}") - case _ => - } - } - - private def userAndGroupsFromJwtToken(token: Jwt.Token) - (implicit requestId: RequestId) = { - claimsFrom(token).map { decodedJwtToken => - (decodedJwtToken, userIdFrom(decodedJwtToken), groupsFrom(decodedJwtToken)) - } - } - - private def logBadToken(ex: Throwable, token: Jwt.Token) - (implicit requestId: RequestId): Unit = { - val tokenParts = token.show.split("\\.") - val printableToken = if (!logger.delegate.isDebugEnabled && tokenParts.length === 3) { - // signed JWT, last block is the cryptographic digest, which should be treated as a secret. - s"${tokenParts(0)}.${tokenParts(1)} (omitted digest)" - } - else { - token.show - } - logger.debug(s"[${requestId.show}] JWT token '${printableToken.show}' parsing error: ${ex.getClass.getSimpleName.show} ${ex.getMessage.show}") - } - - private def claimsFrom(token: Jwt.Token) - (implicit requestId: RequestId) = { - settings.jwt.checkMethod match { - case NoCheck(_) => - token.value.value.split("\\.").toList match { - case fst :: snd :: _ => - Try(parser.parseUnsecuredClaims(s"$fst.$snd.").getPayload) - .toEither - .map(Jwt.Payload.apply) - .left.map { ex => logBadToken(ex, token) } - case _ => - Left(()) - } - case Hmac(_) | Rsa(_) | Ec(_) => - Try(parser.parseSignedClaims(token.value.value).getPayload) - .toEither - .map(Jwt.Payload.apply) - .left.map { ex => logBadToken(ex, token) } - } - } - - private def userIdFrom(payload: Jwt.Payload) = { - settings.jwt.userClaim.map(payload.claims.userIdClaim) - } - - private def groupsFrom(payload: Jwt.Payload) = { - settings.jwt.groupsConfig.map(groupsConfig => - payload.claims.groupsClaim(groupsConfig.idsClaim, groupsConfig.namesClaim) - ) - } - - private def handleUserClaimSearchResult[B <: BlockContext : BlockContextUpdater](blockContext: B, - result: Option[ClaimSearchResult[User.Id]]) = { - result match { - case None => Right(blockContext) - case Some(Found(userId)) => Right(blockContext.withUserMetadata(_.withLoggedUser(DirectlyLoggedUser(userId)))) - case Some(NotFound) => Left(()) - } - } - - private def handleGroupsClaimSearchResult[B <: BlockContext : BlockContextUpdater](blockContext: B, - result: Option[ClaimSearchResult[UniqueList[Group]]]) = { - (result, settings.permittedGroups) match { - case (None, Groups.Defined(_)) => - Left(()) - case (None, Groups.NotDefined) => - Right(blockContext) - case (Some(NotFound), Groups.Defined(_)) => - Left(()) - case (Some(NotFound), Groups.NotDefined) => - Right(blockContext) // if groups field is not found, we treat this situation as same as empty groups would be passed - case (Some(Found(groups)), Groups.Defined(groupsLogic)) => - UniqueNonEmptyList.from(groups) match { - case Some(nonEmptyGroups) => - groupsLogic.availableGroupsFrom(nonEmptyGroups) match { - case Some(matchedGroups) => - checkIfCanContinueWithGroups(blockContext, UniqueList.from(matchedGroups)) - .map(_.withUserMetadata(_.addAvailableGroups(matchedGroups))) - case None => - Left(()) - } - case None => - Left(()) - } - case (Some(Found(groups)), Groups.NotDefined) => - checkIfCanContinueWithGroups(blockContext, groups) - } - } - - private def checkIfCanContinueWithGroups[B <: BlockContext](blockContext: B, - groups: UniqueList[Group]) = { - UniqueNonEmptyList.from(groups.toList.map(_.id)) match { - case Some(nonEmptyGroups) if blockContext.isCurrentGroupEligible(GroupIds(nonEmptyGroups)) => - Right(blockContext) - case Some(_) | None => - Left(()) - } - } + override val userIdCaseSensitivity: CaseSensitivity = authentication.userIdCaseSensitivity } object JwtAuthRule { - implicit case object Name extends RuleName[JwtAuthRule] { override val name = Rule.Name("jwt_auth") } - - final case class Settings(jwt: JwtDef, permittedGroups: Groups) - - sealed trait Groups - - object Groups { - case object NotDefined extends Groups - - final case class Defined(groupsLogic: GroupsLogic) extends Groups - } } \ No newline at end of file diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthenticationRule.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthenticationRule.scala new file mode 100644 index 0000000000..ea8af49f61 --- /dev/null +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthenticationRule.scala @@ -0,0 +1,68 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.accesscontrol.blocks.rules.auth + +import monix.eval.Task +import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef +import tech.beshu.ror.accesscontrol.blocks.rules.Rule +import tech.beshu.ror.accesscontrol.blocks.rules.Rule.AuthenticationRule.EligibleUsersSupport +import tech.beshu.ror.accesscontrol.blocks.rules.Rule.{AuthenticationRule, RuleName, RuleResult} +import tech.beshu.ror.accesscontrol.blocks.rules.auth.JwtAuthenticationRule.Settings +import tech.beshu.ror.accesscontrol.blocks.rules.auth.base.BaseJwtRule +import tech.beshu.ror.accesscontrol.blocks.rules.auth.base.impersonation.AuthenticationImpersonationCustomSupport +import tech.beshu.ror.accesscontrol.blocks.{BlockContext, BlockContextUpdater} +import tech.beshu.ror.accesscontrol.domain.* +import tech.beshu.ror.accesscontrol.domain.LoggedUser.DirectlyLoggedUser +import tech.beshu.ror.accesscontrol.utils.ClaimsOps.ClaimSearchResult +import tech.beshu.ror.accesscontrol.utils.ClaimsOps.ClaimSearchResult.{Found, NotFound} + +final class JwtAuthenticationRule(val settings: Settings, + override val userIdCaseSensitivity: CaseSensitivity) + extends AuthenticationRule + with AuthenticationImpersonationCustomSupport + with BaseJwtRule { + + override val name: Rule.Name = JwtAuthenticationRule.Name.name + + override val eligibleUsers: EligibleUsersSupport = EligibleUsersSupport.NotAvailable + + override protected[rules] def authenticate[B <: BlockContext : BlockContextUpdater](blockContext: B): Task[RuleResult[B]] = { + processUsingJwtToken(blockContext, settings.jwt) { tokenData => + authenticate(blockContext, tokenData.userId, tokenData.payload) + }.flatMap(finalize(_, settings.jwt)) + } + + private def authenticate[B <: BlockContext : BlockContextUpdater](blockContext: B, + result: Option[ClaimSearchResult[User.Id]], + payload: Jwt.Payload) = { + (result match { + case None => Right(blockContext) + case Some(NotFound) => Left(()) + case Some(Found(userId)) => Right(blockContext.withUserMetadata(_.withLoggedUser(DirectlyLoggedUser(userId)))) + }).map(_.withUserMetadata(_.withJwtToken(payload))) + } + +} + +object JwtAuthenticationRule { + + implicit case object Name extends RuleName[JwtAuthenticationRule] { + override val name = Rule.Name("jwt_authentication") + } + + final case class Settings(jwt: JwtDef) +} diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthorizationRule.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthorizationRule.scala new file mode 100644 index 0000000000..9fdf453701 --- /dev/null +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthorizationRule.scala @@ -0,0 +1,93 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.accesscontrol.blocks.rules.auth + +import monix.eval.Task +import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef +import tech.beshu.ror.accesscontrol.blocks.rules.Rule +import tech.beshu.ror.accesscontrol.blocks.rules.Rule.{AuthorizationRule, RuleName, RuleResult} +import tech.beshu.ror.accesscontrol.blocks.rules.auth.JwtAuthorizationRule.Settings +import tech.beshu.ror.accesscontrol.blocks.rules.auth.base.BaseJwtRule +import tech.beshu.ror.accesscontrol.blocks.rules.auth.base.impersonation.AuthorizationImpersonationCustomSupport +import tech.beshu.ror.accesscontrol.blocks.{BlockContext, BlockContextUpdater} +import tech.beshu.ror.accesscontrol.domain.{Group, GroupIds, GroupsLogic} +import tech.beshu.ror.accesscontrol.utils.ClaimsOps.ClaimSearchResult +import tech.beshu.ror.accesscontrol.utils.ClaimsOps.ClaimSearchResult.* +import tech.beshu.ror.utils.uniquelist.{UniqueList, UniqueNonEmptyList} + +final class JwtAuthorizationRule(val settings: Settings) + extends AuthorizationRule + with AuthorizationImpersonationCustomSupport + with BaseJwtRule { + + override val name: Rule.Name = JwtAuthorizationRule.Name.name + + override protected[rules] def authorize[B <: BlockContext : BlockContextUpdater](blockContext: B): Task[RuleResult[B]] = { + settings.groupsLogic match { + case groupsLogic if blockContext.isCurrentGroupPotentiallyEligible(groupsLogic) => + processUsingJwtToken(blockContext, settings.jwt) { tokenData => + authorize(blockContext, tokenData.groups, groupsLogic) + } + case _ => + Task.now(RuleResult.Rejected()) + } + } + + private def authorize[B <: BlockContext : BlockContextUpdater](blockContext: B, + result: Option[ClaimSearchResult[UniqueList[Group]]], + groupsLogic: GroupsLogic) = { + (result, groupsLogic) match { + case (None, _) => + Left(()) + case (Some(NotFound), _) => + Left(()) + case (Some(Found(groups)), groupsLogic) => + UniqueNonEmptyList.from(groups) match { + case Some(nonEmptyGroups) => + groupsLogic.availableGroupsFrom(nonEmptyGroups) match { + case Some(matchedGroups) => + checkIfCanContinueWithGroups(blockContext, UniqueList.from(matchedGroups)) + .map(_.withUserMetadata(_.addAvailableGroups(matchedGroups))) + case None => + Left(()) + } + case None => + Left(()) + } + } + } + + private def checkIfCanContinueWithGroups[B <: BlockContext](blockContext: B, + groups: UniqueList[Group]) = { + UniqueNonEmptyList.from(groups.toList.map(_.id)) match { + case Some(nonEmptyGroups) if blockContext.isCurrentGroupEligible(GroupIds(nonEmptyGroups)) => + Right(blockContext) + case Some(_) | None => + Left(()) + } + } + +} + +object JwtAuthorizationRule { + + implicit case object Name extends RuleName[JwtAuthorizationRule] { + override val name = Rule.Name("jwt_authorization") + } + + final case class Settings(jwt: JwtDef, groupsLogic: GroupsLogic) +} diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/base/BaseJwtRule.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/base/BaseJwtRule.scala new file mode 100644 index 0000000000..19716a00ab --- /dev/null +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/base/BaseJwtRule.scala @@ -0,0 +1,195 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.accesscontrol.blocks.rules.auth.base + +import cats.implicits.toShow +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.security.Keys +import monix.eval.Task +import org.apache.logging.log4j.scala.Logging +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef +import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef.SignatureCheckMethod.{Ec, Hmac, NoCheck, Rsa} +import tech.beshu.ror.accesscontrol.blocks.rules.Rule +import tech.beshu.ror.accesscontrol.blocks.rules.Rule.RuleResult +import tech.beshu.ror.accesscontrol.blocks.rules.Rule.RuleResult.{Fulfilled, Rejected} +import tech.beshu.ror.accesscontrol.blocks.rules.auth.base.BaseJwtRule.* +import tech.beshu.ror.accesscontrol.domain.* +import tech.beshu.ror.accesscontrol.request.RequestContext +import tech.beshu.ror.accesscontrol.request.RequestContextOps.from +import tech.beshu.ror.accesscontrol.utils.ClaimsOps.* +import tech.beshu.ror.implicits.* +import tech.beshu.ror.utils.RefinedUtils.nes +import tech.beshu.ror.utils.uniquelist.UniqueList + +import scala.util.Try + +trait JwtRule extends Rule + +trait BaseJwtRule extends Logging { + + protected def processUsingJwtToken[B <: BlockContext](blockContext: B, + jwt: JwtDef) + (operation: JwtData => Either[Unit, B]): Task[RuleResult[B]] = { + implicit val jwtImpl: JwtDef = jwt + jwtTokenFrom(blockContext.requestContext) match { + case None => + logger.debug(s"[${blockContext.requestContext.id.show}] Authorization header '${jwt.authorizationTokenDef.headerName.show}' is missing or does not contain a JWT token") + Task.now(Rejected()) + case Some(token) => + implicit val requestId: RequestId = blockContext.requestContext.id.toRequestId + userAndGroupsFromJwtToken(token) match { + case Left(_) => + Task.now(Rejected()) + case Right(jwtData) => + if (logger.delegate.isDebugEnabled) { + logClaimSearchResults(jwtData.userId, jwtData.groups) + } + val claimProcessingResult = operation(jwtData) + claimProcessingResult match { + case Left(_) => + Task.now(Rejected()) + case Right(modifiedBlockContext) => + jwt.checkMethod match { + case NoCheck(service) => + service + .authenticate(Credentials(User.Id(nes("jwt")), PlainTextSecret(token.value))) + .map(RuleResult.resultBasedOnCondition(modifiedBlockContext)(_)) + case Hmac(_) | Rsa(_) | Ec(_) => + Task.now(Fulfilled(modifiedBlockContext)) + } + } + } + } + } + + protected def finalize[B <: BlockContext](result: RuleResult[B], + jwt: JwtDef): Task[RuleResult[B]] = { + implicit val jwtImpl: JwtDef = jwt + result match { + case rejected: Rejected[B] => + Task.now(rejected) + case Fulfilled(blockContext) => + jwtTokenFrom(blockContext.requestContext) match { + case None => + Task.now(Rejected()) + case Some(token) => + implicit val requestId: RequestId = blockContext.requestContext.id.toRequestId + jwt.checkMethod match { + case NoCheck(service) => + service + .authenticate(Credentials(User.Id(nes("jwt")), PlainTextSecret(token.value))) + .map(RuleResult.resultBasedOnCondition(blockContext)(_)) + case Hmac(_) | Rsa(_) | Ec(_) => + Task.now(Fulfilled(blockContext)) + } + } + } + } + + private def jwtTokenFrom(requestContext: RequestContext)(implicit jwt: JwtDef) = { + requestContext + .authorizationToken(jwt.authorizationTokenDef) + .map(t => Jwt.Token(t.value)) + } + + private def logClaimSearchResults(user: Option[ClaimSearchResult[User.Id]], + groups: Option[ClaimSearchResult[UniqueList[Group]]]) + (implicit requestId: RequestId, jwt: JwtDef): Unit = { + (jwt.userClaim, user) match { + case (Some(userClaim), Some(u)) => + logger.debug(s"[${requestId.show}] JWT resolved user for claim ${userClaim.name.rawPath}: ${u.show}") + case _ => + } + (jwt.groupsConfig, groups) match { + case (Some(groupsConfig), Some(g)) => + val claimsDescription = groupsConfig.namesClaim match { + case Some(namesClaim) => s"claims (id:'${groupsConfig.idsClaim.name.show}',name:'${namesClaim.name.show}')" + case None => s"claim '${groupsConfig.idsClaim.name.show}'" + } + logger.debug(s"[${requestId.show}] JWT resolved groups for ${claimsDescription.show}: ${g.show}") + case _ => + } + } + + private def userAndGroupsFromJwtToken(token: Jwt.Token) + (implicit requestId: RequestId, + jwt: JwtDef): Either[Unit, JwtData] = { + claimsFrom(token).map { decodedJwtToken => + JwtData(decodedJwtToken, userIdFrom(decodedJwtToken), groupsFrom(decodedJwtToken)) + } + } + + private def logBadToken(ex: Throwable, token: Jwt.Token) + (implicit requestId: RequestId): Unit = { + val tokenParts = token.show.split("\\.") + val printableToken = if (!logger.delegate.isDebugEnabled && tokenParts.length === 3) { + // signed JWT, last block is the cryptographic digest, which should be treated as a secret. + s"${tokenParts(0)}.${tokenParts(1)} (omitted digest)" + } + else { + token.show + } + logger.debug(s"[${requestId.show}] JWT token '${printableToken.show}' parsing error: ${ex.getClass.getSimpleName.show} ${ex.getMessage.show}") + } + + private def claimsFrom(token: Jwt.Token) + (implicit requestId: RequestId, + jwt: JwtDef) = { + val parser = jwt.checkMethod match { + case NoCheck(_) => Jwts.parser().unsecured().build() + case Hmac(rawKey) => Jwts.parser().verifyWith(Keys.hmacShaKeyFor(rawKey)).build() + case Rsa(pubKey) => Jwts.parser().verifyWith(pubKey).build() + case Ec(pubKey) => Jwts.parser().verifyWith(pubKey).build() + } + jwt.checkMethod match { + case NoCheck(_) => + token.value.value.split("\\.").toList match { + case fst :: snd :: _ => + Try(parser.parseUnsecuredClaims(s"$fst.$snd.").getPayload) + .toEither + .map(Jwt.Payload.apply) + .left.map { ex => logBadToken(ex, token) } + case _ => + Left(()) + } + case Hmac(_) | Rsa(_) | Ec(_) => + Try(parser.parseSignedClaims(token.value.value).getPayload) + .toEither + .map(Jwt.Payload.apply) + .left.map { ex => logBadToken(ex, token) } + } + } + + private def userIdFrom(payload: Jwt.Payload)(implicit jwt: JwtDef): Option[ClaimSearchResult[User.Id]] = { + jwt.userClaim.map(payload.claims.userIdClaim) + } + + private def groupsFrom(payload: Jwt.Payload)(implicit jwt: JwtDef): Option[ClaimSearchResult[UniqueList[Group]]] = { + jwt.groupsConfig.map(groupsConfig => + payload.claims.groupsClaim(groupsConfig.idsClaim, groupsConfig.namesClaim) + ) + } +} + +object BaseJwtRule { + + protected final case class JwtData(payload: Jwt.Payload, + userId: Option[ClaimSearchResult[User.Id]], + groups: Option[ClaimSearchResult[UniqueList[Group]]]) + +} diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/users/LocalUsersContext.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/users/LocalUsersContext.scala index c5f9d26671..dd68b8b1ec 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/users/LocalUsersContext.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/users/LocalUsersContext.scala @@ -61,6 +61,8 @@ object LocalUsersContext { implicit val hostsRule: LocalUsersSupport[HostsRule] = NotAvailableLocalUsers() implicit val indicesRule: LocalUsersSupport[IndicesRule] = NotAvailableLocalUsers() implicit val jwtAuthRule: LocalUsersSupport[JwtAuthRule] = NotAvailableLocalUsers() + implicit val jwtAuthenticationRule: LocalUsersSupport[JwtAuthenticationRule] = NotAvailableLocalUsers() + implicit val jwtAuthorizationRule: LocalUsersSupport[JwtAuthorizationRule] = NotAvailableLocalUsers() implicit val kibanaUserDataRule: LocalUsersSupport[KibanaUserDataRule] = NotAvailableLocalUsers() implicit val kibanaAccessRule: LocalUsersSupport[KibanaAccessRule] = NotAvailableLocalUsers() implicit val kibanaHideAppsRule: LocalUsersSupport[KibanaHideAppsRule] = NotAvailableLocalUsers() diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/variables/runtime/VariableContext.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/variables/runtime/VariableContext.scala index dd21e2f995..f26455a01f 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/variables/runtime/VariableContext.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/variables/runtime/VariableContext.scala @@ -86,6 +86,8 @@ object VariableContext { implicit val headersAndRule: VariableUsage[HeadersAndRule] = NotUsingVariable() implicit val headersOrRule: VariableUsage[HeadersOrRule] = NotUsingVariable() implicit val jwtAuthRule: VariableUsage[JwtAuthRule] = NotUsingVariable() + implicit val jwtAuthenticationRule: VariableUsage[JwtAuthenticationRule] = NotUsingVariable() + implicit val jwtAuthorizationRule: VariableUsage[JwtAuthorizationRule] = NotUsingVariable() implicit val kibanaHideAppsRule: VariableUsage[KibanaHideAppsRule] = NotUsingVariable() implicit val ldapAuthenticationRule: VariableUsage[LdapAuthenticationRule] = NotUsingVariable() implicit val ldapAuthorizationRule: VariableUsage[LdapAuthorizationRule] = NotUsingVariable() diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtAuthRuleDecoder.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtAuthRuleDecoder.scala index 35d45c592e..cd23f7a599 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtAuthRuleDecoder.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtAuthRuleDecoder.scala @@ -17,66 +17,86 @@ package tech.beshu.ror.accesscontrol.factory.decoders.rules.auth import io.circe.Decoder +import monix.eval.Task +import org.apache.logging.log4j.scala.Logging import tech.beshu.ror.accesscontrol.blocks.Block.RuleDefinition import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef -import tech.beshu.ror.accesscontrol.blocks.rules.auth.JwtAuthRule -import tech.beshu.ror.accesscontrol.blocks.rules.auth.JwtAuthRule.Groups +import tech.beshu.ror.accesscontrol.blocks.rules.Rule +import tech.beshu.ror.accesscontrol.blocks.rules.Rule.{AuthorizationRule, RuleName} +import tech.beshu.ror.accesscontrol.blocks.rules.auth.base.impersonation.AuthorizationImpersonationCustomSupport +import tech.beshu.ror.accesscontrol.blocks.rules.auth.{JwtAuthRule, JwtAuthenticationRule, JwtAuthorizationRule} +import tech.beshu.ror.accesscontrol.blocks.{BlockContext, BlockContextUpdater} +import tech.beshu.ror.accesscontrol.domain.GroupsLogic import tech.beshu.ror.accesscontrol.factory.GlobalSettings import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.Reason.Message import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.RulesLevelCreationError import tech.beshu.ror.accesscontrol.factory.decoders.definitions.Definitions import tech.beshu.ror.accesscontrol.factory.decoders.definitions.JwtDefinitionsDecoder.* import tech.beshu.ror.accesscontrol.factory.decoders.rules.RuleBaseDecoder.RuleBaseDecoderWithoutAssociatedFields -import tech.beshu.ror.accesscontrol.factory.decoders.rules.auth.groups.GroupsLogicRepresentationDecoder.GroupsLogicDecodingResult +import tech.beshu.ror.accesscontrol.factory.decoders.rules.auth.JwtAuthRuleDecoder.cannotFindJwtDefinition import tech.beshu.ror.accesscontrol.factory.decoders.rules.auth.groups.GroupsLogicDecoder +import tech.beshu.ror.accesscontrol.factory.decoders.rules.auth.groups.GroupsLogicRepresentationDecoder.GroupsLogicDecodingResult import tech.beshu.ror.accesscontrol.utils.CirceOps.* import tech.beshu.ror.implicits.* +private implicit val ruleName: RuleName[Rule] = new RuleName[Rule] { + override def name: Rule.Name = JwtAuthRule.Name.name +} + class JwtAuthRuleDecoder(jwtDefinitions: Definitions[JwtDef], globalSettings: GlobalSettings) - extends RuleBaseDecoderWithoutAssociatedFields[JwtAuthRule] { + extends RuleBaseDecoderWithoutAssociatedFields[JwtAuthRule | JwtAuthenticationRule] with Logging { - override protected def decoder: Decoder[RuleDefinition[JwtAuthRule]] = { + override protected def decoder: Decoder[RuleDefinition[JwtAuthRule | JwtAuthenticationRule]] = { JwtAuthRuleDecoder.nameAndGroupsSimpleDecoder .or(JwtAuthRuleDecoder.nameAndGroupsExtendedDecoder) .toSyncDecoder - .emapE { case (name, groupsLogic) => - jwtDefinitions.items.find(_.id === name) match { - case Some(jwtDef) => Right(JwtAuthRule.Settings(jwtDef, groupsLogic)) - case None => Left(RulesLevelCreationError(Message(s"Cannot find JWT definition with name: ${name.show}"))) + .emapE[RuleDefinition[JwtAuthRule | JwtAuthenticationRule]] { case (name, groupsLogicOpt) => + val foundKbnDef = jwtDefinitions.items.find(_.id === name) + (foundKbnDef, groupsLogicOpt) match { + case (Some(jwtDef), Some(groupsLogic)) => + val authentication = new JwtAuthenticationRule(JwtAuthenticationRule.Settings(jwtDef), globalSettings.userIdCaseSensitivity) + val authorization = new JwtAuthorizationRule(JwtAuthorizationRule.Settings(jwtDef, groupsLogic)) + val rule = new JwtAuthRule(authentication, authorization) + Right(RuleDefinition.create(rule)) + case (Some(jwtDef), None) => + val rule = new JwtAuthenticationRule(JwtAuthenticationRule.Settings(jwtDef), globalSettings.userIdCaseSensitivity) + Right(RuleDefinition.create(rule): RuleDefinition[JwtAuthenticationRule]) + case (None, _) => + Left(cannotFindJwtDefinition(name)) } } - .map { settings => - RuleDefinition.create(new JwtAuthRule(settings, globalSettings.userIdCaseSensitivity)) - } .decoder } } private object JwtAuthRuleDecoder { - private val nameAndGroupsSimpleDecoder: Decoder[(JwtDef.Name, Groups)] = + def cannotFindJwtDefinition(name: JwtDef.Name) = + RulesLevelCreationError(Message(s"Cannot find JWT definition with name: ${name.show}")) + + private val nameAndGroupsSimpleDecoder: Decoder[(JwtDef.Name, Option[GroupsLogic])] = DecoderHelpers .decodeStringLikeNonEmpty .map(JwtDef.Name.apply) - .map((_, Groups.NotDefined)) + .map((_, None)) - private val nameAndGroupsExtendedDecoder: Decoder[(JwtDef.Name, Groups)] = + private val nameAndGroupsExtendedDecoder: Decoder[(JwtDef.Name, Option[GroupsLogic])] = Decoder .instance { c => for { - rorKbnDefName <- c.downField("name").as[JwtDef.Name] + jwtDefName <- c.downField("name").as[JwtDef.Name] groupsLogicDecodingResult <- GroupsLogicDecoder.decoder[JwtAuthRule].apply(c) - } yield (rorKbnDefName, groupsLogicDecodingResult) + } yield (jwtDefName, groupsLogicDecodingResult) } .toSyncDecoder .emapE { case (name, groupsLogicDecodingResult) => groupsLogicDecodingResult match { case GroupsLogicDecodingResult.Success(groupsLogic) => - Right((name, Groups.Defined(groupsLogic))) + Right((name, Some(groupsLogic))) case GroupsLogicDecodingResult.GroupsLogicNotDefined(_) => - Right((name, Groups.NotDefined: Groups)) + Right((name, None)) case GroupsLogicDecodingResult.MultipleGroupsLogicsDefined(_, fields) => val fieldsStr = fields.map(f => s"'$f'").mkString(" or ") Left(RulesLevelCreationError(Message( diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthRuleTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthRuleTests.scala index 90a2fb9bb0..7ee223f444 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthRuleTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthRuleTests.scala @@ -33,8 +33,7 @@ import tech.beshu.ror.accesscontrol.blocks.definitions.ExternalAuthenticationSer import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef.{GroupsConfig, SignatureCheckMethod} import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata import tech.beshu.ror.accesscontrol.blocks.rules.Rule.RuleResult.{Fulfilled, Rejected} -import tech.beshu.ror.accesscontrol.blocks.rules.auth.JwtAuthRule -import tech.beshu.ror.accesscontrol.blocks.rules.auth.JwtAuthRule.Groups +import tech.beshu.ror.accesscontrol.blocks.rules.auth.{JwtAuthRule, JwtAuthenticationRule, JwtAuthorizationRule} import tech.beshu.ror.accesscontrol.domain import tech.beshu.ror.accesscontrol.domain.GroupIdLike.GroupId import tech.beshu.ror.accesscontrol.domain.LoggedUser.DirectlyLoggedUser @@ -252,7 +251,7 @@ class JwtAuthRuleTests userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) ), - configuredGroups = Groups.NotDefined, + configuredGroups = None, tokenHeader = bearerHeader(jwt) ) { blockContext => @@ -392,7 +391,7 @@ class JwtAuthRuleTests namesClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.name)].name"))) )) ), - configuredGroups = Groups.Defined(GroupsLogic.AnyOf(GroupIds( + configuredGroups = Some(GroupsLogic.AnyOf(GroupIds( UniqueNonEmptyList.of(GroupId("group3"), GroupId("group2")) ))), tokenHeader = bearerHeader(jwt) @@ -426,7 +425,7 @@ class JwtAuthRuleTests namesClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.name)].name"))) )) ), - configuredGroups = Groups.Defined(GroupsLogic.AnyOf(GroupIds( + configuredGroups = Some(GroupsLogic.AnyOf(GroupIds( UniqueNonEmptyList.of(GroupId("group3"), GroupIdLike.from("*2")) ))), tokenHeader = bearerHeader(jwt) @@ -460,7 +459,7 @@ class JwtAuthRuleTests namesClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.name)].name"))) )) ), - configuredGroups = Groups.Defined(GroupsLogic.AllOf(GroupIds( + configuredGroups = Some(GroupsLogic.AllOf(GroupIds( UniqueNonEmptyList.of(GroupId("group1"), GroupId("group2")) ))), tokenHeader = bearerHeader(jwt) @@ -494,7 +493,7 @@ class JwtAuthRuleTests namesClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.name)].name"))) )) ), - configuredGroups = Groups.Defined(GroupsLogic.AllOf(GroupIds( + configuredGroups = Some(GroupsLogic.AllOf(GroupIds( UniqueNonEmptyList.of(GroupIdLike.from("*1"), GroupIdLike.from("*2")) ))), tokenHeader = bearerHeader(jwt) @@ -636,7 +635,7 @@ class JwtAuthRuleTests userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("tech.beshu.groups.subgroups")), None)) ), - configuredGroups = Groups.Defined(GroupsLogic.AnyOf(GroupIds( + configuredGroups = Some(GroupsLogic.AnyOf(GroupIds( UniqueNonEmptyList.of(GroupId("group1")) ))), tokenHeader = bearerHeader(jwt) @@ -656,7 +655,7 @@ class JwtAuthRuleTests userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) ), - configuredGroups = Groups.Defined(GroupsLogic.AnyOf(GroupIds( + configuredGroups = Some(GroupsLogic.AnyOf(GroupIds( UniqueNonEmptyList.of(GroupId("group3"), GroupId("group4")) ))), tokenHeader = bearerHeader(jwt) @@ -676,7 +675,7 @@ class JwtAuthRuleTests userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) ), - configuredGroups = Groups.Defined(GroupsLogic.AllOf(GroupIds( + configuredGroups = Some(GroupsLogic.AllOf(GroupIds( UniqueNonEmptyList.of(GroupId("group2"), GroupId("group3")) ))), tokenHeader = bearerHeader(jwt) @@ -714,7 +713,7 @@ class JwtAuthRuleTests userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) ), - configuredGroups = Groups.Defined(GroupsLogic.AnyOf(GroupIds( + configuredGroups = Some(GroupsLogic.AnyOf(GroupIds( UniqueNonEmptyList.of(GroupId("group2")) ))), tokenHeader = bearerHeader(jwt), @@ -725,24 +724,33 @@ class JwtAuthRuleTests } private def assertMatchRule(configuredJwtDef: JwtDef, - configuredGroups: Groups = Groups.NotDefined, + configuredGroups: Option[GroupsLogic] = None, tokenHeader: Header, preferredGroupId: Option[GroupId] = None) (blockContextAssertion: BlockContext => Unit): Unit = assertRule(configuredJwtDef, configuredGroups, tokenHeader, preferredGroupId, Some(blockContextAssertion)) private def assertNotMatchRule(configuredJwtDef: JwtDef, - configuredGroups: Groups = Groups.NotDefined, + configuredGroups: Option[GroupsLogic] = None, tokenHeader: Header, preferredGroupId: Option[GroupId] = None): Unit = assertRule(configuredJwtDef, configuredGroups, tokenHeader, preferredGroupId, blockContextAssertion = None) private def assertRule(configuredJwtDef: JwtDef, - configuredGroups: Groups, + configuredGroups: Option[GroupsLogic], tokenHeader: Header, preferredGroup: Option[GroupId], blockContextAssertion: Option[BlockContext => Unit]) = { - val rule = new JwtAuthRule(JwtAuthRule.Settings(configuredJwtDef, configuredGroups), CaseSensitivity.Enabled) + val rule = configuredGroups match { + case Some(groupsLogic) => + new JwtAuthRule( + new JwtAuthenticationRule(JwtAuthenticationRule.Settings(configuredJwtDef), CaseSensitivity.Enabled), + new JwtAuthorizationRule(JwtAuthorizationRule.Settings(configuredJwtDef, groupsLogic)), + ) + case None => + new JwtAuthenticationRule(JwtAuthenticationRule.Settings(configuredJwtDef), CaseSensitivity.Enabled) + } + val requestContext = MockRequestContext.indices.withHeaders( preferredGroup.map(_.toCurrentGroupHeader).toSeq :+ tokenHeader ) diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthRuleSettingsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthRuleSettingsTests.scala index 8a8b573863..c065ae06d0 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthRuleSettingsTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthRuleSettingsTests.scala @@ -20,7 +20,6 @@ import org.scalatest.matchers.should.Matchers.* import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef.{GroupsConfig, SignatureCheckMethod} import tech.beshu.ror.accesscontrol.blocks.definitions.{CacheableExternalAuthenticationServiceDecorator, JwtDef} import tech.beshu.ror.accesscontrol.blocks.rules.auth.JwtAuthRule -import tech.beshu.ror.accesscontrol.blocks.rules.auth.JwtAuthRule.Groups import tech.beshu.ror.accesscontrol.domain import tech.beshu.ror.accesscontrol.domain.GroupIdLike.GroupId import tech.beshu.ror.accesscontrol.domain.* @@ -62,12 +61,12 @@ class JwtAuthRuleSettingsTests | |""".stripMargin, assertion = rule => { - rule.settings.jwt.id should be(JwtDef.Name("jwt1")) - rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) - rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(None) - rule.settings.permittedGroups should be(Groups.NotDefined) + rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) + rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] + rule.authentication.settings.jwt.userClaim should be(None) + rule.authentication.settings.jwt.groupsConfig should be(None) + //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) } ) } @@ -90,12 +89,12 @@ class JwtAuthRuleSettingsTests | |""".stripMargin, assertion = rule => { - rule.settings.jwt.id should be(JwtDef.Name("jwt1")) - rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) - rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(None) - rule.settings.permittedGroups should be(Groups.NotDefined) + rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) + rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] + rule.authentication.settings.jwt.userClaim should be(None) + rule.authentication.settings.jwt.groupsConfig should be(None) + //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) } ) } @@ -120,14 +119,14 @@ class JwtAuthRuleSettingsTests | |""".stripMargin, assertion = rule => { - rule.settings.jwt.id should be(JwtDef.Name("jwt1")) - rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) - rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(None) - rule.settings.permittedGroups should be(Groups.Defined(GroupsLogic.AnyOf(GroupIds( + rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) + rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] + rule.authentication.settings.jwt.userClaim should be(None) + rule.authentication.settings.jwt.groupsConfig should be(None) + rule.authorization.settings.groupsLogic should be(GroupsLogic.AnyOf(GroupIds( UniqueNonEmptyList.of(GroupIdLike.from("group1*"), GroupId("group2")) - )))) + ))) } ) } @@ -153,14 +152,14 @@ class JwtAuthRuleSettingsTests | |""".stripMargin, assertion = rule => { - rule.settings.jwt.id should be(JwtDef.Name("jwt1")) - rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) - rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(None) - rule.settings.permittedGroups should be(Groups.Defined(GroupsLogic.AllOf(GroupIds( + rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) + rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] + rule.authentication.settings.jwt.userClaim should be(None) + rule.authentication.settings.jwt.groupsConfig should be(None) + rule.authorization.settings.groupsLogic should be(GroupsLogic.AllOf(GroupIds( UniqueNonEmptyList.of(GroupIdLike.from("group1*"), GroupId("group2")) - )))) + ))) } ) } @@ -185,12 +184,12 @@ class JwtAuthRuleSettingsTests | |""".stripMargin, assertion = rule => { - rule.settings.jwt.id should be(JwtDef.Name("jwt1")) - rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(headerNameFrom("X-JWT-Custom-Header"), "Bearer ")) - rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(None) - rule.settings.permittedGroups should be(Groups.NotDefined) + rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(headerNameFrom("X-JWT-Custom-Header"), "Bearer ")) + rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] + rule.authentication.settings.jwt.userClaim should be(None) + rule.authentication.settings.jwt.groupsConfig should be(None) + //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) } ) } @@ -214,12 +213,12 @@ class JwtAuthRuleSettingsTests | |""".stripMargin, assertion = rule => { - rule.settings.jwt.id should be(JwtDef.Name("jwt1")) - rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(headerNameFrom("X-JWT-Custom-Header"), "MyPrefix ")) - rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(None) - rule.settings.permittedGroups should be(Groups.NotDefined) + rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(headerNameFrom("X-JWT-Custom-Header"), "MyPrefix ")) + rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] + rule.authentication.settings.jwt.userClaim should be(None) + rule.authentication.settings.jwt.groupsConfig should be(None) + //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) } ) } @@ -242,12 +241,12 @@ class JwtAuthRuleSettingsTests | |""".stripMargin, assertion = rule => { - rule.settings.jwt.id should be(JwtDef.Name("jwt1")) - rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "MyPrefix ")) - rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(None) - rule.settings.permittedGroups should be(Groups.NotDefined) + rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "MyPrefix ")) + rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] + rule.authentication.settings.jwt.userClaim should be(None) + rule.authentication.settings.jwt.groupsConfig should be(None) + //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) } ) } @@ -271,12 +270,12 @@ class JwtAuthRuleSettingsTests | |""".stripMargin, assertion = rule => { - rule.settings.jwt.id should be(JwtDef.Name("jwt1")) - rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(headerNameFrom("X-JWT-Custom-Header"), "")) - rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(None) - rule.settings.permittedGroups should be(Groups.NotDefined) + rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(headerNameFrom("X-JWT-Custom-Header"), "")) + rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] + rule.authentication.settings.jwt.userClaim should be(None) + rule.authentication.settings.jwt.groupsConfig should be(None) + //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) } ) } @@ -299,12 +298,12 @@ class JwtAuthRuleSettingsTests | |""".stripMargin, assertion = rule => { - rule.settings.jwt.id should be(JwtDef.Name("jwt1")) - rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) - rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] - rule.settings.jwt.userClaim should be(Some(domain.Jwt.ClaimName(jsonPathFrom("user")))) - rule.settings.jwt.groupsConfig should be(None) - rule.settings.permittedGroups should be(Groups.NotDefined) + rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) + rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] + rule.authentication.settings.jwt.userClaim should be(Some(domain.Jwt.ClaimName(jsonPathFrom("user")))) + rule.authentication.settings.jwt.groupsConfig should be(None) + //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) } ) } @@ -329,12 +328,12 @@ class JwtAuthRuleSettingsTests | |""".stripMargin, assertion = rule => { - rule.settings.jwt.id should be(JwtDef.Name("jwt1")) - rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) - rule.settings.jwt.checkMethod shouldBe a[SignatureCheckMethod.Hmac] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) - rule.settings.permittedGroups should be(Groups.NotDefined) + rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) + rule.authentication.settings.jwt.checkMethod shouldBe a[SignatureCheckMethod.Hmac] + rule.authentication.settings.jwt.userClaim should be(None) + rule.authentication.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) + //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) } ) } @@ -359,15 +358,15 @@ class JwtAuthRuleSettingsTests | |""".stripMargin, assertion = rule => { - rule.settings.jwt.id should be(JwtDef.Name("jwt1")) - rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) - rule.settings.jwt.checkMethod shouldBe a[SignatureCheckMethod.Hmac] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(Some(GroupsConfig( + rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) + rule.authentication.settings.jwt.checkMethod shouldBe a[SignatureCheckMethod.Hmac] + rule.authentication.settings.jwt.userClaim should be(None) + rule.authentication.settings.jwt.groupsConfig should be(Some(GroupsConfig( idsClaim = domain.Jwt.ClaimName(jsonPathFrom("groups")), namesClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("group_names"))) ))) - rule.settings.permittedGroups should be(Groups.NotDefined) + //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) } ) } @@ -390,12 +389,12 @@ class JwtAuthRuleSettingsTests | |""".stripMargin, assertion = rule => { - rule.settings.jwt.id should be(JwtDef.Name("jwt1")) - rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) - rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("https://{domain}/claims/roles")), None))) - rule.settings.permittedGroups should be(Groups.NotDefined) + rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) + rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] + rule.authentication.settings.jwt.userClaim should be(None) + rule.authentication.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("https://{domain}/claims/roles")), None))) + //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) } ) } @@ -420,12 +419,12 @@ class JwtAuthRuleSettingsTests | |""".stripMargin, assertion = rule => { - rule.settings.jwt.id should be(JwtDef.Name("jwt1")) - rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) - rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Rsa] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) - rule.settings.permittedGroups should be(Groups.NotDefined) + rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) + rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Rsa] + rule.authentication.settings.jwt.userClaim should be(None) + rule.authentication.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) + //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) } ) } @@ -449,12 +448,12 @@ class JwtAuthRuleSettingsTests | |""".stripMargin, assertion = rule => { - rule.settings.jwt.id should be(JwtDef.Name("jwt1")) - rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) - rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Rsa] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) - rule.settings.permittedGroups should be(Groups.NotDefined) + rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) + rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Rsa] + rule.authentication.settings.jwt.userClaim should be(None) + rule.authentication.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) + //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) } ) } @@ -480,12 +479,12 @@ class JwtAuthRuleSettingsTests | |""".stripMargin, assertion = rule => { - rule.settings.jwt.id should be(JwtDef.Name("jwt1")) - rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) - rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Rsa] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) - rule.settings.permittedGroups should be(Groups.NotDefined) + rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) + rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Rsa] + rule.authentication.settings.jwt.userClaim should be(None) + rule.authentication.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) + //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) } ) } @@ -510,12 +509,12 @@ class JwtAuthRuleSettingsTests | |""".stripMargin, assertion = rule => { - rule.settings.jwt.id should be(JwtDef.Name("jwt1")) - rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) - rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Ec] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) - rule.settings.permittedGroups should be(Groups.NotDefined) + rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) + rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Ec] + rule.authentication.settings.jwt.userClaim should be(None) + rule.authentication.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) + //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) } ) } @@ -544,13 +543,13 @@ class JwtAuthRuleSettingsTests |""".stripMargin, httpClientsFactory = mockedHttpClientsFactory, assertion = rule => { - rule.settings.jwt.id should be(JwtDef.Name("jwt1")) - rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) - rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.NoCheck] - rule.settings.jwt.checkMethod.asInstanceOf[SignatureCheckMethod.NoCheck].service shouldBe a[CacheableExternalAuthenticationServiceDecorator] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")),None))) - rule.settings.permittedGroups should be(Groups.NotDefined) + rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) + rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.NoCheck] + rule.authentication.settings.jwt.checkMethod.asInstanceOf[SignatureCheckMethod.NoCheck].service shouldBe a[CacheableExternalAuthenticationServiceDecorator] + rule.authentication.settings.jwt.userClaim should be(None) + rule.authentication.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")),None))) + //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) } ) } @@ -582,13 +581,13 @@ class JwtAuthRuleSettingsTests |""".stripMargin, httpClientsFactory = mockedHttpClientsFactory, assertion = rule => { - rule.settings.jwt.id should be(JwtDef.Name("jwt1")) - rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) - rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.NoCheck] - rule.settings.jwt.checkMethod.asInstanceOf[SignatureCheckMethod.NoCheck].service shouldBe a[CacheableExternalAuthenticationServiceDecorator] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")),None))) - rule.settings.permittedGroups should be(Groups.NotDefined) + rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) + rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.NoCheck] + rule.authentication.settings.jwt.checkMethod.asInstanceOf[SignatureCheckMethod.NoCheck].service shouldBe a[CacheableExternalAuthenticationServiceDecorator] + rule.authentication.settings.jwt.userClaim should be(None) + rule.authentication.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")),None))) + //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) } ) } From 7f08d4aadc8c124fb2d9064633776f45aa2883b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Sun, 9 Nov 2025 14:33:06 +0100 Subject: [PATCH 02/15] qs --- .../blocks/rules/auth/JwtAuthRule.scala | 7 +- .../rules/auth/JwtAuthenticationRule.scala | 10 +- .../rules/auth/JwtAuthorizationRule.scala | 35 ++---- .../auth/JwtPseudoAuthorizationRule.scala | 68 ++++++++++++ .../blocks/rules/auth/base/BaseJwtRule.scala | 34 +----- .../factory/decoders/ruleDecoders.scala | 4 + .../rules/auth/JwtAuthRuleDecoder.scala | 101 +++++++++++++----- .../blocks/rules/auth/JwtAuthRuleTests.scala | 17 ++- .../rules/auth/JwtAuthRuleSettingsTests.scala | 6 +- 9 files changed, 183 insertions(+), 99 deletions(-) create mode 100644 core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtPseudoAuthorizationRule.scala diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthRule.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthRule.scala index 74c1a0ef80..3f392623dd 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthRule.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthRule.scala @@ -23,8 +23,11 @@ import tech.beshu.ror.accesscontrol.blocks.rules.auth.base.BaseComposedAuthentic import tech.beshu.ror.accesscontrol.domain.* final class JwtAuthRule(val authentication: JwtAuthenticationRule, - val authorization: JwtAuthorizationRule) - extends BaseComposedAuthenticationAndAuthorizationRule(authentication, authorization) { + val authorization: JwtAuthorizationRule | JwtPseudoAuthorizationRule) + extends BaseComposedAuthenticationAndAuthorizationRule( + authenticationRule = authentication.withDisabledCallsToExternalAuthenticationService, + authorizationRule = authorization + ) { override val name: Rule.Name = JwtAuthRule.Name.name diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthenticationRule.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthenticationRule.scala index ea8af49f61..9510d6cda7 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthenticationRule.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthenticationRule.scala @@ -31,7 +31,8 @@ import tech.beshu.ror.accesscontrol.utils.ClaimsOps.ClaimSearchResult import tech.beshu.ror.accesscontrol.utils.ClaimsOps.ClaimSearchResult.{Found, NotFound} final class JwtAuthenticationRule(val settings: Settings, - override val userIdCaseSensitivity: CaseSensitivity) + override val userIdCaseSensitivity: CaseSensitivity, + disabledCallsToExternalAuthenticationService: Boolean = false) extends AuthenticationRule with AuthenticationImpersonationCustomSupport with BaseJwtRule { @@ -41,9 +42,9 @@ final class JwtAuthenticationRule(val settings: Settings, override val eligibleUsers: EligibleUsersSupport = EligibleUsersSupport.NotAvailable override protected[rules] def authenticate[B <: BlockContext : BlockContextUpdater](blockContext: B): Task[RuleResult[B]] = { - processUsingJwtToken(blockContext, settings.jwt) { tokenData => + processUsingJwtToken(blockContext, settings.jwt, disabledCallsToExternalAuthenticationService) { tokenData => authenticate(blockContext, tokenData.userId, tokenData.payload) - }.flatMap(finalize(_, settings.jwt)) + } } private def authenticate[B <: BlockContext : BlockContextUpdater](blockContext: B, @@ -56,6 +57,9 @@ final class JwtAuthenticationRule(val settings: Settings, }).map(_.withUserMetadata(_.withJwtToken(payload))) } + def withDisabledCallsToExternalAuthenticationService = + new JwtAuthenticationRule(settings, userIdCaseSensitivity, disabledCallsToExternalAuthenticationService = true) + } object JwtAuthenticationRule { diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthorizationRule.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthorizationRule.scala index 9fdf453701..9d405039a4 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthorizationRule.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthorizationRule.scala @@ -50,34 +50,15 @@ final class JwtAuthorizationRule(val settings: Settings) private def authorize[B <: BlockContext : BlockContextUpdater](blockContext: B, result: Option[ClaimSearchResult[UniqueList[Group]]], groupsLogic: GroupsLogic) = { - (result, groupsLogic) match { - case (None, _) => - Left(()) - case (Some(NotFound), _) => - Left(()) - case (Some(Found(groups)), groupsLogic) => - UniqueNonEmptyList.from(groups) match { - case Some(nonEmptyGroups) => - groupsLogic.availableGroupsFrom(nonEmptyGroups) match { - case Some(matchedGroups) => - checkIfCanContinueWithGroups(blockContext, UniqueList.from(matchedGroups)) - .map(_.withUserMetadata(_.addAvailableGroups(matchedGroups))) - case None => - Left(()) - } - case None => - Left(()) - } - } - } - - private def checkIfCanContinueWithGroups[B <: BlockContext](blockContext: B, - groups: UniqueList[Group]) = { - UniqueNonEmptyList.from(groups.toList.map(_.id)) match { - case Some(nonEmptyGroups) if blockContext.isCurrentGroupEligible(GroupIds(nonEmptyGroups)) => - Right(blockContext) - case Some(_) | None => + result match { + case None | Some(NotFound) => Left(()) + case Some(Found(groups)) => + (for { + nonEmptyGroups <- UniqueNonEmptyList.from(groups) + matchedGroups <- groupsLogic.availableGroupsFrom(nonEmptyGroups) + if blockContext.isCurrentGroupEligible(GroupIds.from(matchedGroups)) + } yield blockContext.withUserMetadata(_.addAvailableGroups(matchedGroups))).toRight(()) } } diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtPseudoAuthorizationRule.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtPseudoAuthorizationRule.scala new file mode 100644 index 0000000000..a2eeb98af3 --- /dev/null +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtPseudoAuthorizationRule.scala @@ -0,0 +1,68 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.accesscontrol.blocks.rules.auth + +import monix.eval.Task +import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef +import tech.beshu.ror.accesscontrol.blocks.rules.Rule +import tech.beshu.ror.accesscontrol.blocks.rules.Rule.{AuthorizationRule, RuleName, RuleResult} +import tech.beshu.ror.accesscontrol.blocks.rules.auth.JwtPseudoAuthorizationRule.Settings +import tech.beshu.ror.accesscontrol.blocks.rules.auth.base.BaseJwtRule +import tech.beshu.ror.accesscontrol.blocks.rules.auth.base.impersonation.AuthorizationImpersonationCustomSupport +import tech.beshu.ror.accesscontrol.blocks.{BlockContext, BlockContextUpdater} +import tech.beshu.ror.accesscontrol.domain.{Group, GroupIds} +import tech.beshu.ror.accesscontrol.utils.ClaimsOps.ClaimSearchResult +import tech.beshu.ror.accesscontrol.utils.ClaimsOps.ClaimSearchResult.* +import tech.beshu.ror.utils.uniquelist.{UniqueList, UniqueNonEmptyList} + +// Pseudo-authorization rule should be used exclusively as part of the JwtAuthRule, when there are is no groups logic defined. +final class JwtPseudoAuthorizationRule(val settings: Settings) + extends AuthorizationRule + with AuthorizationImpersonationCustomSupport + with BaseJwtRule { + + override val name: Rule.Name = JwtAuthorizationRule.Name.name + + override protected[rules] def authorize[B <: BlockContext : BlockContextUpdater](blockContext: B): Task[RuleResult[B]] = { + processUsingJwtToken(blockContext, settings.jwt) { tokenData => + pseudoAuthorize(blockContext, tokenData.groups) + } + } + + private def pseudoAuthorize[B <: BlockContext](blockContext: B, + result: Option[ClaimSearchResult[UniqueList[Group]]]) = { + result match { + case None | Some(NotFound) => + Right(blockContext) + case Some(Found(groups)) => + (for { + nonEmptyGroups <- UniqueNonEmptyList.from(groups) + if blockContext.isCurrentGroupEligible(GroupIds.from(nonEmptyGroups)) + } yield blockContext).toRight(()) + } + } + +} + +object JwtPseudoAuthorizationRule { + + implicit case object Name extends RuleName[JwtAuthorizationRule] { + override val name = Rule.Name("jwt_authorization") + } + + final case class Settings(jwt: JwtDef) +} diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/base/BaseJwtRule.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/base/BaseJwtRule.scala index 19716a00ab..a502737137 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/base/BaseJwtRule.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/base/BaseJwtRule.scala @@ -24,7 +24,6 @@ import org.apache.logging.log4j.scala.Logging import tech.beshu.ror.accesscontrol.blocks.BlockContext import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef.SignatureCheckMethod.{Ec, Hmac, NoCheck, Rsa} -import tech.beshu.ror.accesscontrol.blocks.rules.Rule import tech.beshu.ror.accesscontrol.blocks.rules.Rule.RuleResult import tech.beshu.ror.accesscontrol.blocks.rules.Rule.RuleResult.{Fulfilled, Rejected} import tech.beshu.ror.accesscontrol.blocks.rules.auth.base.BaseJwtRule.* @@ -38,12 +37,11 @@ import tech.beshu.ror.utils.uniquelist.UniqueList import scala.util.Try -trait JwtRule extends Rule - trait BaseJwtRule extends Logging { protected def processUsingJwtToken[B <: BlockContext](blockContext: B, - jwt: JwtDef) + jwt: JwtDef, + disabledCallsToExternalAuthenticationService: Boolean = false) (operation: JwtData => Either[Unit, B]): Task[RuleResult[B]] = { implicit val jwtImpl: JwtDef = jwt jwtTokenFrom(blockContext.requestContext) match { @@ -65,11 +63,11 @@ trait BaseJwtRule extends Logging { Task.now(Rejected()) case Right(modifiedBlockContext) => jwt.checkMethod match { - case NoCheck(service) => + case NoCheck(service) if !disabledCallsToExternalAuthenticationService => service .authenticate(Credentials(User.Id(nes("jwt")), PlainTextSecret(token.value))) .map(RuleResult.resultBasedOnCondition(modifiedBlockContext)(_)) - case Hmac(_) | Rsa(_) | Ec(_) => + case _ => Task.now(Fulfilled(modifiedBlockContext)) } } @@ -77,30 +75,6 @@ trait BaseJwtRule extends Logging { } } - protected def finalize[B <: BlockContext](result: RuleResult[B], - jwt: JwtDef): Task[RuleResult[B]] = { - implicit val jwtImpl: JwtDef = jwt - result match { - case rejected: Rejected[B] => - Task.now(rejected) - case Fulfilled(blockContext) => - jwtTokenFrom(blockContext.requestContext) match { - case None => - Task.now(Rejected()) - case Some(token) => - implicit val requestId: RequestId = blockContext.requestContext.id.toRequestId - jwt.checkMethod match { - case NoCheck(service) => - service - .authenticate(Credentials(User.Id(nes("jwt")), PlainTextSecret(token.value))) - .map(RuleResult.resultBasedOnCondition(blockContext)(_)) - case Hmac(_) | Rsa(_) | Ec(_) => - Task.now(Fulfilled(blockContext)) - } - } - } - } - private def jwtTokenFrom(requestContext: RequestContext)(implicit jwt: JwtDef) = { requestContext .authorizationToken(jwt.authorizationTokenDef) diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/ruleDecoders.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/ruleDecoders.scala index 2fe4cdf6c3..0f8e80ab61 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/ruleDecoders.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/ruleDecoders.scala @@ -133,6 +133,10 @@ object ruleDecoders { Some(new ExternalAuthorizationRuleDecoder(authorizationServiceDefinitions, impersonatorsDefinitions, mocksProvider, globalSettings)) case JwtAuthRule.Name.name => Some(new JwtAuthRuleDecoder(jwtDefinitions, globalSettings)) + case JwtAuthenticationRule.Name.name => + Some(new JwtAuthenticationRuleDecoder(jwtDefinitions, globalSettings)) + case JwtAuthorizationRule.Name.name => + Some(new JwtAuthorizationRuleDecoder(jwtDefinitions)) case LdapAuthorizationRule.Name.name => Some(new LdapAuthorizationRuleDecoder(ldapServiceDefinitions, impersonatorsDefinitions, mocksProvider, globalSettings)) case LdapAuthRule.Name.name => diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtAuthRuleDecoder.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtAuthRuleDecoder.scala index cd23f7a599..28404d95dd 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtAuthRuleDecoder.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtAuthRuleDecoder.scala @@ -17,15 +17,12 @@ package tech.beshu.ror.accesscontrol.factory.decoders.rules.auth import io.circe.Decoder -import monix.eval.Task import org.apache.logging.log4j.scala.Logging import tech.beshu.ror.accesscontrol.blocks.Block.RuleDefinition import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef import tech.beshu.ror.accesscontrol.blocks.rules.Rule -import tech.beshu.ror.accesscontrol.blocks.rules.Rule.{AuthorizationRule, RuleName} -import tech.beshu.ror.accesscontrol.blocks.rules.auth.base.impersonation.AuthorizationImpersonationCustomSupport -import tech.beshu.ror.accesscontrol.blocks.rules.auth.{JwtAuthRule, JwtAuthenticationRule, JwtAuthorizationRule} -import tech.beshu.ror.accesscontrol.blocks.{BlockContext, BlockContextUpdater} +import tech.beshu.ror.accesscontrol.blocks.rules.Rule.RuleName +import tech.beshu.ror.accesscontrol.blocks.rules.auth.{JwtAuthRule, JwtAuthenticationRule, JwtAuthorizationRule, JwtPseudoAuthorizationRule} import tech.beshu.ror.accesscontrol.domain.GroupsLogic import tech.beshu.ror.accesscontrol.factory.GlobalSettings import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.Reason.Message @@ -33,36 +30,90 @@ import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCre import tech.beshu.ror.accesscontrol.factory.decoders.definitions.Definitions import tech.beshu.ror.accesscontrol.factory.decoders.definitions.JwtDefinitionsDecoder.* import tech.beshu.ror.accesscontrol.factory.decoders.rules.RuleBaseDecoder.RuleBaseDecoderWithoutAssociatedFields -import tech.beshu.ror.accesscontrol.factory.decoders.rules.auth.JwtAuthRuleDecoder.cannotFindJwtDefinition +import tech.beshu.ror.accesscontrol.factory.decoders.rules.auth.JwtAuthRuleHelper.* import tech.beshu.ror.accesscontrol.factory.decoders.rules.auth.groups.GroupsLogicDecoder import tech.beshu.ror.accesscontrol.factory.decoders.rules.auth.groups.GroupsLogicRepresentationDecoder.GroupsLogicDecodingResult import tech.beshu.ror.accesscontrol.utils.CirceOps.* import tech.beshu.ror.implicits.* -private implicit val ruleName: RuleName[Rule] = new RuleName[Rule] { - override def name: Rule.Name = JwtAuthRule.Name.name +class JwtAuthenticationRuleDecoder(jwtDefinitions: Definitions[JwtDef], + globalSettings: GlobalSettings) + extends RuleBaseDecoderWithoutAssociatedFields[JwtAuthenticationRule] with Logging { + + override protected def decoder: Decoder[RuleDefinition[JwtAuthenticationRule]] = { + nameAndGroupsSimpleDecoder + .or(nameAndGroupsExtendedDecoder[JwtAuthenticationRule]) + .toSyncDecoder + .emapE { case (name, groupsLogicOpt) => + val foundJwtDef = jwtDefinitions.items.find(_.id === name) + (foundJwtDef, groupsLogicOpt) match { + case (Some(_), Some(_)) => + Left(RulesLevelCreationError(Message(s"Cannot create ${JwtAuthenticationRule.Name.name.show}, because there are superfluous groups settings. Remove the groups settings, or use ${JwtAuthorizationRule.Name.name.show} or ${JwtAuthRule.Name.name.show} rule, if group settings are required."))) + case (Some(jwtDef), None) => + val settings = JwtAuthenticationRule.Settings(jwtDef) + val rule = new JwtAuthenticationRule(settings, globalSettings.userIdCaseSensitivity) + Right(RuleDefinition.create(rule)) + case (None, _) => + Left(cannotFindJwtDefinition(name)) + } + } + .decoder + } +} + +class JwtAuthorizationRuleDecoder(jwtDefinitions: Definitions[JwtDef]) + extends RuleBaseDecoderWithoutAssociatedFields[JwtAuthorizationRule] with Logging { + + override protected def decoder: Decoder[RuleDefinition[JwtAuthorizationRule]] = { + nameAndGroupsSimpleDecoder + .or(nameAndGroupsExtendedDecoder[JwtAuthorizationRule]) + .toSyncDecoder + .emapE { case (name, groupsLogicOpt) => + val foundJwtDef = jwtDefinitions.items.find(_.id === name) + (foundJwtDef, groupsLogicOpt) match { + case (Some(jwtDef), Some(groupsLogic)) => + val settings = JwtAuthorizationRule.Settings(jwtDef, groupsLogic) + val rule = new JwtAuthorizationRule(settings) + Right(RuleDefinition.create[JwtAuthorizationRule](rule)) + case (Some(_), None) => + Left(RulesLevelCreationError(Message(s"Cannot create ${JwtAuthorizationRule.Name.name.show} - missing groups logic (https://github.com/beshu-tech/readonlyrest-docs/blob/master/details/authorization-rules-details.md#checking-groups-logic)"))) + case (None, _) => + Left(cannotFindJwtDefinition(name)) + } + } + .decoder + } } class JwtAuthRuleDecoder(jwtDefinitions: Definitions[JwtDef], globalSettings: GlobalSettings) - extends RuleBaseDecoderWithoutAssociatedFields[JwtAuthRule | JwtAuthenticationRule] with Logging { + extends RuleBaseDecoderWithoutAssociatedFields[JwtAuthRule] with Logging { - override protected def decoder: Decoder[RuleDefinition[JwtAuthRule | JwtAuthenticationRule]] = { - JwtAuthRuleDecoder.nameAndGroupsSimpleDecoder - .or(JwtAuthRuleDecoder.nameAndGroupsExtendedDecoder) + override protected def decoder: Decoder[RuleDefinition[JwtAuthRule]] = { + nameAndGroupsSimpleDecoder + .or(nameAndGroupsExtendedDecoder[JwtAuthRule]) .toSyncDecoder - .emapE[RuleDefinition[JwtAuthRule | JwtAuthenticationRule]] { case (name, groupsLogicOpt) => - val foundKbnDef = jwtDefinitions.items.find(_.id === name) - (foundKbnDef, groupsLogicOpt) match { - case (Some(jwtDef), Some(groupsLogic)) => + .emapE { case (name, groupsLogicOpt) => + val foundJwtDef = jwtDefinitions.items.find(_.id === name) + foundJwtDef match { + case Some(jwtDef) => val authentication = new JwtAuthenticationRule(JwtAuthenticationRule.Settings(jwtDef), globalSettings.userIdCaseSensitivity) - val authorization = new JwtAuthorizationRule(JwtAuthorizationRule.Settings(jwtDef, groupsLogic)) + val authorization: JwtAuthorizationRule | JwtPseudoAuthorizationRule = groupsLogicOpt match { + case Some(groupsLogic) => + new JwtAuthorizationRule(JwtAuthorizationRule.Settings(jwtDef, groupsLogic)) + case None => + logger.warn( + s"""Missing groups logic settings in ${JwtAuthRule.Name.name.show} rule. + |For old configs, ROR treats this as `groups_any_of: ["*"]`. + |This syntax is deprecated. Add groups logic (https://github.com/beshu-tech/readonlyrest-docs/blob/master/details/authorization-rules-details.md#checking-groups-logic), + |or use ${JwtAuthRule.Name.name.show} if you only need authentication. + |""".stripMargin + ) + new JwtPseudoAuthorizationRule(JwtPseudoAuthorizationRule.Settings(jwtDef)) + } val rule = new JwtAuthRule(authentication, authorization) Right(RuleDefinition.create(rule)) - case (Some(jwtDef), None) => - val rule = new JwtAuthenticationRule(JwtAuthenticationRule.Settings(jwtDef), globalSettings.userIdCaseSensitivity) - Right(RuleDefinition.create(rule): RuleDefinition[JwtAuthenticationRule]) - case (None, _) => + case None => Left(cannotFindJwtDefinition(name)) } } @@ -70,23 +121,23 @@ class JwtAuthRuleDecoder(jwtDefinitions: Definitions[JwtDef], } } -private object JwtAuthRuleDecoder { +private object JwtAuthRuleHelper { def cannotFindJwtDefinition(name: JwtDef.Name) = RulesLevelCreationError(Message(s"Cannot find JWT definition with name: ${name.show}")) - private val nameAndGroupsSimpleDecoder: Decoder[(JwtDef.Name, Option[GroupsLogic])] = + val nameAndGroupsSimpleDecoder: Decoder[(JwtDef.Name, Option[GroupsLogic])] = DecoderHelpers .decodeStringLikeNonEmpty .map(JwtDef.Name.apply) .map((_, None)) - private val nameAndGroupsExtendedDecoder: Decoder[(JwtDef.Name, Option[GroupsLogic])] = + def nameAndGroupsExtendedDecoder[T <: Rule](implicit ruleName: RuleName[T]): Decoder[(JwtDef.Name, Option[GroupsLogic])] = Decoder .instance { c => for { jwtDefName <- c.downField("name").as[JwtDef.Name] - groupsLogicDecodingResult <- GroupsLogicDecoder.decoder[JwtAuthRule].apply(c) + groupsLogicDecodingResult <- GroupsLogicDecoder.decoder[T].apply(c) } yield (jwtDefName, groupsLogicDecodingResult) } .toSyncDecoder diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthRuleTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthRuleTests.scala index 7ee223f444..40d4c7d6ea 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthRuleTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthRuleTests.scala @@ -33,7 +33,7 @@ import tech.beshu.ror.accesscontrol.blocks.definitions.ExternalAuthenticationSer import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef.{GroupsConfig, SignatureCheckMethod} import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata import tech.beshu.ror.accesscontrol.blocks.rules.Rule.RuleResult.{Fulfilled, Rejected} -import tech.beshu.ror.accesscontrol.blocks.rules.auth.{JwtAuthRule, JwtAuthenticationRule, JwtAuthorizationRule} +import tech.beshu.ror.accesscontrol.blocks.rules.auth.{JwtAuthRule, JwtAuthenticationRule, JwtAuthorizationRule, JwtPseudoAuthorizationRule} import tech.beshu.ror.accesscontrol.domain import tech.beshu.ror.accesscontrol.domain.GroupIdLike.GroupId import tech.beshu.ror.accesscontrol.domain.LoggedUser.DirectlyLoggedUser @@ -741,15 +741,14 @@ class JwtAuthRuleTests tokenHeader: Header, preferredGroup: Option[GroupId], blockContextAssertion: Option[BlockContext => Unit]) = { - val rule = configuredGroups match { - case Some(groupsLogic) => - new JwtAuthRule( - new JwtAuthenticationRule(JwtAuthenticationRule.Settings(configuredJwtDef), CaseSensitivity.Enabled), - new JwtAuthorizationRule(JwtAuthorizationRule.Settings(configuredJwtDef, groupsLogic)), - ) - case None => - new JwtAuthenticationRule(JwtAuthenticationRule.Settings(configuredJwtDef), CaseSensitivity.Enabled) + val authorization: JwtAuthorizationRule | JwtPseudoAuthorizationRule = configuredGroups match { + case Some(groupsLogic) => new JwtAuthorizationRule(JwtAuthorizationRule.Settings(configuredJwtDef, groupsLogic)) + case None => new JwtPseudoAuthorizationRule(JwtPseudoAuthorizationRule.Settings(configuredJwtDef)) } + val rule = new JwtAuthRule( + new JwtAuthenticationRule(JwtAuthenticationRule.Settings(configuredJwtDef), CaseSensitivity.Enabled), + authorization, + ) val requestContext = MockRequestContext.indices.withHeaders( preferredGroup.map(_.toCurrentGroupHeader).toSeq :+ tokenHeader diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthRuleSettingsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthRuleSettingsTests.scala index c065ae06d0..f764576027 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthRuleSettingsTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthRuleSettingsTests.scala @@ -19,7 +19,7 @@ import org.scalamock.scalatest.MockFactory import org.scalatest.matchers.should.Matchers.* import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef.{GroupsConfig, SignatureCheckMethod} import tech.beshu.ror.accesscontrol.blocks.definitions.{CacheableExternalAuthenticationServiceDecorator, JwtDef} -import tech.beshu.ror.accesscontrol.blocks.rules.auth.JwtAuthRule +import tech.beshu.ror.accesscontrol.blocks.rules.auth.{JwtAuthRule, JwtAuthorizationRule} import tech.beshu.ror.accesscontrol.domain import tech.beshu.ror.accesscontrol.domain.GroupIdLike.GroupId import tech.beshu.ror.accesscontrol.domain.* @@ -124,7 +124,7 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] rule.authentication.settings.jwt.userClaim should be(None) rule.authentication.settings.jwt.groupsConfig should be(None) - rule.authorization.settings.groupsLogic should be(GroupsLogic.AnyOf(GroupIds( + rule.authorization.asInstanceOf[JwtAuthorizationRule].settings.groupsLogic should be(GroupsLogic.AnyOf(GroupIds( UniqueNonEmptyList.of(GroupIdLike.from("group1*"), GroupId("group2")) ))) } @@ -157,7 +157,7 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] rule.authentication.settings.jwt.userClaim should be(None) rule.authentication.settings.jwt.groupsConfig should be(None) - rule.authorization.settings.groupsLogic should be(GroupsLogic.AllOf(GroupIds( + rule.authorization.asInstanceOf[JwtAuthorizationRule].settings.groupsLogic should be(GroupsLogic.AllOf(GroupIds( UniqueNonEmptyList.of(GroupIdLike.from("group1*"), GroupId("group2")) ))) } From beebddd16646d907be0cb8104226db2b383c1f20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Wed, 12 Nov 2025 19:05:08 +0100 Subject: [PATCH 03/15] qs --- .../auth/JwtPseudoAuthorizationRule.scala | 1 + .../rules/auth/JwtAuthRuleSettingsTests.scala | 34 +- .../JwtAuthenticationRuleSettingsTests.scala | 593 ++++++++++++++++++ .../JwtAuthorizationRuleSettingsTests.scala | 253 ++++++++ 4 files changed, 864 insertions(+), 17 deletions(-) create mode 100644 core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthenticationRuleSettingsTests.scala create mode 100644 core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthorizationRuleSettingsTests.scala diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtPseudoAuthorizationRule.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtPseudoAuthorizationRule.scala index a2eeb98af3..39f30890d5 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtPseudoAuthorizationRule.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtPseudoAuthorizationRule.scala @@ -30,6 +30,7 @@ import tech.beshu.ror.accesscontrol.utils.ClaimsOps.ClaimSearchResult.* import tech.beshu.ror.utils.uniquelist.{UniqueList, UniqueNonEmptyList} // Pseudo-authorization rule should be used exclusively as part of the JwtAuthRule, when there are is no groups logic defined. +// It preserves the kbn_auth rule behavior from before introducing separate authn and authz rules. final class JwtPseudoAuthorizationRule(val settings: Settings) extends AuthorizationRule with AuthorizationImpersonationCustomSupport diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthRuleSettingsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthRuleSettingsTests.scala index f764576027..fb65e36bfe 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthRuleSettingsTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthRuleSettingsTests.scala @@ -19,7 +19,7 @@ import org.scalamock.scalatest.MockFactory import org.scalatest.matchers.should.Matchers.* import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef.{GroupsConfig, SignatureCheckMethod} import tech.beshu.ror.accesscontrol.blocks.definitions.{CacheableExternalAuthenticationServiceDecorator, JwtDef} -import tech.beshu.ror.accesscontrol.blocks.rules.auth.{JwtAuthRule, JwtAuthorizationRule} +import tech.beshu.ror.accesscontrol.blocks.rules.auth.{JwtAuthRule, JwtAuthorizationRule, JwtPseudoAuthorizationRule} import tech.beshu.ror.accesscontrol.domain import tech.beshu.ror.accesscontrol.domain.GroupIdLike.GroupId import tech.beshu.ror.accesscontrol.domain.* @@ -66,7 +66,7 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] rule.authentication.settings.jwt.userClaim should be(None) rule.authentication.settings.jwt.groupsConfig should be(None) - //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) + rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] } ) } @@ -94,7 +94,7 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] rule.authentication.settings.jwt.userClaim should be(None) rule.authentication.settings.jwt.groupsConfig should be(None) - //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) + rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] } ) } @@ -189,7 +189,7 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] rule.authentication.settings.jwt.userClaim should be(None) rule.authentication.settings.jwt.groupsConfig should be(None) - //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) + rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] } ) } @@ -218,7 +218,7 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] rule.authentication.settings.jwt.userClaim should be(None) rule.authentication.settings.jwt.groupsConfig should be(None) - //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) + rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] } ) } @@ -246,7 +246,7 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] rule.authentication.settings.jwt.userClaim should be(None) rule.authentication.settings.jwt.groupsConfig should be(None) - //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) + rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] } ) } @@ -275,7 +275,7 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] rule.authentication.settings.jwt.userClaim should be(None) rule.authentication.settings.jwt.groupsConfig should be(None) - //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) + rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] } ) } @@ -303,7 +303,7 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] rule.authentication.settings.jwt.userClaim should be(Some(domain.Jwt.ClaimName(jsonPathFrom("user")))) rule.authentication.settings.jwt.groupsConfig should be(None) - //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) + rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] } ) } @@ -333,7 +333,7 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.checkMethod shouldBe a[SignatureCheckMethod.Hmac] rule.authentication.settings.jwt.userClaim should be(None) rule.authentication.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) - //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) + rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] } ) } @@ -366,7 +366,7 @@ class JwtAuthRuleSettingsTests idsClaim = domain.Jwt.ClaimName(jsonPathFrom("groups")), namesClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("group_names"))) ))) - //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) + rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] } ) } @@ -394,7 +394,7 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] rule.authentication.settings.jwt.userClaim should be(None) rule.authentication.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("https://{domain}/claims/roles")), None))) - //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) + rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] } ) } @@ -424,7 +424,7 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Rsa] rule.authentication.settings.jwt.userClaim should be(None) rule.authentication.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) - //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) + rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] } ) } @@ -453,7 +453,7 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Rsa] rule.authentication.settings.jwt.userClaim should be(None) rule.authentication.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) - //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) + rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] } ) } @@ -484,7 +484,7 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Rsa] rule.authentication.settings.jwt.userClaim should be(None) rule.authentication.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) - //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) + rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] } ) } @@ -514,7 +514,7 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Ec] rule.authentication.settings.jwt.userClaim should be(None) rule.authentication.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) - //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) + rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] } ) } @@ -549,7 +549,7 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.checkMethod.asInstanceOf[SignatureCheckMethod.NoCheck].service shouldBe a[CacheableExternalAuthenticationServiceDecorator] rule.authentication.settings.jwt.userClaim should be(None) rule.authentication.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")),None))) - //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) + rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] } ) } @@ -587,7 +587,7 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.checkMethod.asInstanceOf[SignatureCheckMethod.NoCheck].service shouldBe a[CacheableExternalAuthenticationServiceDecorator] rule.authentication.settings.jwt.userClaim should be(None) rule.authentication.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")),None))) - //rule.authorization.settings.groupsLogic should be(Groups.NotDefined) + rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] } ) } diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthenticationRuleSettingsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthenticationRuleSettingsTests.scala new file mode 100644 index 0000000000..b3a1b04b9b --- /dev/null +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthenticationRuleSettingsTests.scala @@ -0,0 +1,593 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.unit.acl.factory.decoders.rules.auth + +import org.scalamock.scalatest.MockFactory +import org.scalatest.matchers.should.Matchers.* +import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef.{GroupsConfig, SignatureCheckMethod} +import tech.beshu.ror.accesscontrol.blocks.definitions.{CacheableExternalAuthenticationServiceDecorator, JwtDef} +import tech.beshu.ror.accesscontrol.blocks.rules.auth.JwtAuthenticationRule +import tech.beshu.ror.accesscontrol.domain +import tech.beshu.ror.accesscontrol.domain.* +import tech.beshu.ror.accesscontrol.factory.HttpClientsFactory +import tech.beshu.ror.accesscontrol.factory.HttpClientsFactory.HttpClient +import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.Reason.{MalformedValue, Message} +import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.RulesLevelCreationError +import tech.beshu.ror.mocks.MockHttpClientsFactoryWithFixedHttpClient +import tech.beshu.ror.providers.EnvVarProvider.EnvVarName +import tech.beshu.ror.providers.EnvVarsProvider +import tech.beshu.ror.unit.acl.factory.decoders.rules.BaseRuleSettingsDecoderTest +import tech.beshu.ror.utils.TestsUtils.* + +import java.security.KeyPairGenerator +import java.util.Base64 + +class JwtAuthenticationRuleSettingsTests + extends BaseRuleSettingsDecoderTest[JwtAuthenticationRule] + with MockFactory { + + "A JwtAuthenticationRule" should { + "be able to be loaded from config" when { + "rule is defined using simplified version and minimal required set of fields in JWT definition" in { + assertDecodingSuccess( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authentication: jwt1 + | + | jwt: + | + | - name: jwt1 + | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" + | + |""".stripMargin, + assertion = rule => { + rule.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) + rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] + rule.settings.jwt.userClaim should be(None) + rule.settings.jwt.groupsConfig should be(None) + } + ) + } + "rule is defined using extended version and minimal request set of fields in JWT definition" in { + assertDecodingSuccess( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authentication: + | name: "jwt1" + | + | jwt: + | + | - name: jwt1 + | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" + | + |""".stripMargin, + assertion = rule => { + rule.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) + rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] + rule.settings.jwt.userClaim should be(None) + rule.settings.jwt.groupsConfig should be(None) + } + ) + } + "token header name can be changes in JWT definition" in { + assertDecodingSuccess( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authentication: jwt1 + | + | jwt: + | + | - name: jwt1 + | header_name: X-JWT-Custom-Header + | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" + | + |""".stripMargin, + assertion = rule => { + rule.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(headerNameFrom("X-JWT-Custom-Header"), "Bearer ")) + rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] + rule.settings.jwt.userClaim should be(None) + rule.settings.jwt.groupsConfig should be(None) + } + ) + } + "token prefix can be changes in JWT definition for custom token header" in { + assertDecodingSuccess( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authentication: jwt1 + | + | jwt: + | + | - name: jwt1 + | header_name: X-JWT-Custom-Header + | header_prefix: "MyPrefix " + | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" + | + |""".stripMargin, + assertion = rule => { + rule.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(headerNameFrom("X-JWT-Custom-Header"), "MyPrefix ")) + rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] + rule.settings.jwt.userClaim should be(None) + rule.settings.jwt.groupsConfig should be(None) + } + ) + } + "token prefix can be changes in JWT definition for standard token header" in { + assertDecodingSuccess( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authentication: jwt1 + | + | jwt: + | + | - name: jwt1 + | header_prefix: "MyPrefix " + | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" + | + |""".stripMargin, + assertion = rule => { + rule.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "MyPrefix ")) + rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] + rule.settings.jwt.userClaim should be(None) + rule.settings.jwt.groupsConfig should be(None) + } + ) + } + "custom prefix attribute is empty" in { + assertDecodingSuccess( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authentication: jwt1 + | + | jwt: + | + | - name: jwt1 + | header_name: X-JWT-Custom-Header + | header_prefix: "" + | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" + | + |""".stripMargin, + assertion = rule => { + rule.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(headerNameFrom("X-JWT-Custom-Header"), "")) + rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] + rule.settings.jwt.userClaim should be(None) + rule.settings.jwt.groupsConfig should be(None) + } + ) + } + "user claim can be enabled in JWT definition" in { + assertDecodingSuccess( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authentication: jwt1 + | + | jwt: + | + | - name: jwt1 + | user_claim: user + | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" + | + |""".stripMargin, + assertion = rule => { + rule.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) + rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] + rule.settings.jwt.userClaim should be(Some(domain.Jwt.ClaimName(jsonPathFrom("user")))) + rule.settings.jwt.groupsConfig should be(None) + } + ) + } + "group IDs claim can be enabled in JWT definition" in { + val claimKeys = List("roles_claim", "groups_claim", "group_ids_claim") + claimKeys.foreach { claimKey => + assertDecodingSuccess( + yaml = + s""" + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authentication: jwt1 + | + | jwt: + | + | - name: jwt1 + | $claimKey: groups + | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" + | + |""".stripMargin, + assertion = rule => { + rule.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) + rule.settings.jwt.checkMethod shouldBe a[SignatureCheckMethod.Hmac] + rule.settings.jwt.userClaim should be(None) + rule.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) + } + ) + } + } + "group names claim can be enabled in JWT definition" in { + assertDecodingSuccess( + yaml = + s""" + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authentication: jwt1 + | + | jwt: + | + | - name: jwt1 + | group_ids_claim: groups + | group_names_claim: group_names + | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" + | + |""".stripMargin, + assertion = rule => { + rule.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) + rule.settings.jwt.checkMethod shouldBe a[SignatureCheckMethod.Hmac] + rule.settings.jwt.userClaim should be(None) + rule.settings.jwt.groupsConfig should be(Some(GroupsConfig( + idsClaim = domain.Jwt.ClaimName(jsonPathFrom("groups")), + namesClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("group_names"))) + ))) + } + ) + } + "groups claim can be enabled in JWT definition and is a http address" in { + assertDecodingSuccess( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authentication: jwt1 + | + | jwt: + | + | - name: jwt1 + | group_ids_claim: "https://{domain}/claims/roles" + | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" + | + |""".stripMargin, + assertion = rule => { + rule.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) + rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] + rule.settings.jwt.userClaim should be(None) + rule.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("https://{domain}/claims/roles")), None))) + } + ) + } + "RSA family algorithm can be used in JWT signature" in { + val pkey = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPublic + assertDecodingSuccess( + yaml = + s""" + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authentication: jwt1 + | + | jwt: + | + | - name: jwt1 + | group_ids_claim: groups + | signature_algo: "RSA" + | signature_key: "${Base64.getEncoder.encodeToString(pkey.getEncoded)}" + | + |""".stripMargin, + assertion = rule => { + rule.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) + rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Rsa] + rule.settings.jwt.userClaim should be(None) + rule.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) + } + ) + } + "RSA family algorithm can be used in JWT signature and key is being read from system env in old format" in { + assertDecodingSuccess( + yaml = + s""" + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authentication: jwt1 + | + | jwt: + | + | - name: jwt1 + | group_ids_claim: groups + | signature_algo: "RSA" + | signature_key: "env:SECRET_RSA" + | + |""".stripMargin, + assertion = rule => { + rule.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) + rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Rsa] + rule.settings.jwt.userClaim should be(None) + rule.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) + } + ) + } + "RSA family algorithm can be used in JWT signature and key is being read from system env in new format" in { + val pkey = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPublic + System.setProperty("SECRET_KEY", Base64.getEncoder.encodeToString(pkey.getEncoded)) + assertDecodingSuccess( + yaml = + s""" + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authentication: jwt1 + | + | jwt: + | + | - name: jwt1 + | group_ids_claim: groups + | signature_algo: "RSA" + | signature_key: "@{env:SECRET_RSA}" + | + |""".stripMargin, + assertion = rule => { + rule.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) + rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Rsa] + rule.settings.jwt.userClaim should be(None) + rule.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) + } + ) + } + "EC family algorithm can be used in JWT signature" in { + val pkey = KeyPairGenerator.getInstance("EC").generateKeyPair().getPublic + assertDecodingSuccess( + yaml = + s""" + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authentication: jwt1 + | + | jwt: + | + | - name: jwt1 + | group_ids_claim: groups + | signature_algo: "EC" + | signature_key: "text: ${Base64.getEncoder.encodeToString(pkey.getEncoded)}" + | + |""".stripMargin, + assertion = rule => { + rule.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) + rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Ec] + rule.settings.jwt.userClaim should be(None) + rule.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) + } + ) + } + "None signature check can be used in JWT definition" in { + assertDecodingSuccess( + yaml = + s""" + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authentication: jwt1 + | + | jwt: + | + | - name: jwt1 + | group_ids_claim: groups + | signature_algo: "NONE" + | external_validator: + | url: "http://192.168.0.1:8080/jwt" + | success_status_code: 204 + | cache_ttl_in_sec: 60 + | validate: false + | + |""".stripMargin, + httpClientsFactory = mockedHttpClientsFactory, + assertion = rule => { + rule.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) + rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.NoCheck] + rule.settings.jwt.checkMethod.asInstanceOf[SignatureCheckMethod.NoCheck].service shouldBe a[CacheableExternalAuthenticationServiceDecorator] + rule.settings.jwt.userClaim should be(None) + rule.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")),None))) + } + ) + } + "None signature check can be used in JWT definition with custom http client settings for external validator" in { + assertDecodingSuccess( + yaml = + s""" + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authentication: jwt1 + | + | jwt: + | + | - name: jwt1 + | group_ids_claim: groups + | signature_algo: "NONE" + | external_validator: + | url: "http://192.168.0.1:8080/jwt" + | success_status_code: 204 + | cache_ttl_in_sec: 60 + | http_connection_settings: + | connection_timeout_in_sec: 1 + | connection_request_timeout_in_sec: 10 + | connection_pool_size: 30 + | validate: true + |""".stripMargin, + httpClientsFactory = mockedHttpClientsFactory, + assertion = rule => { + rule.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) + rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.NoCheck] + rule.settings.jwt.checkMethod.asInstanceOf[SignatureCheckMethod.NoCheck].service shouldBe a[CacheableExternalAuthenticationServiceDecorator] + rule.settings.jwt.userClaim should be(None) + rule.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")),None))) + } + ) + } + } + "not be able to be loaded from config" when { + "no JWT definition name is defined in rule setting" in { + assertDecodingFailure( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authentication: + | + | jwt: + | + | - name: jwt1 + | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" + | + |""".stripMargin, + assertion = errors => { + errors should have size 1 + errors.head should be(RulesLevelCreationError(MalformedValue.fromString( + """jwt_authentication: null + |""".stripMargin + ))) + } + ) + } + "JWT definition with given name is not found" in { + assertDecodingFailure( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authentication: jwt2 + | + | jwt: + | + | - name: jwt1 + | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" + | + |""".stripMargin, + assertion = errors => { + errors should have size 1 + errors.head should be(RulesLevelCreationError(Message("Cannot find JWT definition with name: jwt2"))) + } + ) + } + "no JWT definition is defined" in { + assertDecodingFailure( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authentication: jwt1 + |""".stripMargin, + assertion = errors => { + errors should have size 1 + errors.head should be(RulesLevelCreationError(Message("Cannot find JWT definition with name: jwt1"))) + } + ) + } + } + } + + override implicit protected def envVarsProvider: EnvVarsProvider = { + case EnvVarName(env) if env.value == "SECRET_RSA" => + val pkey = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPublic + Some(Base64.getEncoder.encodeToString(pkey.getEncoded)) + case _ => + None + } + + private val mockedHttpClientsFactory: HttpClientsFactory = { + val httpClientMock = mock[HttpClient] + new MockHttpClientsFactoryWithFixedHttpClient(httpClientMock) + } +} diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthorizationRuleSettingsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthorizationRuleSettingsTests.scala new file mode 100644 index 0000000000..16968b903a --- /dev/null +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthorizationRuleSettingsTests.scala @@ -0,0 +1,253 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.unit.acl.factory.decoders.rules.auth + +import org.scalamock.scalatest.MockFactory +import org.scalatest.matchers.should.Matchers.* +import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef +import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef.SignatureCheckMethod +import tech.beshu.ror.accesscontrol.blocks.rules.auth.JwtAuthorizationRule +import tech.beshu.ror.accesscontrol.domain.* +import tech.beshu.ror.accesscontrol.domain.GroupIdLike.GroupId +import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.Reason.{MalformedValue, Message} +import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.RulesLevelCreationError +import tech.beshu.ror.providers.EnvVarProvider.EnvVarName +import tech.beshu.ror.providers.EnvVarsProvider +import tech.beshu.ror.unit.acl.factory.decoders.rules.BaseRuleSettingsDecoderTest +import tech.beshu.ror.utils.TestsUtils.* +import tech.beshu.ror.utils.uniquelist.UniqueNonEmptyList + +import java.security.KeyPairGenerator +import java.util.Base64 + +class JwtAuthorizationRuleSettingsTests + extends BaseRuleSettingsDecoderTest[JwtAuthorizationRule] + with MockFactory { + + "A JwtAuthorizationRule" should { + "be able to be loaded from config" when { + "rule is defined using extended version with groups 'or' logic and minimal request set of fields in JWT definition" in { + val ruleKeys = List("roles", "groups", "groups_or") + ruleKeys.foreach { ruleKey => + assertDecodingSuccess( + yaml = + s""" + |readonlyrest: + | access_control_rules: + | + | - name: test_block1 + | auth_key_sha1: "d27aaf7fa3c1603948bb29b7339f2559dc02019a" + | jwt_authorization: + | name: "jwt1" + | $ruleKey: ["group1*","group2"] + | + | jwt: + | + | - name: jwt1 + | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" + | + |""".stripMargin, + assertion = rule => { + rule.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) + rule.settings.jwt.checkMethod shouldBe a[SignatureCheckMethod.Hmac] + rule.settings.jwt.userClaim should be(None) + rule.settings.jwt.groupsConfig should be(None) + rule.settings.groupsLogic should be(GroupsLogic.AnyOf(GroupIds( + UniqueNonEmptyList.of(GroupIdLike.from("group1*"), GroupId("group2")) + ))) + } + ) + } + } + "rule is defined using extended version with groups 'and' logic and minimal request set of fields in JWT definition" in { + val ruleKeys = List("roles_and", "groups_and") + ruleKeys.foreach { ruleKey => + assertDecodingSuccess( + yaml = + s""" + |readonlyrest: + | access_control_rules: + | + | - name: test_block1 + | auth_key_sha1: "d27aaf7fa3c1603948bb29b7339f2559dc02019a" + | jwt_authorization: + | name: "jwt1" + | $ruleKey: ["group1*","group2"] + | + | jwt: + | + | - name: jwt1 + | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" + | + |""".stripMargin, + assertion = rule => { + rule.settings.jwt.id should be(JwtDef.Name("jwt1")) + rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) + rule.settings.jwt.checkMethod shouldBe a[SignatureCheckMethod.Hmac] + rule.settings.jwt.userClaim should be(None) + rule.settings.jwt.groupsConfig should be(None) + rule.settings.groupsLogic should be(GroupsLogic.AllOf(GroupIds( + UniqueNonEmptyList.of(GroupIdLike.from("group1*"), GroupId("group2")) + ))) + } + ) + } + } + } + "not be able to be loaded from config" when { + "no JWT definition name is defined in rule setting" in { + assertDecodingFailure( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authorization: + | + | jwt: + | + | - name: jwt1 + | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" + | + |""".stripMargin, + assertion = errors => { + errors should have size 1 + errors.head should be(RulesLevelCreationError(MalformedValue.fromString( + """jwt_authorization: null + |""".stripMargin + ))) + } + ) + } + "JWT definition with given name is not found" in { + assertDecodingFailure( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authorization: jwt2 + | + | jwt: + | + | - name: jwt1 + | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" + | + |""".stripMargin, + assertion = errors => { + errors should have size 1 + errors.head should be(RulesLevelCreationError(Message("Cannot find JWT definition with name: jwt2"))) + } + ) + } + "no JWT definition is defined" in { + assertDecodingFailure( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authorization: jwt1 + |""".stripMargin, + assertion = errors => { + errors should have size 1 + errors.head should be(RulesLevelCreationError(Message("Cannot find JWT definition with name: jwt1"))) + } + ) + } + "extended version of rule settings is used, but no JWT definition name attribute is used" in { + assertDecodingFailure( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authorization: + | roles: ["group1","group2"] + | + | jwt: + | + | - name: jwt1 + | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" + | + |""".stripMargin, + assertion = errors => { + errors should have size 1 + errors.head should be(RulesLevelCreationError(MalformedValue.fromString( + """jwt_authorization: + | roles: + | - "group1" + | - "group2" + |""".stripMargin + ))) + } + ) + } + "extended version of rule settings is used, but both 'groups or' key and 'groups and' key used" in { + List( + ("roles", "roles_and"), + ("groups", "groups_and") + ) + .foreach { case (groupsAnyOfKey, groupsAllOfKey) => + assertDecodingFailure( + yaml = + s""" + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authorization: + | name: "jwt1" + | $groupsAnyOfKey: ["group1","group2"] + | $groupsAllOfKey: ["group1","group2"] + | + | jwt: + | + | - name: jwt1 + | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" + | + |""".stripMargin, + assertion = errors => { + errors should have size 1 + errors.head should be(RulesLevelCreationError(Message( + s"Please specify either '$groupsAnyOfKey' or '$groupsAllOfKey' for JWT authorization rule 'jwt1'" + ))) + } + ) + } + } + } + } + + override implicit protected def envVarsProvider: EnvVarsProvider = { + case EnvVarName(env) if env.value == "SECRET_RSA" => + val pkey = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPublic + Some(Base64.getEncoder.encodeToString(pkey.getEncoded)) + case _ => + None + } +} From 4922ba51ce0103e3fd326fe91ad9e277a0650ee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Thu, 13 Nov 2025 22:37:45 +0100 Subject: [PATCH 04/15] tests --- .../auth/JwtAuthenticationRuleTests.scala | 574 ++++++++++++++++++ .../auth/JwtAuthorizationRuleTests.scala | 308 ++++++++++ .../ror/unit/acl/factory/LocalUsersTest.scala | 4 - 3 files changed, 882 insertions(+), 4 deletions(-) create mode 100644 core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthenticationRuleTests.scala create mode 100644 core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthorizationRuleTests.scala diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthenticationRuleTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthenticationRuleTests.scala new file mode 100644 index 0000000000..2be82d9372 --- /dev/null +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthenticationRuleTests.scala @@ -0,0 +1,574 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.unit.acl.blocks.rules.auth + +import cats.data.NonEmptyList +import eu.timepit.refined.api.Refined +import eu.timepit.refined.types.string.NonEmptyString +import io.jsonwebtoken.Jwts +import monix.eval.Task +import monix.execution.Scheduler.Implicits.global +import org.scalamock.scalatest.MockFactory +import org.scalatest.Inside +import org.scalatest.matchers.should.Matchers.* +import org.scalatest.wordspec.AnyWordSpec +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.GeneralIndexRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.definitions.* +import tech.beshu.ror.accesscontrol.blocks.definitions.ExternalAuthenticationService.Name +import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef.{GroupsConfig, SignatureCheckMethod} +import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata +import tech.beshu.ror.accesscontrol.blocks.rules.Rule.RuleResult.{Fulfilled, Rejected} +import tech.beshu.ror.accesscontrol.blocks.rules.auth.JwtAuthenticationRule +import tech.beshu.ror.accesscontrol.domain +import tech.beshu.ror.accesscontrol.domain.GroupIdLike.GroupId +import tech.beshu.ror.accesscontrol.domain.LoggedUser.DirectlyLoggedUser +import tech.beshu.ror.accesscontrol.domain.{Jwt as _, *} +import tech.beshu.ror.mocks.MockRequestContext +import tech.beshu.ror.syntax.* +import tech.beshu.ror.utils.DurationOps.* +import tech.beshu.ror.utils.TestsUtils.* +import tech.beshu.ror.utils.WithDummyRequestIdSupport +import tech.beshu.ror.utils.misc.JwtUtils.* +import tech.beshu.ror.utils.misc.Random + +import java.security.Key +import scala.concurrent.duration.* +import scala.jdk.CollectionConverters.* +import scala.language.postfixOps + +class JwtAuthenticationRuleTests + extends AnyWordSpec with MockFactory with Inside with BlockContextAssertion with WithDummyRequestIdSupport { + + "A JwtAuthenticationRule" should { + "match" when { + "token has valid HS256 signature" in { + val secret: Key = Jwts.SIG.HS256.key().build() + val jwt = Jwt(secret, claims = List.empty) + assertMatchRule( + configuredJwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name.authorization, "Bearer "), + SignatureCheckMethod.Hmac(secret.getEncoded), + userClaim = None, + groupsConfig = None + ), + tokenHeader = bearerHeader(jwt) + ) { + blockContext => + assertBlockContext( + jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())) + )(blockContext) + } + } + "token has valid RS256 signature" in { + val (pub, secret) = Random.generateRsaRandomKeys + val jwt = Jwt(secret, claims = List.empty) + assertMatchRule( + configuredJwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name.authorization, "Bearer "), + SignatureCheckMethod.Rsa(pub), + userClaim = None, + groupsConfig = None + ), + tokenHeader = bearerHeader(jwt) + ) { + blockContext => + assertBlockContext( + jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())) + )(blockContext) + } + } + "token has no signature and external auth service returns true" in { + val jwt = Jwt(claims = List.empty) + assertMatchRule( + configuredJwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name.authorization, "Bearer "), + SignatureCheckMethod.NoCheck(authService(jwt.stringify(), authenticated = true)), + userClaim = None, + groupsConfig = None + ), + tokenHeader = bearerHeader(jwt) + ) { + blockContext => + assertBlockContext( + jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())) + )(blockContext) + } + } + "token has no signature and external auth service state is cached" in { + val validJwt = Jwt(claims = List.empty) + val invalidJwt = Jwt(claims = List("user" := "testuser")) + val authService = cachedAuthService(validJwt.stringify(), invalidJwt.stringify()) + val jwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name.authorization, "Bearer "), + SignatureCheckMethod.NoCheck(authService), + userClaim = None, + groupsConfig = None + ) + + def checkValidToken(): Unit = assertMatchRule( + configuredJwtDef = jwtDef, + tokenHeader = bearerHeader(validJwt) + ) { + blockContext => + assertBlockContext( + jwt = Some(domain.Jwt.Payload(validJwt.defaultClaims())) + )(blockContext) + } + + def checkInvalidToken(): Unit = assertNotMatchRule( + configuredJwtDef = jwtDef, + tokenHeader = bearerHeader(invalidJwt) + ) + + checkValidToken() + checkValidToken() + checkInvalidToken() + checkValidToken() + } + "user claim name is defined and userId is passed in JWT token claim" in { + val key: Key = Jwts.SIG.HS256.key().build() + val jwt = Jwt(key, claims = List( + "userId" := "user1" + )) + assertMatchRule( + configuredJwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name.authorization, "Bearer "), + SignatureCheckMethod.Hmac(key.getEncoded), + userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), + groupsConfig = None, + ), + tokenHeader = bearerHeader(jwt) + ) { + blockContext => + assertBlockContext( + loggedUser = Some(DirectlyLoggedUser(User.Id("user1"))), + jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())) + )(blockContext) + } + } + "group IDs claim name is defined and groups are passed in JWT token claim (no preferred group)" in { + val key: Key = Jwts.SIG.HS256.key().build() + val jwt = Jwt(key, claims = List( + "userId" := "user1", + "groups" := List("group1", "group2") + )) + assertMatchRule( + configuredJwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name.authorization, "Bearer "), + SignatureCheckMethod.Hmac(key.getEncoded), + userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), + groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + ), + tokenHeader = bearerHeader(jwt) + ) { + blockContext => + assertBlockContext( + loggedUser = Some(DirectlyLoggedUser(User.Id("user1"))), + jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())) + )(blockContext) + } + } + "group IDs claim name is defined and groups are passed in JWT token claim (with preferred group)" in { + val key: Key = Jwts.SIG.HS256.key().build() + val jwt = Jwt(key, claims = List( + "userId" := "user1", + "groups" := List("group1", "group2") + )) + assertMatchRule( + configuredJwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name.authorization, "Bearer "), + SignatureCheckMethod.Hmac(key.getEncoded), + userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), + groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + ), + tokenHeader = bearerHeader(jwt), + preferredGroupId = Some(GroupId("group1")) + ) { + blockContext => + assertBlockContext( + loggedUser = Some(DirectlyLoggedUser(User.Id("user1"))), + jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())), + currentGroup = Some(GroupId("group1")) + )(blockContext) + } + } + "group IDs claim name is defined as http address and groups are passed in JWT token claim" in { + val key: Key = Jwts.SIG.HS256.key().build() + val jwt = Jwt(key, claims = List( + "userId" := "user1", + "https://{domain}/claims/roles" := List("group1", "group2") + )) + assertMatchRule( + configuredJwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name.authorization, "Bearer "), + SignatureCheckMethod.Hmac(key.getEncoded), + userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), + groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("https://{domain}/claims/roles")), None)) + ), + tokenHeader = bearerHeader(jwt) + ) { + blockContext => + assertBlockContext( + loggedUser = Some(DirectlyLoggedUser(User.Id("user1"))), + jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())) + )(blockContext) + } + } + "group IDs claim name is defined and no groups field is passed in JWT token claim" in { + val key: Key = Jwts.SIG.HS256.key().build() + val jwt = Jwt(key, claims = List( + "userId" := "user1" + )) + assertMatchRule( + configuredJwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name.authorization, "Bearer "), + SignatureCheckMethod.Hmac(key.getEncoded), + userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), + groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + ), + tokenHeader = bearerHeader(jwt) + ) { + blockContext => + assertBlockContext( + loggedUser = Some(DirectlyLoggedUser(User.Id("user1"))), + jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())), + )(blockContext) + } + } + "group IDs claim path is defined and groups are passed in JWT token claim" in { + val key: Key = Jwts.SIG.HS256.key().build() + val jwt = Jwt(key, claims = List( + "userId" := "user1", + "tech" :-> "beshu" :-> "groups" := List("group1", "group2") + )) + assertMatchRule( + configuredJwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name.authorization, "Bearer "), + SignatureCheckMethod.Hmac(key.getEncoded), + userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), + groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("tech.beshu.groups")), None)) + ), + tokenHeader = bearerHeader(jwt) + ) { + blockContext => + assertBlockContext( + loggedUser = Some(DirectlyLoggedUser(User.Id("user1"))), + jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())) + )(blockContext) + } + } + "group names claim is defined and group names are passed in JWT token claim" in { + val key: Key = Jwts.SIG.HS256.key().build() + val jwt = Jwt(key, claims = List( + "userId" := "user1", + Claim(NonEmptyList.one(ClaimKey("groups")), List( + Map("id" -> "group1", "name" -> "Group 1").asJava, + Map("id" -> "group2", "name" -> "Group 2").asJava + ).asJava) + )) + assertMatchRule( + configuredJwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name.authorization, "Bearer "), + SignatureCheckMethod.Hmac(key.getEncoded), + userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), + groupsConfig = Some(GroupsConfig( + idsClaim = domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.id)].id")), + namesClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.name)].name"))) + )) + ), + tokenHeader = bearerHeader(jwt) + ) { + blockContext => + assertBlockContext( + loggedUser = Some(DirectlyLoggedUser(User.Id("user1"))), + jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())), + )(blockContext) + } + } + "group names claim is defined and group names passed in JWT token claim are malformed" when { + "group names count differs from the group ID count" in { + val key: Key = Jwts.SIG.HS256.key().build() + val jwt = Jwt(key, claims = List( + "userId" := "user1", + Claim(NonEmptyList.one(ClaimKey("groups")), List( + Map("id" -> "group1", "name" -> List("Group 1", "Group A").asJava).asJava, + Map("id" -> "group2", "name" -> List("Group 2", "Group B").asJava).asJava + ).asJava) + )) + assertMatchRule( + configuredJwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name.authorization, "Bearer "), + SignatureCheckMethod.Hmac(key.getEncoded), + userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), + groupsConfig = Some(GroupsConfig( + idsClaim = domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.id)].id")), + namesClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.name)].name"))) + )) + ), + tokenHeader = bearerHeader(jwt) + ) { + blockContext => + assertBlockContext( + loggedUser = Some(DirectlyLoggedUser(User.Id("user1"))), + jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())), + )(blockContext) + } + } + "one group does not have a name" in { + val key: Key = Jwts.SIG.HS256.key().build() + val jwt = Jwt(key, claims = List( + "userId" := "user1", + Claim(NonEmptyList.one(ClaimKey("groups")), List( + Map("id" -> "group1", "name" -> "Group 1").asJava, + Map("id" -> "group2").asJava, + Map("id" -> "group3", "name" -> "Group 3").asJava + ).asJava) + )) + assertMatchRule( + configuredJwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name.authorization, "Bearer "), + SignatureCheckMethod.Hmac(key.getEncoded), + userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), + groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.id)].id")), Some(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.name)].name"))))) + ), + tokenHeader = bearerHeader(jwt) + ) { + blockContext => + assertBlockContext( + loggedUser = Some(DirectlyLoggedUser(User.Id("user1"))), + jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())), + )(blockContext) + } + } + } + "custom authorization header is used" in { + val key: Key = Jwts.SIG.HS256.key().build() + val jwt = Jwt(key, claims = List.empty) + assertMatchRule( + configuredJwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name("x-jwt-custom-header"), "Bearer "), + SignatureCheckMethod.Hmac(key.getEncoded), + userClaim = None, + groupsConfig = None + ), + tokenHeader = bearerHeader("x-jwt-custom-header", jwt) + ) { + blockContext => + assertBlockContext( + jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())) + )(blockContext) + } + } + "custom authorization token prefix is used" in { + val key: Key = Jwts.SIG.HS256.key().build() + val jwt = Jwt(key, claims = List.empty) + assertMatchRule( + configuredJwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name("x-jwt-custom-header"), "MyPrefix "), + SignatureCheckMethod.Hmac(key.getEncoded), + userClaim = None, + groupsConfig = None + ), + tokenHeader = new Header( + Header.Name("x-jwt-custom-header"), + NonEmptyString.unsafeFrom(s"MyPrefix ${jwt.stringify()}") + ) + ) { + blockContext => + assertBlockContext( + jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())) + )(blockContext) + } + } + } + "not match" when { + "token has invalid HS256 signature" in { + val key1: Key = Jwts.SIG.HS256.key().build() + val key2: Key = Jwts.SIG.HS256.key().build() + val jwt2 = Jwt(key2, claims = List.empty) + assertNotMatchRule( + configuredJwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name.authorization, "Bearer "), + SignatureCheckMethod.Hmac(key1.getEncoded), + userClaim = None, + groupsConfig = None + ), + tokenHeader = bearerHeader(jwt2) + ) + } + "token has invalid RS256 signature" in { + val (pub, _) = Random.generateRsaRandomKeys + val (_, secret) = Random.generateRsaRandomKeys + val jwt = Jwt(secret, claims = List.empty) + assertNotMatchRule( + configuredJwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name.authorization, "Bearer "), + SignatureCheckMethod.Rsa(pub), + userClaim = None, + groupsConfig = None + ), + tokenHeader = bearerHeader(jwt) + ) + } + "token has no signature but external auth service returns false" in { + val jwt = Jwt(claims = List.empty) + assertNotMatchRule( + configuredJwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name.authorization, "Bearer "), + SignatureCheckMethod.NoCheck(authService(jwt.stringify(), authenticated = false)), + userClaim = None, + groupsConfig = None + ), + tokenHeader = bearerHeader(jwt) + ) + } + "user claim name is defined but userId isn't passed in JWT token claim" in { + val key: Key = Jwts.SIG.HS256.key().build() + val jwt = Jwt(key, claims = List.empty) + assertNotMatchRule( + configuredJwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name.authorization, "Bearer "), + SignatureCheckMethod.Hmac(key.getEncoded), + userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), + groupsConfig = None + ), + tokenHeader = bearerHeader(jwt) + ) + } + "group IDs claim name is defined but groups aren't passed in JWT token claim" in { + val key: Key = Jwts.SIG.HS256.key().build() + val jwt = Jwt(key, claims = List.empty) + assertNotMatchRule( + configuredJwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name.authorization, "Bearer "), + SignatureCheckMethod.Hmac(key.getEncoded), + userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), + groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + ), + tokenHeader = bearerHeader(jwt) + ) + } + "preferred group is not on the groups list from JWT" in { + val key: Key = Jwts.SIG.HS256.key().build() + val jwt = Jwt(key, claims = List( + "userId" := "user1", + "groups" := List("group1", "group2") + )) + assertMatchRule( + configuredJwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name.authorization, "Bearer "), + SignatureCheckMethod.Hmac(key.getEncoded), + userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), + groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + ), + tokenHeader = bearerHeader(jwt), + preferredGroupId = Some(GroupId("group3")) + ) + } + } + } + + private def assertMatchRule(configuredJwtDef: JwtDef, + tokenHeader: Header, + preferredGroupId: Option[GroupId] = None) + (blockContextAssertion: BlockContext => Unit): Unit = + assertRule(configuredJwtDef, tokenHeader, preferredGroupId, Some(blockContextAssertion)) + + private def assertNotMatchRule(configuredJwtDef: JwtDef, + tokenHeader: Header, + preferredGroupId: Option[GroupId] = None): Unit = + assertRule(configuredJwtDef, tokenHeader, preferredGroupId, blockContextAssertion = None) + + private def assertRule(configuredJwtDef: JwtDef, + tokenHeader: Header, + preferredGroup: Option[GroupId], + blockContextAssertion: Option[BlockContext => Unit]) = { + val rule = new JwtAuthenticationRule(JwtAuthenticationRule.Settings(configuredJwtDef), CaseSensitivity.Enabled) + + val requestContext = MockRequestContext.indices.withHeaders( + preferredGroup.map(_.toCurrentGroupHeader).toSeq :+ tokenHeader + ) + val blockContext = GeneralIndexRequestBlockContext( + requestContext = requestContext, + userMetadata = UserMetadata.from(requestContext), + responseHeaders = Set.empty, + responseTransformations = List.empty, + filteredIndices = Set.empty, + allAllowedIndices = Set.empty + ) + val result = rule.check(blockContext).runSyncUnsafe(1 second) + blockContextAssertion match { + case Some(assertOutputBlockContext) => + inside(result) { case Fulfilled(outBlockContext) => + assertOutputBlockContext(outBlockContext) + } + case None => + result should be(Rejected()) + } + } + + private def authService(rawToken: String, authenticated: Boolean) = { + val service = mock[ExternalAuthenticationService] + (service.authenticate(_: Credentials)(_: RequestId)) + .expects(where { (credentials: Credentials, _) => credentials.secret === PlainTextSecret(NonEmptyString.unsafeFrom(rawToken)) }) + .returning(Task.now(authenticated)) + service + } + + private def cachedAuthService(authenticatedToken: String, unauthenticatedToken: String) = { + val service = mock[ExternalAuthenticationService] + (service.authenticate(_: Credentials)(_: RequestId)) + .expects(where { (credentials: Credentials, _) => credentials.secret === PlainTextSecret(NonEmptyString.unsafeFrom(authenticatedToken)) }) + .returning(Task.now(true)) + .once() + (service.authenticate(_: Credentials)(_: RequestId)) + .expects(where { (credentials: Credentials, _) => credentials.secret === PlainTextSecret(NonEmptyString.unsafeFrom(unauthenticatedToken)) }) + .returning(Task.now(false)) + .once() + (() => service.id) + .expects() + .returning(Name("external_service")) + (() => service.serviceTimeout) + .expects() + .anyNumberOfTimes() + .returning(Refined.unsafeApply(10 seconds)) + val ttl = (1 hour).toRefinedPositiveUnsafe + new CacheableExternalAuthenticationServiceDecorator(service, ttl) + } +} diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthorizationRuleTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthorizationRuleTests.scala new file mode 100644 index 0000000000..859251c1b3 --- /dev/null +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthorizationRuleTests.scala @@ -0,0 +1,308 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.unit.acl.blocks.rules.auth + +import cats.data.NonEmptyList +import io.jsonwebtoken.Jwts +import monix.execution.Scheduler.Implicits.global +import org.scalamock.scalatest.MockFactory +import org.scalatest.Inside +import org.scalatest.matchers.should.Matchers.* +import org.scalatest.wordspec.AnyWordSpec +import tech.beshu.ror.accesscontrol.blocks.BlockContext +import tech.beshu.ror.accesscontrol.blocks.BlockContext.GeneralIndexRequestBlockContext +import tech.beshu.ror.accesscontrol.blocks.definitions.* +import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef.{GroupsConfig, SignatureCheckMethod} +import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata +import tech.beshu.ror.accesscontrol.blocks.rules.Rule.RuleResult.{Fulfilled, Rejected} +import tech.beshu.ror.accesscontrol.blocks.rules.auth.JwtAuthorizationRule +import tech.beshu.ror.accesscontrol.domain +import tech.beshu.ror.accesscontrol.domain.GroupIdLike.GroupId +import tech.beshu.ror.accesscontrol.domain.{Jwt as _, *} +import tech.beshu.ror.mocks.MockRequestContext +import tech.beshu.ror.syntax.* +import tech.beshu.ror.utils.TestsUtils.* +import tech.beshu.ror.utils.WithDummyRequestIdSupport +import tech.beshu.ror.utils.misc.JwtUtils.* +import tech.beshu.ror.utils.uniquelist.{UniqueList, UniqueNonEmptyList} + +import java.security.Key +import scala.concurrent.duration.* +import scala.jdk.CollectionConverters.* +import scala.language.postfixOps + +class JwtAuthorizationRuleTests + extends AnyWordSpec with MockFactory with Inside with BlockContextAssertion with WithDummyRequestIdSupport { + + "A JwtAuthorizationRule" should { + "match" when { + "rule groups with 'or' logic are defined and intersection between those groups and JWT ones is not empty (1)" in { + val key: Key = Jwts.SIG.HS256.key().build() + val jwt = Jwt(key, claims = List( + "userId" := "user1", + Claim(NonEmptyList.one(ClaimKey("groups")), List( + Map("id" -> "group1", "name" -> "Group 1").asJava, + Map("id" -> "group2", "name" -> "Group 2").asJava + ).asJava) + )) + assertMatchRule( + configuredJwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name.authorization, "Bearer "), + SignatureCheckMethod.Hmac(key.getEncoded), + userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), + groupsConfig = Some(GroupsConfig( + idsClaim = domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.id)].id")), + namesClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.name)].name"))) + )) + ), + configuredGroups = GroupsLogic.AnyOf(GroupIds( + UniqueNonEmptyList.of(GroupId("group3"), GroupId("group2")) + )), + tokenHeader = bearerHeader(jwt) + ) { + blockContext => + assertBlockContext( + currentGroup = Some(GroupId("group2")), + availableGroups = UniqueList.of(group("group2", "Group 2")) + )(blockContext) + } + } + "rule groups with 'or' logic are defined and intersection between those groups and JWT ones is not empty (2)" in { + val key: Key = Jwts.SIG.HS256.key().build() + val jwt = Jwt(key, claims = List( + "userId" := "user1", + Claim(NonEmptyList.one(ClaimKey("groups")), List( + Map("id" -> "group1", "name" -> "Group 1").asJava, + Map("id" -> "group2", "name" -> "Group 2").asJava + ).asJava) + )) + assertMatchRule( + configuredJwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name.authorization, "Bearer "), + SignatureCheckMethod.Hmac(key.getEncoded), + userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), + groupsConfig = Some(GroupsConfig( + idsClaim = domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.id)].id")), + namesClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.name)].name"))) + )) + ), + configuredGroups = GroupsLogic.AnyOf(GroupIds( + UniqueNonEmptyList.of(GroupId("group3"), GroupIdLike.from("*2")) + )), + tokenHeader = bearerHeader(jwt) + ) { + blockContext => + assertBlockContext( + currentGroup = Some(GroupId("group2")), + availableGroups = UniqueList.of(group("group2", "Group 2")) + )(blockContext) + } + } + "rule groups with 'and' logic are defined and intersection between those groups and JWT ones is not empty (1)" in { + val key: Key = Jwts.SIG.HS256.key().build() + val jwt = Jwt(key, claims = List( + "userId" := "user1", + Claim(NonEmptyList.one(ClaimKey("groups")), List( + Map("id" -> "group1", "name" -> "Group 1").asJava, + Map("id" -> "group2", "name" -> "Group 2").asJava + ).asJava) + )) + assertMatchRule( + configuredJwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name.authorization, "Bearer "), + SignatureCheckMethod.Hmac(key.getEncoded), + userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), + groupsConfig = Some(GroupsConfig( + idsClaim = domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.id)].id")), + namesClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.name)].name"))) + )) + ), + configuredGroups = GroupsLogic.AllOf(GroupIds( + UniqueNonEmptyList.of(GroupId("group1"), GroupId("group2")) + )), + tokenHeader = bearerHeader(jwt) + ) { + blockContext => + assertBlockContext( + currentGroup = Some(GroupId("group1")), + availableGroups = UniqueList.of(group("group1", "Group 1"), group("group2", "Group 2")) + )(blockContext) + } + } + "rule groups with 'and' logic are defined and intersection between those groups and JWT ones is not empty (2)" in { + val key: Key = Jwts.SIG.HS256.key().build() + val jwt = Jwt(key, claims = List( + "userId" := "user1", + Claim(NonEmptyList.one(ClaimKey("groups")), List( + Map("id" -> "group1", "name" -> "Group 1").asJava, + Map("id" -> "group2", "name" -> "Group 2").asJava + ).asJava) + )) + assertMatchRule( + configuredJwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name.authorization, "Bearer "), + SignatureCheckMethod.Hmac(key.getEncoded), + userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), + groupsConfig = Some(GroupsConfig( + idsClaim = domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.id)].id")), + namesClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.name)].name"))) + )) + ), + configuredGroups = GroupsLogic.AllOf(GroupIds( + UniqueNonEmptyList.of(GroupIdLike.from("*1"), GroupIdLike.from("*2")) + )), + tokenHeader = bearerHeader(jwt) + ) { + blockContext => + assertBlockContext( + currentGroup = Some(GroupId("group1")), + availableGroups = UniqueList.of(group("group1", "Group 1"), group("group2", "Group 2")) + )(blockContext) + } + } + } + "not match" when { + "group IDs claim path is wrong" in { + val key: Key = Jwts.SIG.HS256.key().build() + val jwt = Jwt(key, claims = List( + "userId" := "user1", + "groups" := List("group1", "group2") + )) + assertNotMatchRule( + configuredJwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name.authorization, "Bearer "), + SignatureCheckMethod.Hmac(key.getEncoded), + userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), + groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("tech.beshu.groups.subgroups")), None)) + ), + configuredGroups = GroupsLogic.AnyOf(GroupIds( + UniqueNonEmptyList.of(GroupId("group1")) + )), + tokenHeader = bearerHeader(jwt) + ) + } + "rule groups with 'or' logic are defined and intersection between those groups and JWT ones is empty" in { + val key: Key = Jwts.SIG.HS256.key().build() + val jwt = Jwt(key, claims = List( + "userId" := "user1", + "groups" := List("group1", "group2") + )) + assertNotMatchRule( + configuredJwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name.authorization, "Bearer "), + SignatureCheckMethod.Hmac(key.getEncoded), + userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), + groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + ), + configuredGroups = GroupsLogic.AnyOf(GroupIds( + UniqueNonEmptyList.of(GroupId("group3"), GroupId("group4")) + )), + tokenHeader = bearerHeader(jwt) + ) + } + "rule groups with 'and' logic are defined and intersection between those groups and JWT ones is empty" in { + val key: Key = Jwts.SIG.HS256.key().build() + val jwt = Jwt(key, claims = List( + "userId" := "user1", + "groups" := List("group1", "group2") + )) + assertNotMatchRule( + configuredJwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name.authorization, "Bearer "), + SignatureCheckMethod.Hmac(key.getEncoded), + userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), + groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + ), + configuredGroups = GroupsLogic.AllOf(GroupIds( + UniqueNonEmptyList.of(GroupId("group2"), GroupId("group3")) + )), + tokenHeader = bearerHeader(jwt) + ) + } + "preferred group is not on the permitted groups list" in { + val key: Key = Jwts.SIG.HS256.key().build() + val jwt = Jwt(key, claims = List( + "userId" := "user1", + "groups" := List("group1", "group2") + )) + assertNotMatchRule( + configuredJwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name.authorization, "Bearer "), + SignatureCheckMethod.Hmac(key.getEncoded), + userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), + groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + ), + configuredGroups = GroupsLogic.AnyOf(GroupIds( + UniqueNonEmptyList.of(GroupId("group2")) + )), + tokenHeader = bearerHeader(jwt), + preferredGroupId = Some(GroupId("group3")) + ) + } + } + } + + private def assertMatchRule(configuredJwtDef: JwtDef, + configuredGroups: GroupsLogic, + tokenHeader: Header, + preferredGroupId: Option[GroupId] = None) + (blockContextAssertion: BlockContext => Unit): Unit = + assertRule(configuredJwtDef, configuredGroups, tokenHeader, preferredGroupId, Some(blockContextAssertion)) + + private def assertNotMatchRule(configuredJwtDef: JwtDef, + configuredGroups: GroupsLogic, + tokenHeader: Header, + preferredGroupId: Option[GroupId] = None): Unit = + assertRule(configuredJwtDef, configuredGroups, tokenHeader, preferredGroupId, blockContextAssertion = None) + + private def assertRule(configuredJwtDef: JwtDef, + configuredGroups: GroupsLogic, + tokenHeader: Header, + preferredGroup: Option[GroupId], + blockContextAssertion: Option[BlockContext => Unit]) = { + val rule = new JwtAuthorizationRule(JwtAuthorizationRule.Settings(configuredJwtDef, configuredGroups)) + + val requestContext = MockRequestContext.indices.withHeaders( + preferredGroup.map(_.toCurrentGroupHeader).toSeq :+ tokenHeader + ) + val blockContext = GeneralIndexRequestBlockContext( + requestContext = requestContext, + userMetadata = UserMetadata.from(requestContext), + responseHeaders = Set.empty, + responseTransformations = List.empty, + filteredIndices = Set.empty, + allAllowedIndices = Set.empty + ) + val result = rule.check(blockContext).runSyncUnsafe(1 second) + blockContextAssertion match { + case Some(assertOutputBlockContext) => + inside(result) { case Fulfilled(outBlockContext) => + assertOutputBlockContext(outBlockContext) + } + case None => + result should be(Rejected()) + } + } +} diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/LocalUsersTest.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/LocalUsersTest.scala index e931d05d60..a3dc79074a 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/LocalUsersTest.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/LocalUsersTest.scala @@ -255,8 +255,6 @@ class LocalUsersTest extends AnyWordSpec with Inside { core.rorConfig.localUsers should be(allUsersResolved(Set( User.Id("admin"), User.Id("cartman"), User.Id("Bìlbö Bággįnš"), User.Id("bong"), User.Id("morgan") ))) - case Left(error) => - println(error) } } "ror_kbn_authentication rule used" in { @@ -307,8 +305,6 @@ class LocalUsersTest extends AnyWordSpec with Inside { core.rorConfig.localUsers should be(allUsersResolved(Set( User.Id("admin"), User.Id("cartman"), User.Id("Bìlbö Bággįnš"), User.Id("bong"), User.Id("morgan") ))) - case Left(error) => - println(error) } } } From 8a43e38bfe1b0c0b4b3f7465b5e69a06a614b650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Fri, 14 Nov 2025 23:41:33 +0100 Subject: [PATCH 05/15] perhaps fix bug --- .../factory/decoders/definitions/UsersDefinitionsDecoder.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/definitions/UsersDefinitionsDecoder.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/definitions/UsersDefinitionsDecoder.scala index 8925882778..12ca108572 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/definitions/UsersDefinitionsDecoder.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/definitions/UsersDefinitionsDecoder.scala @@ -256,7 +256,7 @@ object UsersDefinitionsDecoder { val localGroup: String = "local_group" val externalGroups: String = "external_group_ids" - val simpleMappingRequiredKeys: Set[String] = Set(localGroup) + val simpleMappingRequiredKeys: Set[String] = Set(id, name) val advancedMappingRequiredKeys: Set[String] = Set(localGroup, externalGroups) } From 07470fc18fbc44051aa87e77be9a9993e1e7fe96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Mon, 17 Nov 2025 22:15:14 +0100 Subject: [PATCH 06/15] qs --- .../auth/JwtPseudoAuthorizationRule.scala | 7 +--- .../rules/auth/JwtAuthRuleDecoder.scala | 2 +- .../blocks/rules/auth/JwtAuthRuleTests.scala | 36 +++++++++---------- 3 files changed, 20 insertions(+), 25 deletions(-) diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtPseudoAuthorizationRule.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtPseudoAuthorizationRule.scala index 39f30890d5..8153029e39 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtPseudoAuthorizationRule.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtPseudoAuthorizationRule.scala @@ -19,7 +19,7 @@ package tech.beshu.ror.accesscontrol.blocks.rules.auth import monix.eval.Task import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef import tech.beshu.ror.accesscontrol.blocks.rules.Rule -import tech.beshu.ror.accesscontrol.blocks.rules.Rule.{AuthorizationRule, RuleName, RuleResult} +import tech.beshu.ror.accesscontrol.blocks.rules.Rule.{AuthorizationRule, RuleResult} import tech.beshu.ror.accesscontrol.blocks.rules.auth.JwtPseudoAuthorizationRule.Settings import tech.beshu.ror.accesscontrol.blocks.rules.auth.base.BaseJwtRule import tech.beshu.ror.accesscontrol.blocks.rules.auth.base.impersonation.AuthorizationImpersonationCustomSupport @@ -60,10 +60,5 @@ final class JwtPseudoAuthorizationRule(val settings: Settings) } object JwtPseudoAuthorizationRule { - - implicit case object Name extends RuleName[JwtAuthorizationRule] { - override val name = Rule.Name("jwt_authorization") - } - final case class Settings(jwt: JwtDef) } diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtAuthRuleDecoder.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtAuthRuleDecoder.scala index 28404d95dd..80242e6705 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtAuthRuleDecoder.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtAuthRuleDecoder.scala @@ -106,7 +106,7 @@ class JwtAuthRuleDecoder(jwtDefinitions: Definitions[JwtDef], s"""Missing groups logic settings in ${JwtAuthRule.Name.name.show} rule. |For old configs, ROR treats this as `groups_any_of: ["*"]`. |This syntax is deprecated. Add groups logic (https://github.com/beshu-tech/readonlyrest-docs/blob/master/details/authorization-rules-details.md#checking-groups-logic), - |or use ${JwtAuthRule.Name.name.show} if you only need authentication. + |or use ${JwtAuthenticationRule.Name.name.show} if you only need authentication. |""".stripMargin ) new JwtPseudoAuthorizationRule(JwtPseudoAuthorizationRule.Settings(jwtDef)) diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthRuleTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthRuleTests.scala index 40d4c7d6ea..22dcd22a26 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthRuleTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthRuleTests.scala @@ -548,6 +548,24 @@ class JwtAuthRuleTests )(blockContext) } } + "preferred group is not on the groups list from JWT" in { + val key: Key = Jwts.SIG.HS256.key().build() + val jwt = Jwt(key, claims = List( + "userId" := "user1", + "groups" := List("group1", "group2") + )) + assertNotMatchRule( + configuredJwtDef = JwtDef( + JwtDef.Name("test"), + AuthorizationTokenDef(Header.Name.authorization, "Bearer "), + SignatureCheckMethod.Hmac(key.getEncoded), + userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), + groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + ), + tokenHeader = bearerHeader(jwt), + preferredGroupId = Some(GroupId("group3")) + ) + } } "not match" when { "token has invalid HS256 signature" in { @@ -681,24 +699,6 @@ class JwtAuthRuleTests tokenHeader = bearerHeader(jwt) ) } - "preferred group is not on the groups list from JWT" in { - val key: Key = Jwts.SIG.HS256.key().build() - val jwt = Jwt(key, claims = List( - "userId" := "user1", - "groups" := List("group1", "group2") - )) - assertNotMatchRule( - configuredJwtDef = JwtDef( - JwtDef.Name("test"), - AuthorizationTokenDef(Header.Name.authorization, "Bearer "), - SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) - ), - tokenHeader = bearerHeader(jwt), - preferredGroupId = Some(GroupId("group3")) - ) - } "preferred group is not on the permitted groups list" in { val key: Key = Jwts.SIG.HS256.key().build() val jwt = Jwt(key, claims = List( From 6ff686815818ad6bc20e08b328a6dc660cc213d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Fri, 21 Nov 2025 00:41:14 +0100 Subject: [PATCH 07/15] qs --- .../enabled_auditing_tools/readonlyrest.yml | 5 + .../LocalClusterAuditingToolsSuite.scala | 1 + .../log4j2_es_7.10_and_newer.properties | 98 +++++++++++++++---- 3 files changed, 84 insertions(+), 20 deletions(-) diff --git a/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest.yml b/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest.yml index cef500597e..432e2e5ddc 100644 --- a/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest.yml +++ b/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest.yml @@ -15,6 +15,11 @@ readonlyrest: tid: "{TASK_ID}" bytes: "{CONTENT_LENGTH_IN_BYTES}" block: "{REASON}" + - type: log + logger_name: readonlyrest_audit + serializer: + type: "ecs" + verbosity_level_serialization_mode: [INFO] - type: data_stream data_stream: "audit_data_stream" serializer: diff --git a/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala b/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala index ec17869ee9..da04f74c0c 100644 --- a/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala +++ b/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala @@ -261,6 +261,7 @@ class LocalClusterAuditingToolsSuite } shouldBe true } updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1") + Thread.sleep(Long.MaxValue) } } } diff --git a/tests-utils/src/main/resources/log4j2_es_7.10_and_newer.properties b/tests-utils/src/main/resources/log4j2_es_7.10_and_newer.properties index 2fa3a9662c..4c60e1d954 100644 --- a/tests-utils/src/main/resources/log4j2_es_7.10_and_newer.properties +++ b/tests-utils/src/main/resources/log4j2_es_7.10_and_newer.properties @@ -15,14 +15,20 @@ # along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ # # +######################################## +# Basic Settings +######################################## status=error -# log actionPost execution errors for easier debugging -logger.action.name=org.elasticsearch.action -logger.action.level=info +######################################## +# Console Appender +######################################## appender.console.type=Console appender.console.name=console appender.console.layout.type=PatternLayout appender.console.layout.pattern=[%d{ISO8601}][%-5p][%-25c{1.}] %marker%m%n +######################################## +# Rolling File Appender for main logs +######################################## appender.rolling.type=RollingFile appender.rolling.name=rolling appender.rolling.fileName=${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}.log @@ -33,9 +39,9 @@ appender.rolling.policies.type=Policies appender.rolling.policies.time.type=TimeBasedTriggeringPolicy appender.rolling.policies.time.interval=1 appender.rolling.policies.time.modulate=true -rootLogger.level=info -rootLogger.appenderRef.console.ref=console -rootLogger.appenderRef.rolling.ref=rolling +######################################## +# Deprecation logs +######################################## appender.deprecation_rolling.type=RollingFile appender.deprecation_rolling.name=deprecation_rolling appender.deprecation_rolling.fileName=${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_deprecation.log @@ -47,11 +53,9 @@ appender.deprecation_rolling.policies.size.type=SizeBasedTriggeringPolicy appender.deprecation_rolling.policies.size.size=1GB appender.deprecation_rolling.strategy.type=DefaultRolloverStrategy appender.deprecation_rolling.strategy.max=4 -logger.deprecation.name = org.elasticsearch.deprecation -logger.deprecation.level = deprecation -logger.deprecation.appenderRef.header_warning.ref = header_warning -logger.deprecation.appenderRef.deprecation_rolling.ref=deprecation_rolling -logger.deprecation.additivity=false +######################################## +# Slowlogs +######################################## appender.index_search_slowlog_rolling.type=RollingFile appender.index_search_slowlog_rolling.name=index_search_slowlog_rolling appender.index_search_slowlog_rolling.fileName=${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_index_search_slowlog.log @@ -62,10 +66,6 @@ appender.index_search_slowlog_rolling.policies.type=Policies appender.index_search_slowlog_rolling.policies.time.type=TimeBasedTriggeringPolicy appender.index_search_slowlog_rolling.policies.time.interval=1 appender.index_search_slowlog_rolling.policies.time.modulate=true -logger.index_search_slowlog_rolling.name=index.search.slowlog -logger.index_search_slowlog_rolling.level=trace -logger.index_search_slowlog_rolling.appenderRef.index_search_slowlog_rolling.ref=index_search_slowlog_rolling -logger.index_search_slowlog_rolling.additivity=false appender.index_indexing_slowlog_rolling.type=RollingFile appender.index_indexing_slowlog_rolling.name=index_indexing_slowlog_rolling appender.index_indexing_slowlog_rolling.fileName=${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_index_indexing_slowlog.log @@ -76,13 +76,71 @@ appender.index_indexing_slowlog_rolling.policies.type=Policies appender.index_indexing_slowlog_rolling.policies.time.type=TimeBasedTriggeringPolicy appender.index_indexing_slowlog_rolling.policies.time.interval=1 appender.index_indexing_slowlog_rolling.policies.time.modulate=true +######################################## +# Header Warning +######################################## +appender.header_warning.type=HeaderWarningAppender +appender.header_warning.name=header_warning +######################################## +# ReadonlyREST Audit Appender +######################################## +appender.ror_audit_rolling.type=RollingFile +appender.ror_audit_rolling.name=ror_audit_rolling +appender.ror_audit_rolling.fileName=${sys:es.logs.base_path}${sys:file.separator}readonlyrest_audit.log +appender.ror_audit_rolling.layout.type=PatternLayout +appender.ror_audit_rolling.layout.pattern=[%d{ISO8601}] %m%n +appender.ror_audit_rolling.filePattern=${sys:es.logs.base_path}${sys:file.separator}readonlyrest_audit-%i.log.gz +appender.ror_audit_rolling.policies.type=Policies +appender.ror_audit_rolling.policies.size.type=SizeBasedTriggeringPolicy +appender.ror_audit_rolling.policies.size.size=1GB +appender.ror_audit_rolling.strategy.type=DefaultRolloverStrategy +appender.ror_audit_rolling.strategy.max=4 +######################################## +# Root Logger +######################################## +rootLogger.level=info +rootLogger.appenderRef.console.type=AppenderRef +rootLogger.appenderRef.console.ref=console +rootLogger.appenderRef.rolling.type=AppenderRef +rootLogger.appenderRef.rolling.ref=rolling +rootLogger.appenderRef.ror_audit_router.type=AppenderRef +rootLogger.appenderRef.ror_audit_router.ref=ror_audit_rolling +######################################## +# Logger Definitions +######################################## +# Action +logger.action.name=org.elasticsearch.action +logger.action.level=info +logger.action.appenderRef.console.type=AppenderRef +logger.action.appenderRef.console.ref=console +logger.action.additivity=true +# Deprecation +logger.deprecation.name=org.elasticsearch.deprecation +logger.deprecation.level=deprecation +logger.deprecation.appenderRef.deprecation_rolling.type=AppenderRef +logger.deprecation.appenderRef.deprecation_rolling.ref=deprecation_rolling +logger.deprecation.appenderRef.header_warning.type=AppenderRef +logger.deprecation.appenderRef.header_warning.ref=header_warning +logger.deprecation.additivity=false +# index_search_slowlog +logger.index_search_slowlog_rolling.name=index.search.slowlog +logger.index_search_slowlog_rolling.level=trace +logger.index_search_slowlog_rolling.appenderRef.index_search_slowlog_rolling.type=AppenderRef +logger.index_search_slowlog_rolling.appenderRef.index_search_slowlog_rolling.ref=index_search_slowlog_rolling +logger.index_search_slowlog_rolling.additivity=false +# index_indexing_slowlog logger.index_indexing_slowlog.name=index.indexing.slowlog.index logger.index_indexing_slowlog.level=trace +logger.index_indexing_slowlog.appenderRef.index_indexing_slowlog_rolling.type=AppenderRef logger.index_indexing_slowlog.appenderRef.index_indexing_slowlog_rolling.ref=index_indexing_slowlog_rolling logger.index_indexing_slowlog.additivity=false - -appender.header_warning.type = HeaderWarningAppender -appender.header_warning.name = header_warning - +# ror_audit +logger.ror_audit.name=readonlyrest_audit +logger.ror_audit.level=debug +logger.ror_audit.appenderRef.ror_audit_rolling.type=AppenderRef +logger.ror_audit.appenderRef.ror_audit_rolling.ref=ror_audit_rolling +logger.ror_audit.additivity=false +# ror_ldap logger.ror_ldap.name=tech.beshu.ror.accesscontrol.blocks.definitions.ldap.implementations -logger.ror_ldap.level=debug \ No newline at end of file +logger.ror_ldap.level=debug +logger.ror_ldap.additivity=true From a54878dd509e07375738135d81e07547dd024b3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Fri, 21 Nov 2025 00:41:14 +0100 Subject: [PATCH 08/15] review changes part 1 --- .../definitions/DefinitionsPack.scala | 4 +- .../factory/decoders/ruleDecoders.scala | 12 +- .../rules/auth/JwtAuthRuleDecoder.scala | 160 ----------- .../rules/auth/JwtAuthRulesDecoders.scala | 54 ++++ .../rules/auth/JwtLikeRulesDecoders.scala | 176 ++++++++++++ .../rules/auth/RorKbnRulesDecoders.scala | 150 ++-------- .../rules/auth/JwtAuthRuleSettingsTests.scala | 152 +--------- .../JwtAuthenticationRuleSettingsTests.scala | 261 ++++++++++-------- .../JwtAuthorizationRuleSettingsTests.scala | 84 +++++- .../auth/RorKbnAuthRuleSettingsTests.scala | 151 +--------- ...orKbnAuthenticationRuleSettingsTests.scala | 6 +- ...RorKbnAuthorizationRuleSettingsTests.scala | 177 +----------- 12 files changed, 504 insertions(+), 883 deletions(-) delete mode 100644 core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtAuthRuleDecoder.scala create mode 100644 core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtAuthRulesDecoders.scala create mode 100644 core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtLikeRulesDecoders.scala diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/definitions/DefinitionsPack.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/definitions/DefinitionsPack.scala index 9499dffba2..ef7430eaf2 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/definitions/DefinitionsPack.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/definitions/DefinitionsPack.scala @@ -19,6 +19,7 @@ package tech.beshu.ror.accesscontrol.factory.decoders.definitions import cats.Show import tech.beshu.ror.accesscontrol.blocks.definitions.* import tech.beshu.ror.accesscontrol.blocks.definitions.ldap.LdapService +import tech.beshu.ror.accesscontrol.factory.decoders.definitions.Definitions.Item final case class DefinitionsPack(proxies: Definitions[ProxyAuth], users: Definitions[UserDef], @@ -30,9 +31,8 @@ final case class DefinitionsPack(proxies: Definitions[ProxyAuth], impersonators: Definitions[ImpersonatorDef], variableTransformationAliases: Definitions[VariableTransformationAliasDef]) -final case class Definitions[Item](items: List[Item]) extends AnyVal +final case class Definitions[ITEM <: Item](items: List[ITEM]) extends AnyVal object Definitions { - trait Item { type Id def id: Id diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/ruleDecoders.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/ruleDecoders.scala index 0f8e80ab61..36042b2ae2 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/ruleDecoders.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/ruleDecoders.scala @@ -132,21 +132,21 @@ object ruleDecoders { case ExternalAuthorizationRule.Name.name => Some(new ExternalAuthorizationRuleDecoder(authorizationServiceDefinitions, impersonatorsDefinitions, mocksProvider, globalSettings)) case JwtAuthRule.Name.name => - Some(new JwtAuthRuleDecoder(jwtDefinitions, globalSettings)) + Some(new JwtAuthRulesDecoders.AuthRuleDecoder(jwtDefinitions, globalSettings)) case JwtAuthenticationRule.Name.name => - Some(new JwtAuthenticationRuleDecoder(jwtDefinitions, globalSettings)) + Some(new JwtAuthRulesDecoders.AuthenticationRuleDecoder(jwtDefinitions, globalSettings)) case JwtAuthorizationRule.Name.name => - Some(new JwtAuthorizationRuleDecoder(jwtDefinitions)) + Some(new JwtAuthRulesDecoders.AuthorizationRuleDecoder(jwtDefinitions)) case LdapAuthorizationRule.Name.name => Some(new LdapAuthorizationRuleDecoder(ldapServiceDefinitions, impersonatorsDefinitions, mocksProvider, globalSettings)) case LdapAuthRule.Name.name => Some(new LdapAuthRuleDecoder(ldapServiceDefinitions, impersonatorsDefinitions, mocksProvider, globalSettings)) case RorKbnAuthRule.Name.name => - Some(new RorKbnAuthRuleDecoder(rorKbnDefinitions, globalSettings)) + Some(new RorKbnRulesDecoders.AuthRuleDecoder(rorKbnDefinitions, globalSettings)) case RorKbnAuthenticationRule.Name.name => - Some(new RorKbnAuthenticationRuleDecoder(rorKbnDefinitions, globalSettings)) + Some(new RorKbnRulesDecoders.AuthenticationRuleDecoder(rorKbnDefinitions, globalSettings)) case RorKbnAuthorizationRule.Name.name => - Some(new RorKbnAuthorizationRuleDecoder(rorKbnDefinitions)) + Some(new RorKbnRulesDecoders.AuthorizationRuleDecoder(rorKbnDefinitions)) case _ => authenticationRuleDecoderBy( name, diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtAuthRuleDecoder.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtAuthRuleDecoder.scala deleted file mode 100644 index 80242e6705..0000000000 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtAuthRuleDecoder.scala +++ /dev/null @@ -1,160 +0,0 @@ -/* - * This file is part of ReadonlyREST. - * - * ReadonlyREST is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ReadonlyREST is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ - */ -package tech.beshu.ror.accesscontrol.factory.decoders.rules.auth - -import io.circe.Decoder -import org.apache.logging.log4j.scala.Logging -import tech.beshu.ror.accesscontrol.blocks.Block.RuleDefinition -import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef -import tech.beshu.ror.accesscontrol.blocks.rules.Rule -import tech.beshu.ror.accesscontrol.blocks.rules.Rule.RuleName -import tech.beshu.ror.accesscontrol.blocks.rules.auth.{JwtAuthRule, JwtAuthenticationRule, JwtAuthorizationRule, JwtPseudoAuthorizationRule} -import tech.beshu.ror.accesscontrol.domain.GroupsLogic -import tech.beshu.ror.accesscontrol.factory.GlobalSettings -import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.Reason.Message -import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.RulesLevelCreationError -import tech.beshu.ror.accesscontrol.factory.decoders.definitions.Definitions -import tech.beshu.ror.accesscontrol.factory.decoders.definitions.JwtDefinitionsDecoder.* -import tech.beshu.ror.accesscontrol.factory.decoders.rules.RuleBaseDecoder.RuleBaseDecoderWithoutAssociatedFields -import tech.beshu.ror.accesscontrol.factory.decoders.rules.auth.JwtAuthRuleHelper.* -import tech.beshu.ror.accesscontrol.factory.decoders.rules.auth.groups.GroupsLogicDecoder -import tech.beshu.ror.accesscontrol.factory.decoders.rules.auth.groups.GroupsLogicRepresentationDecoder.GroupsLogicDecodingResult -import tech.beshu.ror.accesscontrol.utils.CirceOps.* -import tech.beshu.ror.implicits.* - -class JwtAuthenticationRuleDecoder(jwtDefinitions: Definitions[JwtDef], - globalSettings: GlobalSettings) - extends RuleBaseDecoderWithoutAssociatedFields[JwtAuthenticationRule] with Logging { - - override protected def decoder: Decoder[RuleDefinition[JwtAuthenticationRule]] = { - nameAndGroupsSimpleDecoder - .or(nameAndGroupsExtendedDecoder[JwtAuthenticationRule]) - .toSyncDecoder - .emapE { case (name, groupsLogicOpt) => - val foundJwtDef = jwtDefinitions.items.find(_.id === name) - (foundJwtDef, groupsLogicOpt) match { - case (Some(_), Some(_)) => - Left(RulesLevelCreationError(Message(s"Cannot create ${JwtAuthenticationRule.Name.name.show}, because there are superfluous groups settings. Remove the groups settings, or use ${JwtAuthorizationRule.Name.name.show} or ${JwtAuthRule.Name.name.show} rule, if group settings are required."))) - case (Some(jwtDef), None) => - val settings = JwtAuthenticationRule.Settings(jwtDef) - val rule = new JwtAuthenticationRule(settings, globalSettings.userIdCaseSensitivity) - Right(RuleDefinition.create(rule)) - case (None, _) => - Left(cannotFindJwtDefinition(name)) - } - } - .decoder - } -} - -class JwtAuthorizationRuleDecoder(jwtDefinitions: Definitions[JwtDef]) - extends RuleBaseDecoderWithoutAssociatedFields[JwtAuthorizationRule] with Logging { - - override protected def decoder: Decoder[RuleDefinition[JwtAuthorizationRule]] = { - nameAndGroupsSimpleDecoder - .or(nameAndGroupsExtendedDecoder[JwtAuthorizationRule]) - .toSyncDecoder - .emapE { case (name, groupsLogicOpt) => - val foundJwtDef = jwtDefinitions.items.find(_.id === name) - (foundJwtDef, groupsLogicOpt) match { - case (Some(jwtDef), Some(groupsLogic)) => - val settings = JwtAuthorizationRule.Settings(jwtDef, groupsLogic) - val rule = new JwtAuthorizationRule(settings) - Right(RuleDefinition.create[JwtAuthorizationRule](rule)) - case (Some(_), None) => - Left(RulesLevelCreationError(Message(s"Cannot create ${JwtAuthorizationRule.Name.name.show} - missing groups logic (https://github.com/beshu-tech/readonlyrest-docs/blob/master/details/authorization-rules-details.md#checking-groups-logic)"))) - case (None, _) => - Left(cannotFindJwtDefinition(name)) - } - } - .decoder - } -} - -class JwtAuthRuleDecoder(jwtDefinitions: Definitions[JwtDef], - globalSettings: GlobalSettings) - extends RuleBaseDecoderWithoutAssociatedFields[JwtAuthRule] with Logging { - - override protected def decoder: Decoder[RuleDefinition[JwtAuthRule]] = { - nameAndGroupsSimpleDecoder - .or(nameAndGroupsExtendedDecoder[JwtAuthRule]) - .toSyncDecoder - .emapE { case (name, groupsLogicOpt) => - val foundJwtDef = jwtDefinitions.items.find(_.id === name) - foundJwtDef match { - case Some(jwtDef) => - val authentication = new JwtAuthenticationRule(JwtAuthenticationRule.Settings(jwtDef), globalSettings.userIdCaseSensitivity) - val authorization: JwtAuthorizationRule | JwtPseudoAuthorizationRule = groupsLogicOpt match { - case Some(groupsLogic) => - new JwtAuthorizationRule(JwtAuthorizationRule.Settings(jwtDef, groupsLogic)) - case None => - logger.warn( - s"""Missing groups logic settings in ${JwtAuthRule.Name.name.show} rule. - |For old configs, ROR treats this as `groups_any_of: ["*"]`. - |This syntax is deprecated. Add groups logic (https://github.com/beshu-tech/readonlyrest-docs/blob/master/details/authorization-rules-details.md#checking-groups-logic), - |or use ${JwtAuthenticationRule.Name.name.show} if you only need authentication. - |""".stripMargin - ) - new JwtPseudoAuthorizationRule(JwtPseudoAuthorizationRule.Settings(jwtDef)) - } - val rule = new JwtAuthRule(authentication, authorization) - Right(RuleDefinition.create(rule)) - case None => - Left(cannotFindJwtDefinition(name)) - } - } - .decoder - } -} - -private object JwtAuthRuleHelper { - - def cannotFindJwtDefinition(name: JwtDef.Name) = - RulesLevelCreationError(Message(s"Cannot find JWT definition with name: ${name.show}")) - - val nameAndGroupsSimpleDecoder: Decoder[(JwtDef.Name, Option[GroupsLogic])] = - DecoderHelpers - .decodeStringLikeNonEmpty - .map(JwtDef.Name.apply) - .map((_, None)) - - def nameAndGroupsExtendedDecoder[T <: Rule](implicit ruleName: RuleName[T]): Decoder[(JwtDef.Name, Option[GroupsLogic])] = - Decoder - .instance { c => - for { - jwtDefName <- c.downField("name").as[JwtDef.Name] - groupsLogicDecodingResult <- GroupsLogicDecoder.decoder[T].apply(c) - } yield (jwtDefName, groupsLogicDecodingResult) - } - .toSyncDecoder - .emapE { - case (name, groupsLogicDecodingResult) => - groupsLogicDecodingResult match { - case GroupsLogicDecodingResult.Success(groupsLogic) => - Right((name, Some(groupsLogic))) - case GroupsLogicDecodingResult.GroupsLogicNotDefined(_) => - Right((name, None)) - case GroupsLogicDecodingResult.MultipleGroupsLogicsDefined(_, fields) => - val fieldsStr = fields.map(f => s"'$f'").mkString(" or ") - Left(RulesLevelCreationError(Message( - s"Please specify either $fieldsStr for JWT authorization rule '${name.show}'" - ))) - } - } - .decoder - -} \ No newline at end of file diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtAuthRulesDecoders.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtAuthRulesDecoders.scala new file mode 100644 index 0000000000..2c0aeab65f --- /dev/null +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtAuthRulesDecoders.scala @@ -0,0 +1,54 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.accesscontrol.factory.decoders.rules.auth + +import org.apache.logging.log4j.scala.Logging +import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef +import tech.beshu.ror.accesscontrol.blocks.rules.auth.{JwtAuthRule, JwtAuthenticationRule, JwtAuthorizationRule, JwtPseudoAuthorizationRule} +import tech.beshu.ror.accesscontrol.domain.GroupsLogic +import tech.beshu.ror.accesscontrol.factory.GlobalSettings + +object JwtAuthRulesDecoders + extends JwtLikeRulesDecoders[ + JwtAuthenticationRule, + JwtAuthorizationRule, + JwtPseudoAuthorizationRule, + JwtAuthRule, + JwtDef, + ] with Logging { + + override def humanReadableName: String = "JWT" + + override def createAuthenticationRule(definition: JwtDef, globalSettings: GlobalSettings): JwtAuthenticationRule = + new JwtAuthenticationRule(JwtAuthenticationRule.Settings(definition), globalSettings.userIdCaseSensitivity) + + override def createAuthorizationRule(definition: JwtDef, groupsLogic: GroupsLogic): JwtAuthorizationRule = + new JwtAuthorizationRule(JwtAuthorizationRule.Settings(definition, groupsLogic)) + + override def createAuthorizationRuleWithoutGroups(definition: JwtDef): JwtPseudoAuthorizationRule = + new JwtPseudoAuthorizationRule(JwtPseudoAuthorizationRule.Settings(definition)) + + override def createAuthRule(authnRule: JwtAuthenticationRule, authzRule: JwtAuthorizationRule): JwtAuthRule = + new JwtAuthRule(authnRule, authzRule) + + override def createAuthRuleWithoutGroups(authnRule: JwtAuthenticationRule, authzRule: JwtPseudoAuthorizationRule): JwtAuthRule = + new JwtAuthRule(authnRule, authzRule) + + override def serializeDefinitionId(definition: JwtDef): String = + definition.id.value.value + +} diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtLikeRulesDecoders.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtLikeRulesDecoders.scala new file mode 100644 index 0000000000..d269fffa19 --- /dev/null +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtLikeRulesDecoders.scala @@ -0,0 +1,176 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ReadonlyREST is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.accesscontrol.factory.decoders.rules.auth + +import eu.timepit.refined.types.string.NonEmptyString +import io.circe.Decoder +import org.apache.logging.log4j.scala.Logging +import tech.beshu.ror.accesscontrol.blocks.Block.RuleDefinition +import tech.beshu.ror.accesscontrol.blocks.ImpersonationWarning.ImpersonationWarningSupport +import tech.beshu.ror.accesscontrol.blocks.rules.Rule +import tech.beshu.ror.accesscontrol.blocks.rules.Rule.{AuthRule, AuthenticationRule, AuthorizationRule, RuleName} +import tech.beshu.ror.accesscontrol.blocks.users.LocalUsersContext.LocalUsersSupport +import tech.beshu.ror.accesscontrol.blocks.variables.runtime.VariableContext.VariableUsage +import tech.beshu.ror.accesscontrol.domain.GroupsLogic +import tech.beshu.ror.accesscontrol.factory.GlobalSettings +import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.Reason.Message +import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.RulesLevelCreationError +import tech.beshu.ror.accesscontrol.factory.decoders.common.nonEmptyStringDecoder +import tech.beshu.ror.accesscontrol.factory.decoders.definitions.Definitions +import tech.beshu.ror.accesscontrol.factory.decoders.rules.RuleBaseDecoder.RuleBaseDecoderWithoutAssociatedFields +import tech.beshu.ror.accesscontrol.factory.decoders.rules.auth.groups.GroupsLogicDecoder +import tech.beshu.ror.accesscontrol.factory.decoders.rules.auth.groups.GroupsLogicRepresentationDecoder.GroupsLogicDecodingResult +import tech.beshu.ror.accesscontrol.utils.CirceOps.* +import tech.beshu.ror.implicits.* + +// Common decoder for JWT rules and ROR KBN rules. They are very similar, and their decoding logic is mostly the same. +trait JwtLikeRulesDecoders[ + AUTHN_RULE <: AuthenticationRule : RuleName : VariableUsage : LocalUsersSupport : ImpersonationWarningSupport, + AUTHZ_RULE <: AuthorizationRule : RuleName : VariableUsage : LocalUsersSupport : ImpersonationWarningSupport, + AUTHZ_WITHOUT_GROUPS_RULE <: AuthorizationRule, + AUTH_RULE <: AuthRule : RuleName : VariableUsage : LocalUsersSupport : ImpersonationWarningSupport, + DEFINITION <: Definitions.Item, +] { + this: Logging => + + def humanReadableName: String + + def createAuthenticationRule(definition: DEFINITION, + globalSettings: GlobalSettings): AUTHN_RULE + + def createAuthorizationRule(definition: DEFINITION, + groupsLogic: GroupsLogic): AUTHZ_RULE + + def createAuthRule(authnRule: AUTHN_RULE, + authzRule: AUTHZ_RULE): AUTH_RULE + + def createAuthorizationRuleWithoutGroups(definition: DEFINITION): AUTHZ_WITHOUT_GROUPS_RULE + + def createAuthRuleWithoutGroups(authnRule: AUTHN_RULE, + authzRule: AUTHZ_WITHOUT_GROUPS_RULE): AUTH_RULE + + def serializeDefinitionId(definition: DEFINITION): String + + class AuthenticationRuleDecoder(definitions: Definitions[DEFINITION], + globalSettings: GlobalSettings) extends RuleBaseDecoderWithoutAssociatedFields[AUTHN_RULE] { + override protected def decoder: Decoder[RuleDefinition[AUTHN_RULE]] = + nameAndGroupsSimpleDecoder + .or(nameAndGroupsExtendedDecoder[AUTHN_RULE]) + .toSyncDecoder + .emapE { case (name, groupsLogicOpt) => + val definitionOpt = definitions.items.find(d => serializeDefinitionId(d) == name) + (definitionOpt, groupsLogicOpt) match { + case (Some(_), Some(_)) => + Left(RulesLevelCreationError(Message(s"Cannot create ${RuleName[AUTHN_RULE].name.show}, because there are superfluous groups settings. Remove the groups settings, or use ${RuleName[AUTHZ_RULE].name.show} or ${RuleName[AUTH_RULE].name.show} rule, if group settings are required."))) + case (Some(definition), None) => + val rule = createAuthenticationRule(definition, globalSettings) + Right(RuleDefinition.create(rule)) + case (None, _) => + Left(cannotFindDefinition(name)) + } + } + .decoder + } + + class AuthorizationRuleDecoder(definitions: Definitions[DEFINITION]) extends RuleBaseDecoderWithoutAssociatedFields[AUTHZ_RULE] { + override protected def decoder: Decoder[RuleDefinition[AUTHZ_RULE]] = + nameAndGroupsSimpleDecoder + .or(nameAndGroupsExtendedDecoder[AUTHZ_RULE]) + .toSyncDecoder + .emapE { case (name, groupsLogicOpt) => + val definitionOpt = definitions.items.find(d => serializeDefinitionId(d) == name) + (definitionOpt, groupsLogicOpt) match { + case (Some(definition), Some(groupsLogic)) => + val rule = createAuthorizationRule(definition, groupsLogic) + Right(RuleDefinition.create[AUTHZ_RULE](rule)) + case (Some(_), None) => + Left(RulesLevelCreationError(Message(s"Cannot create ${RuleName[AUTHZ_RULE].name.show} - missing groups logic (https://github.com/beshu-tech/readonlyrest-docs/blob/master/details/authorization-rules-details.md#checking-groups-logic)"))) + case (None, _) => + Left(cannotFindDefinition(name)) + } + } + .decoder + } + + class AuthRuleDecoder(definitions: Definitions[DEFINITION], + globalSettings: GlobalSettings) extends RuleBaseDecoderWithoutAssociatedFields[AUTH_RULE] { + override protected def decoder: Decoder[RuleDefinition[AUTH_RULE]] = + nameAndGroupsSimpleDecoder + .or(nameAndGroupsExtendedDecoder[AUTH_RULE]) + .toSyncDecoder + .emapE { case (name, groupsLogicOpt) => + val definitionOpt = definitions.items.find(d => serializeDefinitionId(d) == name) + (definitionOpt, groupsLogicOpt) match { + case (Some(definition), Some(groupsLogic)) => + val authentication = createAuthenticationRule(definition, globalSettings) + val authorization = createAuthorizationRule(definition, groupsLogic) + val rule = createAuthRule(authentication, authorization) + Right(RuleDefinition.create(rule)) + case (Some(definition), None) => + logger.warn( + s"""Missing groups logic settings in ${RuleName[AUTH_RULE].name.show} rule. + |For old configs, ROR treats this as `groups_any_of: ["*"]`. + |This syntax is deprecated. Add groups logic (https://github.com/beshu-tech/readonlyrest-docs/blob/master/details/authorization-rules-details.md#checking-groups-logic), + |or use ${RuleName[AUTHN_RULE].name.show} if you only need authentication. + |""".stripMargin + ) + val authentication = createAuthenticationRule(definition, globalSettings) + val authorization = createAuthorizationRuleWithoutGroups(definition) + val rule = createAuthRuleWithoutGroups(authentication, authorization) + Right(RuleDefinition.create(rule)) + case (None, _) => + Left(cannotFindDefinition(name)) + } + } + .decoder + } + + private def nameAndGroupsSimpleDecoder: Decoder[(String, Option[GroupsLogic])] = + DecoderHelpers + .decodeStringLikeNonEmpty + .map(_.value) + .map((_, None)) + + private def nameAndGroupsExtendedDecoder[RULE <: Rule : RuleName]: Decoder[(String, Option[GroupsLogic])] = + Decoder + .instance { c => + for { + definitionName <- c.downField("name").as[NonEmptyString].map(_.value) + groupsLogicDecodingResult <- GroupsLogicDecoder.decoder[RULE].apply(c) + } yield (definitionName, groupsLogicDecodingResult) + } + .toSyncDecoder + .emapE { + case (name, groupsLogicDecodingResult) => + groupsLogicDecodingResult match { + case GroupsLogicDecodingResult.Success(groupsLogic) => + Right((name, Some(groupsLogic))) + case GroupsLogicDecodingResult.GroupsLogicNotDefined(_) => + Right((name, None)) + case GroupsLogicDecodingResult.MultipleGroupsLogicsDefined(_, fields) => + val fieldsStr = fields.map(f => s"'$f'").mkString(" or ") + Left(RulesLevelCreationError(Message( + s"Please specify either $fieldsStr for $humanReadableName authorization rule '$name'" + ))) + } + } + .decoder + + private def cannotFindDefinition(name: String) = + RulesLevelCreationError(Message(s"Cannot find $humanReadableName definition with name: $name")) + +} diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/RorKbnRulesDecoders.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/RorKbnRulesDecoders.scala index c79c91a551..2f4e3b7860 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/RorKbnRulesDecoders.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/RorKbnRulesDecoders.scala @@ -16,150 +16,42 @@ */ package tech.beshu.ror.accesscontrol.factory.decoders.rules.auth -import io.circe.Decoder import org.apache.logging.log4j.scala.Logging -import tech.beshu.ror.accesscontrol.blocks.Block.RuleDefinition import tech.beshu.ror.accesscontrol.blocks.definitions.RorKbnDef -import tech.beshu.ror.accesscontrol.blocks.rules.Rule -import tech.beshu.ror.accesscontrol.blocks.rules.Rule.RuleName import tech.beshu.ror.accesscontrol.blocks.rules.auth.{RorKbnAuthRule, RorKbnAuthenticationRule, RorKbnAuthorizationRule} import tech.beshu.ror.accesscontrol.domain.GroupIdLike.GroupIdPattern import tech.beshu.ror.accesscontrol.domain.{GroupIds, GroupsLogic} import tech.beshu.ror.accesscontrol.factory.GlobalSettings -import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.Reason.Message -import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.RulesLevelCreationError -import tech.beshu.ror.accesscontrol.factory.decoders.definitions.Definitions -import tech.beshu.ror.accesscontrol.factory.decoders.definitions.RorKbnDefinitionsDecoder.* -import tech.beshu.ror.accesscontrol.factory.decoders.rules.RuleBaseDecoder.RuleBaseDecoderWithoutAssociatedFields -import tech.beshu.ror.accesscontrol.factory.decoders.rules.auth.RorKbnRulesDecodersHelper.* -import tech.beshu.ror.accesscontrol.factory.decoders.rules.auth.groups.GroupsLogicDecoder -import tech.beshu.ror.accesscontrol.factory.decoders.rules.auth.groups.GroupsLogicRepresentationDecoder.GroupsLogicDecodingResult -import tech.beshu.ror.accesscontrol.utils.CirceOps.* -import tech.beshu.ror.implicits.* import tech.beshu.ror.utils.RefinedUtils.nes import tech.beshu.ror.utils.uniquelist.UniqueNonEmptyList -class RorKbnAuthenticationRuleDecoder(rorKbnDefinitions: Definitions[RorKbnDef], - globalSettings: GlobalSettings) - extends RuleBaseDecoderWithoutAssociatedFields[RorKbnAuthenticationRule] with Logging { +object RorKbnRulesDecoders + extends JwtLikeRulesDecoders[ + RorKbnAuthenticationRule, + RorKbnAuthorizationRule, + RorKbnAuthorizationRule, + RorKbnAuthRule, + RorKbnDef, + ] with Logging { - override protected def decoder: Decoder[RuleDefinition[RorKbnAuthenticationRule]] = { - nameAndGroupsSimpleDecoder - .or(nameAndGroupsExtendedDecoder[RorKbnAuthenticationRule]) - .toSyncDecoder - .emapE { case (name, groupsLogicOpt) => - val foundKbnDef = rorKbnDefinitions.items.find(_.id === name) - (foundKbnDef, groupsLogicOpt) match { - case (Some(_), Some(_)) => - Left(RulesLevelCreationError(Message(s"Cannot create ${RorKbnAuthenticationRule.Name.name.show}, because there are superfluous groups settings. Remove the groups settings, or use ${RorKbnAuthorizationRule.Name.name.show} or ${RorKbnAuthRule.Name.name.show} rule, if group settings are required."))) - case (Some(rorKbnDef), None) => - val settings = RorKbnAuthenticationRule.Settings(rorKbnDef) - val rule = new RorKbnAuthenticationRule(settings, globalSettings.userIdCaseSensitivity) - Right(RuleDefinition.create(rule)) - case (None, _) => - Left(cannotFindRorKibanaDefinition(name)) - } - } - .decoder - } -} - -class RorKbnAuthorizationRuleDecoder(rorKbnDefinitions: Definitions[RorKbnDef]) - extends RuleBaseDecoderWithoutAssociatedFields[RorKbnAuthorizationRule] with Logging { + override def humanReadableName: String = "ROR Kibana" - override protected def decoder: Decoder[RuleDefinition[RorKbnAuthorizationRule]] = { - nameAndGroupsSimpleDecoder - .or(nameAndGroupsExtendedDecoder[RorKbnAuthorizationRule]) - .toSyncDecoder - .emapE { case (name, groupsLogicOpt) => - val foundKbnDef = rorKbnDefinitions.items.find(_.id === name) - (foundKbnDef, groupsLogicOpt) match { - case (Some(rorKbnDef), Some(groupsLogic)) => - val settings = RorKbnAuthorizationRule.Settings(rorKbnDef, groupsLogic) - val rule = new RorKbnAuthorizationRule(settings) - Right(RuleDefinition.create[RorKbnAuthorizationRule](rule)) - case (Some(_), None) => - Left(RulesLevelCreationError(Message(s"Cannot create ${RorKbnAuthorizationRule.Name.name.show} - missing groups logic (https://github.com/beshu-tech/readonlyrest-docs/blob/master/details/authorization-rules-details.md#checking-groups-logic)"))) - case (None, _) => - Left(cannotFindRorKibanaDefinition(name)) - } - } - .decoder - } -} + override def createAuthenticationRule(definition: RorKbnDef, globalSettings: GlobalSettings): RorKbnAuthenticationRule = + new RorKbnAuthenticationRule(RorKbnAuthenticationRule.Settings(definition), globalSettings.userIdCaseSensitivity) -class RorKbnAuthRuleDecoder(rorKbnDefinitions: Definitions[RorKbnDef], - globalSettings: GlobalSettings) - extends RuleBaseDecoderWithoutAssociatedFields[RorKbnAuthRule] with Logging { - - override protected def decoder: Decoder[RuleDefinition[RorKbnAuthRule]] = { - nameAndGroupsSimpleDecoder - .or(nameAndGroupsExtendedDecoder[RorKbnAuthRule]) - .toSyncDecoder - .emapE { case (name, groupsLogicOpt) => - val foundKbnDef = rorKbnDefinitions.items.find(_.id === name) - (foundKbnDef, groupsLogicOpt) match { - case (Some(rorKbnDef), groupsLogicOpt) => - val groupsLogic = groupsLogicOpt match { - case Some(groupsLogic) => - groupsLogic - case None => - logger.warn( - s"""Missing groups logic settings in ${RorKbnAuthRule.Name.name.show} rule. - |For old configs, ROR treats this as `groups_any_of: ["*"]`. - |This syntax is deprecated. Add groups logic (https://github.com/beshu-tech/readonlyrest-docs/blob/master/details/authorization-rules-details.md#checking-groups-logic), - |or use ${RorKbnAuthenticationRule.Name.name.show} if you only need authentication. - |""".stripMargin - ) - GroupsLogic.AnyOf(GroupIds(UniqueNonEmptyList.of(GroupIdPattern.fromNes(nes("*"))))) - } - val rule = new RorKbnAuthRule( - authentication = new RorKbnAuthenticationRule(RorKbnAuthenticationRule.Settings(rorKbnDef), globalSettings.userIdCaseSensitivity), - authorization = new RorKbnAuthorizationRule(RorKbnAuthorizationRule.Settings(rorKbnDef, groupsLogic)), - ) - Right(RuleDefinition.create(rule)) - case (None, _) => - Left(cannotFindRorKibanaDefinition(name)) - } - } - .decoder - } -} + override def createAuthorizationRule(definition: RorKbnDef, groupsLogic: GroupsLogic): RorKbnAuthorizationRule = + new RorKbnAuthorizationRule(RorKbnAuthorizationRule.Settings(definition, groupsLogic)) -private object RorKbnRulesDecodersHelper { + override def createAuthorizationRuleWithoutGroups(definition: RorKbnDef): RorKbnAuthorizationRule = + createAuthorizationRule(definition, GroupsLogic.AnyOf(GroupIds(UniqueNonEmptyList.of(GroupIdPattern.fromNes(nes("*")))))) - def cannotFindRorKibanaDefinition(name: RorKbnDef.Name) = - RulesLevelCreationError(Message(s"Cannot find ROR Kibana definition with name: ${name.show}")) + override def createAuthRule(authnRule: RorKbnAuthenticationRule, authzRule: RorKbnAuthorizationRule): RorKbnAuthRule = + new RorKbnAuthRule(authnRule, authzRule) - val nameAndGroupsSimpleDecoder: Decoder[(RorKbnDef.Name, Option[GroupsLogic])] = - DecoderHelpers - .decodeStringLikeNonEmpty - .map(RorKbnDef.Name.apply) - .map((_, None)) + override def createAuthRuleWithoutGroups(authnRule: RorKbnAuthenticationRule, authzRule: RorKbnAuthorizationRule): RorKbnAuthRule = + new RorKbnAuthRule(authnRule, authzRule) - def nameAndGroupsExtendedDecoder[T <: Rule](implicit ruleName: RuleName[T]): Decoder[(RorKbnDef.Name, Option[GroupsLogic])] = - Decoder - .instance { c => - for { - rorKbnDefName <- c.downField("name").as[RorKbnDef.Name] - groupsLogicDecodingResult <- GroupsLogicDecoder.decoder[T].apply(c) - } yield (rorKbnDefName, groupsLogicDecodingResult) - } - .toSyncDecoder - .emapE { - case (name, groupsLogicDecodingResult) => - groupsLogicDecodingResult match { - case GroupsLogicDecodingResult.Success(groupsLogic) => - Right((name, Some(groupsLogic))) - case GroupsLogicDecodingResult.GroupsLogicNotDefined(_) => - Right((name, None)) - case GroupsLogicDecodingResult.MultipleGroupsLogicsDefined(_, fields) => - val fieldsStr = fields.map(f => s"'$f'").mkString(" or ") - Left(RulesLevelCreationError(Message( - s"Please specify either $fieldsStr for ROR Kibana rule '${name.show}'" - ))) - } - } - .decoder + override def serializeDefinitionId(definition: RorKbnDef): String = + definition.id.value.value } diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthRuleSettingsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthRuleSettingsTests.scala index fb65e36bfe..446da843c6 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthRuleSettingsTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthRuleSettingsTests.scala @@ -15,18 +15,19 @@ * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ */ package tech.beshu.ror.unit.acl.factory.decoders.rules.auth + import org.scalamock.scalatest.MockFactory import org.scalatest.matchers.should.Matchers.* import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef.{GroupsConfig, SignatureCheckMethod} import tech.beshu.ror.accesscontrol.blocks.definitions.{CacheableExternalAuthenticationServiceDecorator, JwtDef} import tech.beshu.ror.accesscontrol.blocks.rules.auth.{JwtAuthRule, JwtAuthorizationRule, JwtPseudoAuthorizationRule} import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.GroupIdLike.GroupId import tech.beshu.ror.accesscontrol.domain.* +import tech.beshu.ror.accesscontrol.domain.GroupIdLike.GroupId import tech.beshu.ror.accesscontrol.factory.HttpClientsFactory import tech.beshu.ror.accesscontrol.factory.HttpClientsFactory.HttpClient import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.Reason.{MalformedValue, Message} -import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.{DefinitionsLevelCreationError, GeneralReadonlyrestSettingsError, RulesLevelCreationError} +import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.{DefinitionsLevelCreationError, RulesLevelCreationError} import tech.beshu.ror.mocks.MockHttpClientsFactoryWithFixedHttpClient import tech.beshu.ror.providers.EnvVarProvider.EnvVarName import tech.beshu.ror.providers.EnvVarsProvider @@ -798,153 +799,6 @@ class JwtAuthRuleSettingsTests } ) } - "no signature key is defined for default HMAC algorithm" in { - assertDecodingFailure( - yaml = - """ - |readonlyrest: - | - | access_control_rules: - | - | - name: test_block1 - | jwt_auth: jwt1 - | - | jwt: - | - | - name: jwt1 - | - |""".stripMargin, - assertion = errors => { - errors should have size 1 - errors.head should be(DefinitionsLevelCreationError(MalformedValue.fromString( - """- name: "jwt1" - |""".stripMargin - ))) - } - ) - } - "RSA algorithm is defined but on signature key" in { - assertDecodingFailure( - yaml = - """ - |readonlyrest: - | - | access_control_rules: - | - | - name: test_block1 - | jwt_auth: jwt1 - | - | jwt: - | - | - name: jwt1 - | signature_algo: "RSA" - | - |""".stripMargin, - assertion = errors => { - errors should have size 1 - errors.head should be(DefinitionsLevelCreationError(MalformedValue.fromString( - """- name: "jwt1" - | signature_algo: "RSA" - |""".stripMargin - ))) - } - ) - } - "unrecognized algorithm is used" in { - assertDecodingFailure( - yaml = - """ - |readonlyrest: - | - | access_control_rules: - | - | - name: test_block1 - | jwt_auth: jwt1 - | - | jwt: - | - | - name: jwt1 - | signature_algo: "UNKNOWN" - | - |""".stripMargin, - assertion = errors => { - errors should have size 1 - errors.head should be(DefinitionsLevelCreationError(Message("Unrecognised algorithm family 'UNKNOWN'. Should be either of: HMAC, EC, RSA, NONE"))) - } - ) - } - "RSA signature key is malformed" in { - assertDecodingFailure( - yaml = - """ - |readonlyrest: - | - | access_control_rules: - | - | - name: test_block1 - | jwt_auth: jwt1 - | - | jwt: - | - | - name: jwt1 - | signature_algo: "RSA" - | signature_key: "malformed_key" - | - |""".stripMargin, - assertion = errors => { - errors should have size 1 - errors.head should be(DefinitionsLevelCreationError(Message("Key 'malformed_key' seems to be invalid"))) - } - ) - } - "RSA signature key cannot be read from system env" in { - assertDecodingFailure( - yaml = - """ - |readonlyrest: - | - | access_control_rules: - | - | - name: test_block1 - | jwt_auth: jwt1 - | - | jwt: - | - | - name: jwt1 - | signature_algo: "RSA" - | signature_key: "@{env:SECRET}" - | - |""".stripMargin, - assertion = errors => { - errors should have size 1 - errors.head should be(GeneralReadonlyrestSettingsError(Message("Cannot resolve ENV variable 'SECRET'"))) - } - ) - } - "EC signature key is malformed" in { - assertDecodingFailure( - yaml = - """ - |readonlyrest: - | - | access_control_rules: - | - | - name: test_block1 - | jwt_auth: jwt1 - | - | jwt: - | - | - name: jwt1 - | signature_algo: "EC" - | signature_key: "malformed_key" - | - |""".stripMargin, - assertion = errors => { - errors should have size 1 - errors.head should be(DefinitionsLevelCreationError(Message("Key 'malformed_key' seems to be invalid"))) - } - ) - } "no signature check is used but required external validation service is not defined" in { assertDecodingFailure( yaml = diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthenticationRuleSettingsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthenticationRuleSettingsTests.scala index b3a1b04b9b..f8deaa4832 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthenticationRuleSettingsTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthenticationRuleSettingsTests.scala @@ -26,7 +26,7 @@ import tech.beshu.ror.accesscontrol.domain.* import tech.beshu.ror.accesscontrol.factory.HttpClientsFactory import tech.beshu.ror.accesscontrol.factory.HttpClientsFactory.HttpClient import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.Reason.{MalformedValue, Message} -import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.RulesLevelCreationError +import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.{DefinitionsLevelCreationError, GeneralReadonlyrestSettingsError, RulesLevelCreationError} import tech.beshu.ror.mocks.MockHttpClientsFactoryWithFixedHttpClient import tech.beshu.ror.providers.EnvVarProvider.EnvVarName import tech.beshu.ror.providers.EnvVarsProvider @@ -95,116 +95,6 @@ class JwtAuthenticationRuleSettingsTests } ) } - "token header name can be changes in JWT definition" in { - assertDecodingSuccess( - yaml = - """ - |readonlyrest: - | - | access_control_rules: - | - | - name: test_block1 - | jwt_authentication: jwt1 - | - | jwt: - | - | - name: jwt1 - | header_name: X-JWT-Custom-Header - | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" - | - |""".stripMargin, - assertion = rule => { - rule.settings.jwt.id should be(JwtDef.Name("jwt1")) - rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(headerNameFrom("X-JWT-Custom-Header"), "Bearer ")) - rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(None) - } - ) - } - "token prefix can be changes in JWT definition for custom token header" in { - assertDecodingSuccess( - yaml = - """ - |readonlyrest: - | - | access_control_rules: - | - | - name: test_block1 - | jwt_authentication: jwt1 - | - | jwt: - | - | - name: jwt1 - | header_name: X-JWT-Custom-Header - | header_prefix: "MyPrefix " - | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" - | - |""".stripMargin, - assertion = rule => { - rule.settings.jwt.id should be(JwtDef.Name("jwt1")) - rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(headerNameFrom("X-JWT-Custom-Header"), "MyPrefix ")) - rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(None) - } - ) - } - "token prefix can be changes in JWT definition for standard token header" in { - assertDecodingSuccess( - yaml = - """ - |readonlyrest: - | - | access_control_rules: - | - | - name: test_block1 - | jwt_authentication: jwt1 - | - | jwt: - | - | - name: jwt1 - | header_prefix: "MyPrefix " - | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" - | - |""".stripMargin, - assertion = rule => { - rule.settings.jwt.id should be(JwtDef.Name("jwt1")) - rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "MyPrefix ")) - rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(None) - } - ) - } - "custom prefix attribute is empty" in { - assertDecodingSuccess( - yaml = - """ - |readonlyrest: - | - | access_control_rules: - | - | - name: test_block1 - | jwt_authentication: jwt1 - | - | jwt: - | - | - name: jwt1 - | header_name: X-JWT-Custom-Header - | header_prefix: "" - | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" - | - |""".stripMargin, - assertion = rule => { - rule.settings.jwt.id should be(JwtDef.Name("jwt1")) - rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(headerNameFrom("X-JWT-Custom-Header"), "")) - rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(None) - } - ) - } "user claim can be enabled in JWT definition" in { assertDecodingSuccess( yaml = @@ -320,6 +210,8 @@ class JwtAuthenticationRuleSettingsTests } ) } + } + "be able to be loaded from config (token-related)" when { "RSA family algorithm can be used in JWT signature" in { val pkey = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPublic assertDecodingSuccess( @@ -575,6 +467,153 @@ class JwtAuthenticationRuleSettingsTests } ) } + "no signature key is defined for default HMAC algorithm" in { + assertDecodingFailure( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authentication: jwt1 + | + | jwt: + | + | - name: jwt1 + | + |""".stripMargin, + assertion = errors => { + errors should have size 1 + errors.head should be(DefinitionsLevelCreationError(MalformedValue.fromString( + """- name: "jwt1" + |""".stripMargin + ))) + } + ) + } + "RSA algorithm is defined but on signature key" in { + assertDecodingFailure( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authentication: jwt1 + | + | jwt: + | + | - name: jwt1 + | signature_algo: "RSA" + | + |""".stripMargin, + assertion = errors => { + errors should have size 1 + errors.head should be(DefinitionsLevelCreationError(MalformedValue.fromString( + """- name: "jwt1" + | signature_algo: "RSA" + |""".stripMargin + ))) + } + ) + } + "unrecognized algorithm is used" in { + assertDecodingFailure( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authentication: jwt1 + | + | jwt: + | + | - name: jwt1 + | signature_algo: "UNKNOWN" + | + |""".stripMargin, + assertion = errors => { + errors should have size 1 + errors.head should be(DefinitionsLevelCreationError(Message("Unrecognised algorithm family 'UNKNOWN'. Should be either of: HMAC, EC, RSA, NONE"))) + } + ) + } + "RSA signature key is malformed" in { + assertDecodingFailure( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authentication: jwt1 + | + | jwt: + | + | - name: jwt1 + | signature_algo: "RSA" + | signature_key: "malformed_key" + | + |""".stripMargin, + assertion = errors => { + errors should have size 1 + errors.head should be(DefinitionsLevelCreationError(Message("Key 'malformed_key' seems to be invalid"))) + } + ) + } + "RSA signature key cannot be read from system env" in { + assertDecodingFailure( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authentication: jwt1 + | + | jwt: + | + | - name: jwt1 + | signature_algo: "RSA" + | signature_key: "@{env:SECRET}" + | + |""".stripMargin, + assertion = errors => { + errors should have size 1 + errors.head should be(GeneralReadonlyrestSettingsError(Message("Cannot resolve ENV variable 'SECRET'"))) + } + ) + } + "EC signature key is malformed" in { + assertDecodingFailure( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authentication: jwt1 + | + | jwt: + | + | - name: jwt1 + | signature_algo: "EC" + | signature_key: "malformed_key" + | + |""".stripMargin, + assertion = errors => { + errors should have size 1 + errors.head should be(DefinitionsLevelCreationError(Message("Key 'malformed_key' seems to be invalid"))) + } + ) + } } } diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthorizationRuleSettingsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthorizationRuleSettingsTests.scala index 16968b903a..28b5785b53 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthorizationRuleSettingsTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthorizationRuleSettingsTests.scala @@ -24,7 +24,7 @@ import tech.beshu.ror.accesscontrol.blocks.rules.auth.JwtAuthorizationRule import tech.beshu.ror.accesscontrol.domain.* import tech.beshu.ror.accesscontrol.domain.GroupIdLike.GroupId import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.Reason.{MalformedValue, Message} -import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.RulesLevelCreationError +import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.{DefinitionsLevelCreationError, RulesLevelCreationError} import tech.beshu.ror.providers.EnvVarProvider.EnvVarName import tech.beshu.ror.providers.EnvVarsProvider import tech.beshu.ror.unit.acl.factory.decoders.rules.BaseRuleSettingsDecoderTest @@ -240,6 +240,88 @@ class JwtAuthorizationRuleSettingsTests ) } } + "no JWT definition name is defined" in { + assertDecodingFailure( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authorization: jwt1 + | + | jwt: + | - signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" + | + |""".stripMargin, + assertion = errors => { + errors should have size 1 + errors.head should be(DefinitionsLevelCreationError(MalformedValue.fromString( + """- signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" + |""".stripMargin + ))) + } + ) + } + "both 'groups or' key and 'groups and' key used" in { + List( + ("roles", "roles_and"), + ("groups", "groups_and") + ) + .foreach { case (groupsAnyOfKey, groupsAllOfKey) => + assertDecodingFailure( + yaml = + s""" + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authorization: + | name: "jwt1" + | $groupsAnyOfKey: ["group1", "group2"] + | $groupsAllOfKey: ["groups1", "groups2"] + | jwt: + | - name: jwt2 + | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" + | + |""".stripMargin, + assertion = errors => { + errors should have size 1 + errors.head should be(RulesLevelCreationError(Message( + s"Please specify either '$groupsAnyOfKey' or '$groupsAllOfKey' for JWT authorization rule 'jwt1'") + )) + } + ) + } + } + "two JWT definitions have the same names" in { + assertDecodingFailure( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_authorization: jwt1 + | + | jwt: + | + | - name: jwt1 + | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" + | + | - name: jwt1 + | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" + | + |""".stripMargin, + assertion = errors => { + errors should have size 1 + errors.head should be(DefinitionsLevelCreationError(Message("jwt definitions must have unique identifiers. Duplicates: jwt1"))) + } + ) + } } } diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/RorKbnAuthRuleSettingsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/RorKbnAuthRuleSettingsTests.scala index deaaceda7a..7e9a293833 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/RorKbnAuthRuleSettingsTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/RorKbnAuthRuleSettingsTests.scala @@ -23,7 +23,7 @@ import tech.beshu.ror.accesscontrol.blocks.rules.auth.RorKbnAuthRule import tech.beshu.ror.accesscontrol.domain.GroupIdLike.GroupId import tech.beshu.ror.accesscontrol.domain.{GroupIdLike, GroupIds, GroupsLogic} import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.Reason.{MalformedValue, Message} -import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.{DefinitionsLevelCreationError, GeneralReadonlyrestSettingsError, RulesLevelCreationError} +import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.{DefinitionsLevelCreationError, RulesLevelCreationError} import tech.beshu.ror.providers.EnvVarProvider.EnvVarName import tech.beshu.ror.providers.EnvVarsProvider import tech.beshu.ror.unit.acl.factory.decoders.rules.BaseRuleSettingsDecoderTest @@ -257,7 +257,7 @@ class RorKbnAuthRuleSettingsTests assertion = errors => { errors should have size 1 errors.head should be(RulesLevelCreationError(Message( - s"Please specify either '$groupsAnyOfKey' or '$groupsAllOfKey' for ROR Kibana rule 'kbn1'") + s"Please specify either '$groupsAnyOfKey' or '$groupsAllOfKey' for ROR Kibana authorization rule 'kbn1'") )) } ) @@ -289,153 +289,6 @@ class RorKbnAuthRuleSettingsTests } ) } - "no signature key is defined for default HMAC algorithm" in { - assertDecodingFailure( - yaml = - """ - |readonlyrest: - | - | access_control_rules: - | - | - name: test_block1 - | ror_kbn_auth: kbn1 - | - | ror_kbn: - | - | - name: kbn1 - | - |""".stripMargin, - assertion = errors => { - errors should have size 1 - errors.head should be(DefinitionsLevelCreationError(MalformedValue.fromString( - """- name: "kbn1" - |""".stripMargin - ))) - } - ) - } - "RSA algorithm is defined but on signature key" in { - assertDecodingFailure( - yaml = - """ - |readonlyrest: - | - | access_control_rules: - | - | - name: test_block1 - | ror_kbn_auth: kbn1 - | - | ror_kbn: - | - | - name: kbn1 - | signature_algo: "RSA" - | - |""".stripMargin, - assertion = errors => { - errors should have size 1 - errors.head should be(DefinitionsLevelCreationError(MalformedValue.fromString( - """- name: "kbn1" - | signature_algo: "RSA" - |""".stripMargin - ))) - } - ) - } - "unrecognized algorithm is used" in { - assertDecodingFailure( - yaml = - """ - |readonlyrest: - | - | access_control_rules: - | - | - name: test_block1 - | ror_kbn_auth: kbn1 - | - | ror_kbn: - | - | - name: kbn1 - | signature_algo: "UNKNOWN" - | - |""".stripMargin, - assertion = errors => { - errors should have size 1 - errors.head should be(DefinitionsLevelCreationError(Message("Unrecognised algorithm family 'UNKNOWN'. Should be either of: HMAC, EC, RSA, NONE"))) - } - ) - } - "RSA signature key is malformed" in { - assertDecodingFailure( - yaml = - """ - |readonlyrest: - | - | access_control_rules: - | - | - name: test_block1 - | ror_kbn_auth: kbn1 - | - | ror_kbn: - | - | - name: kbn1 - | signature_algo: "RSA" - | signature_key: "malformed_key" - | - |""".stripMargin, - assertion = errors => { - errors should have size 1 - errors.head should be(DefinitionsLevelCreationError(Message("Key 'malformed_key' seems to be invalid"))) - } - ) - } - "RSA signature key cannot be read from system env" in { - assertDecodingFailure( - yaml = - """ - |readonlyrest: - | - | access_control_rules: - | - | - name: test_block1 - | ror_kbn_auth: kbn1 - | - | ror_kbn: - | - | - name: kbn1 - | signature_algo: "RSA" - | signature_key: "@{env:SECRET}" - | - |""".stripMargin, - assertion = errors => { - errors should have size 1 - errors.head should be(GeneralReadonlyrestSettingsError(Message("Cannot resolve ENV variable 'SECRET'"))) - } - ) - } - "EC signature key is malformed" in { - assertDecodingFailure( - yaml = - """ - |readonlyrest: - | - | access_control_rules: - | - | - name: test_block1 - | ror_kbn_auth: kbn1 - | - | ror_kbn: - | - | - name: kbn1 - | signature_algo: "EC" - | signature_key: "malformed_key" - | - |""".stripMargin, - assertion = errors => { - errors should have size 1 - errors.head should be(DefinitionsLevelCreationError(Message("Key 'malformed_key' seems to be invalid"))) - } - ) - } } } diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/RorKbnAuthenticationRuleSettingsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/RorKbnAuthenticationRuleSettingsTests.scala index 2ffc8a5a44..b79f810c2b 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/RorKbnAuthenticationRuleSettingsTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/RorKbnAuthenticationRuleSettingsTests.scala @@ -82,6 +82,8 @@ class RorKbnAuthenticationRuleSettingsTests } ) } + } + "be able to be loaded from config (token-related)" when { "RSA family algorithm can be used in token signature" in { val pkey = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPublic assertDecodingSuccess( @@ -360,7 +362,7 @@ class RorKbnAuthenticationRuleSettingsTests assertion = errors => { errors should have size 1 errors.head should be(RulesLevelCreationError(Message( - s"Please specify either '$groupsAnyOfKey' or '$groupsAllOfKey' for ROR Kibana rule 'kbn1'") + s"Please specify either '$groupsAnyOfKey' or '$groupsAllOfKey' for ROR Kibana authorization rule 'kbn1'") )) } ) @@ -392,6 +394,8 @@ class RorKbnAuthenticationRuleSettingsTests } ) } + } + "not be able to be loaded from config (token-related)" when { "no signature key is defined for default HMAC algorithm" in { assertDecodingFailure( yaml = diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/RorKbnAuthorizationRuleSettingsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/RorKbnAuthorizationRuleSettingsTests.scala index 52bb7fd7a3..8fc61d2c1b 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/RorKbnAuthorizationRuleSettingsTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/RorKbnAuthorizationRuleSettingsTests.scala @@ -23,7 +23,7 @@ import tech.beshu.ror.accesscontrol.blocks.rules.auth.RorKbnAuthorizationRule import tech.beshu.ror.accesscontrol.domain.GroupIdLike.GroupId import tech.beshu.ror.accesscontrol.domain.{GroupIdLike, GroupIds, GroupsLogic} import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.Reason.{MalformedValue, Message} -import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.{DefinitionsLevelCreationError, GeneralReadonlyrestSettingsError, RulesLevelCreationError} +import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.{DefinitionsLevelCreationError, RulesLevelCreationError} import tech.beshu.ror.providers.EnvVarProvider.EnvVarName import tech.beshu.ror.providers.EnvVarsProvider import tech.beshu.ror.unit.acl.factory.decoders.rules.BaseRuleSettingsDecoderTest @@ -256,185 +256,12 @@ class RorKbnAuthorizationRuleSettingsTests assertion = errors => { errors should have size 1 errors.head should be(RulesLevelCreationError(Message( - s"Please specify either '$groupsAnyOfKey' or '$groupsAllOfKey' for ROR Kibana rule 'kbn1'") + s"Please specify either '$groupsAnyOfKey' or '$groupsAllOfKey' for ROR Kibana authorization rule 'kbn1'") )) } ) } } - "two ROR kbn definitions have the same names" in { - assertDecodingFailure( - yaml = - """ - |readonlyrest: - | - | access_control_rules: - | - | - name: test_block1 - | ror_kbn_authorization: kbn1 - | - | ror_kbn: - | - | - name: kbn1 - | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" - | - | - name: kbn1 - | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" - | - |""".stripMargin, - assertion = errors => { - errors should have size 1 - errors.head should be(DefinitionsLevelCreationError(Message("ror_kbn definitions must have unique identifiers. Duplicates: kbn1"))) - } - ) - } - "no signature key is defined for default HMAC algorithm" in { - assertDecodingFailure( - yaml = - """ - |readonlyrest: - | - | access_control_rules: - | - | - name: test_block1 - | ror_kbn_authorization: kbn1 - | - | ror_kbn: - | - | - name: kbn1 - | - |""".stripMargin, - assertion = errors => { - errors should have size 1 - errors.head should be(DefinitionsLevelCreationError(MalformedValue.fromString( - """- name: "kbn1" - |""".stripMargin - ))) - } - ) - } - "RSA algorithm is defined but on signature key" in { - assertDecodingFailure( - yaml = - """ - |readonlyrest: - | - | access_control_rules: - | - | - name: test_block1 - | ror_kbn_authorization: kbn1 - | - | ror_kbn: - | - | - name: kbn1 - | signature_algo: "RSA" - | - |""".stripMargin, - assertion = errors => { - errors should have size 1 - errors.head should be(DefinitionsLevelCreationError(MalformedValue.fromString( - """- name: "kbn1" - | signature_algo: "RSA" - |""".stripMargin - ))) - } - ) - } - "unrecognized algorithm is used" in { - assertDecodingFailure( - yaml = - """ - |readonlyrest: - | - | access_control_rules: - | - | - name: test_block1 - | ror_kbn_authorization: kbn1 - | - | ror_kbn: - | - | - name: kbn1 - | signature_algo: "UNKNOWN" - | - |""".stripMargin, - assertion = errors => { - errors should have size 1 - errors.head should be(DefinitionsLevelCreationError(Message("Unrecognised algorithm family 'UNKNOWN'. Should be either of: HMAC, EC, RSA, NONE"))) - } - ) - } - "RSA signature key is malformed" in { - assertDecodingFailure( - yaml = - """ - |readonlyrest: - | - | access_control_rules: - | - | - name: test_block1 - | ror_kbn_authorization: kbn1 - | - | ror_kbn: - | - | - name: kbn1 - | signature_algo: "RSA" - | signature_key: "malformed_key" - | - |""".stripMargin, - assertion = errors => { - errors should have size 1 - errors.head should be(DefinitionsLevelCreationError(Message("Key 'malformed_key' seems to be invalid"))) - } - ) - } - "RSA signature key cannot be read from system env" in { - assertDecodingFailure( - yaml = - """ - |readonlyrest: - | - | access_control_rules: - | - | - name: test_block1 - | ror_kbn_authorization: kbn1 - | - | ror_kbn: - | - | - name: kbn1 - | signature_algo: "RSA" - | signature_key: "@{env:SECRET}" - | - |""".stripMargin, - assertion = errors => { - errors should have size 1 - errors.head should be(GeneralReadonlyrestSettingsError(Message("Cannot resolve ENV variable 'SECRET'"))) - } - ) - } - "EC signature key is malformed" in { - assertDecodingFailure( - yaml = - """ - |readonlyrest: - | - | access_control_rules: - | - | - name: test_block1 - | ror_kbn_authorization: kbn1 - | - | ror_kbn: - | - | - name: kbn1 - | signature_algo: "EC" - | signature_key: "malformed_key" - | - |""".stripMargin, - assertion = errors => { - errors should have size 1 - errors.head should be(DefinitionsLevelCreationError(Message("Key 'malformed_key' seems to be invalid"))) - } - ) - } } } From f81285a43d33d5d4b922cd3a9fc565862ae5b0ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Sat, 29 Nov 2025 15:01:46 +0100 Subject: [PATCH 09/15] Revert "qs" This reverts commit 6ff686815818ad6bc20e08b328a6dc660cc213d1. --- .../enabled_auditing_tools/readonlyrest.yml | 5 - .../LocalClusterAuditingToolsSuite.scala | 1 - .../log4j2_es_7.10_and_newer.properties | 98 ++++--------------- 3 files changed, 20 insertions(+), 84 deletions(-) diff --git a/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest.yml b/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest.yml index 432e2e5ddc..cef500597e 100644 --- a/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest.yml +++ b/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest.yml @@ -15,11 +15,6 @@ readonlyrest: tid: "{TASK_ID}" bytes: "{CONTENT_LENGTH_IN_BYTES}" block: "{REASON}" - - type: log - logger_name: readonlyrest_audit - serializer: - type: "ecs" - verbosity_level_serialization_mode: [INFO] - type: data_stream data_stream: "audit_data_stream" serializer: diff --git a/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala b/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala index da04f74c0c..ec17869ee9 100644 --- a/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala +++ b/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala @@ -261,7 +261,6 @@ class LocalClusterAuditingToolsSuite } shouldBe true } updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1") - Thread.sleep(Long.MaxValue) } } } diff --git a/tests-utils/src/main/resources/log4j2_es_7.10_and_newer.properties b/tests-utils/src/main/resources/log4j2_es_7.10_and_newer.properties index 4c60e1d954..2fa3a9662c 100644 --- a/tests-utils/src/main/resources/log4j2_es_7.10_and_newer.properties +++ b/tests-utils/src/main/resources/log4j2_es_7.10_and_newer.properties @@ -15,20 +15,14 @@ # along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ # # -######################################## -# Basic Settings -######################################## status=error -######################################## -# Console Appender -######################################## +# log actionPost execution errors for easier debugging +logger.action.name=org.elasticsearch.action +logger.action.level=info appender.console.type=Console appender.console.name=console appender.console.layout.type=PatternLayout appender.console.layout.pattern=[%d{ISO8601}][%-5p][%-25c{1.}] %marker%m%n -######################################## -# Rolling File Appender for main logs -######################################## appender.rolling.type=RollingFile appender.rolling.name=rolling appender.rolling.fileName=${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}.log @@ -39,9 +33,9 @@ appender.rolling.policies.type=Policies appender.rolling.policies.time.type=TimeBasedTriggeringPolicy appender.rolling.policies.time.interval=1 appender.rolling.policies.time.modulate=true -######################################## -# Deprecation logs -######################################## +rootLogger.level=info +rootLogger.appenderRef.console.ref=console +rootLogger.appenderRef.rolling.ref=rolling appender.deprecation_rolling.type=RollingFile appender.deprecation_rolling.name=deprecation_rolling appender.deprecation_rolling.fileName=${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_deprecation.log @@ -53,9 +47,11 @@ appender.deprecation_rolling.policies.size.type=SizeBasedTriggeringPolicy appender.deprecation_rolling.policies.size.size=1GB appender.deprecation_rolling.strategy.type=DefaultRolloverStrategy appender.deprecation_rolling.strategy.max=4 -######################################## -# Slowlogs -######################################## +logger.deprecation.name = org.elasticsearch.deprecation +logger.deprecation.level = deprecation +logger.deprecation.appenderRef.header_warning.ref = header_warning +logger.deprecation.appenderRef.deprecation_rolling.ref=deprecation_rolling +logger.deprecation.additivity=false appender.index_search_slowlog_rolling.type=RollingFile appender.index_search_slowlog_rolling.name=index_search_slowlog_rolling appender.index_search_slowlog_rolling.fileName=${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_index_search_slowlog.log @@ -66,6 +62,10 @@ appender.index_search_slowlog_rolling.policies.type=Policies appender.index_search_slowlog_rolling.policies.time.type=TimeBasedTriggeringPolicy appender.index_search_slowlog_rolling.policies.time.interval=1 appender.index_search_slowlog_rolling.policies.time.modulate=true +logger.index_search_slowlog_rolling.name=index.search.slowlog +logger.index_search_slowlog_rolling.level=trace +logger.index_search_slowlog_rolling.appenderRef.index_search_slowlog_rolling.ref=index_search_slowlog_rolling +logger.index_search_slowlog_rolling.additivity=false appender.index_indexing_slowlog_rolling.type=RollingFile appender.index_indexing_slowlog_rolling.name=index_indexing_slowlog_rolling appender.index_indexing_slowlog_rolling.fileName=${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_index_indexing_slowlog.log @@ -76,71 +76,13 @@ appender.index_indexing_slowlog_rolling.policies.type=Policies appender.index_indexing_slowlog_rolling.policies.time.type=TimeBasedTriggeringPolicy appender.index_indexing_slowlog_rolling.policies.time.interval=1 appender.index_indexing_slowlog_rolling.policies.time.modulate=true -######################################## -# Header Warning -######################################## -appender.header_warning.type=HeaderWarningAppender -appender.header_warning.name=header_warning -######################################## -# ReadonlyREST Audit Appender -######################################## -appender.ror_audit_rolling.type=RollingFile -appender.ror_audit_rolling.name=ror_audit_rolling -appender.ror_audit_rolling.fileName=${sys:es.logs.base_path}${sys:file.separator}readonlyrest_audit.log -appender.ror_audit_rolling.layout.type=PatternLayout -appender.ror_audit_rolling.layout.pattern=[%d{ISO8601}] %m%n -appender.ror_audit_rolling.filePattern=${sys:es.logs.base_path}${sys:file.separator}readonlyrest_audit-%i.log.gz -appender.ror_audit_rolling.policies.type=Policies -appender.ror_audit_rolling.policies.size.type=SizeBasedTriggeringPolicy -appender.ror_audit_rolling.policies.size.size=1GB -appender.ror_audit_rolling.strategy.type=DefaultRolloverStrategy -appender.ror_audit_rolling.strategy.max=4 -######################################## -# Root Logger -######################################## -rootLogger.level=info -rootLogger.appenderRef.console.type=AppenderRef -rootLogger.appenderRef.console.ref=console -rootLogger.appenderRef.rolling.type=AppenderRef -rootLogger.appenderRef.rolling.ref=rolling -rootLogger.appenderRef.ror_audit_router.type=AppenderRef -rootLogger.appenderRef.ror_audit_router.ref=ror_audit_rolling -######################################## -# Logger Definitions -######################################## -# Action -logger.action.name=org.elasticsearch.action -logger.action.level=info -logger.action.appenderRef.console.type=AppenderRef -logger.action.appenderRef.console.ref=console -logger.action.additivity=true -# Deprecation -logger.deprecation.name=org.elasticsearch.deprecation -logger.deprecation.level=deprecation -logger.deprecation.appenderRef.deprecation_rolling.type=AppenderRef -logger.deprecation.appenderRef.deprecation_rolling.ref=deprecation_rolling -logger.deprecation.appenderRef.header_warning.type=AppenderRef -logger.deprecation.appenderRef.header_warning.ref=header_warning -logger.deprecation.additivity=false -# index_search_slowlog -logger.index_search_slowlog_rolling.name=index.search.slowlog -logger.index_search_slowlog_rolling.level=trace -logger.index_search_slowlog_rolling.appenderRef.index_search_slowlog_rolling.type=AppenderRef -logger.index_search_slowlog_rolling.appenderRef.index_search_slowlog_rolling.ref=index_search_slowlog_rolling -logger.index_search_slowlog_rolling.additivity=false -# index_indexing_slowlog logger.index_indexing_slowlog.name=index.indexing.slowlog.index logger.index_indexing_slowlog.level=trace -logger.index_indexing_slowlog.appenderRef.index_indexing_slowlog_rolling.type=AppenderRef logger.index_indexing_slowlog.appenderRef.index_indexing_slowlog_rolling.ref=index_indexing_slowlog_rolling logger.index_indexing_slowlog.additivity=false -# ror_audit -logger.ror_audit.name=readonlyrest_audit -logger.ror_audit.level=debug -logger.ror_audit.appenderRef.ror_audit_rolling.type=AppenderRef -logger.ror_audit.appenderRef.ror_audit_rolling.ref=ror_audit_rolling -logger.ror_audit.additivity=false -# ror_ldap + +appender.header_warning.type = HeaderWarningAppender +appender.header_warning.name = header_warning + logger.ror_ldap.name=tech.beshu.ror.accesscontrol.blocks.definitions.ldap.implementations -logger.ror_ldap.level=debug -logger.ror_ldap.additivity=true +logger.ror_ldap.level=debug \ No newline at end of file From 308273a21bd5dbbaa165a68cfbe53e23ab57c504 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Sat, 13 Dec 2025 15:29:26 +0100 Subject: [PATCH 10/15] qs --- .../{JwtDef.scala => JwtDefForAuthRule.scala} | 43 ++- .../blocks/rules/auth/JwtAuthRule.scala | 2 +- .../rules/auth/JwtAuthenticationRule.scala | 27 +- .../rules/auth/JwtAuthorizationRule.scala | 32 +- .../auth/JwtPseudoAuthorizationRule.scala | 64 ---- .../blocks/rules/auth/base/BaseJwtRule.scala | 76 +---- .../definitions/DefinitionsPack.scala | 2 +- .../definitions/JwtDefinitionsDecoder.scala | 65 +++- .../factory/decoders/ruleDecoders.scala | 17 +- .../rules/auth/JwtAuthRulesDecoders.scala | 28 +- .../rules/auth/JwtLikeRulesDecoders.scala | 114 ++++--- .../rules/auth/RorKbnRulesDecoders.scala | 29 +- .../ror/accesscontrol/utils/CirceOps.scala | 4 + .../blocks/rules/auth/JwtAuthRuleTests.scala | 306 +++++++++++------- .../auth/JwtAuthenticationRuleTests.scala | 153 ++++----- .../auth/JwtAuthorizationRuleTests.scala | 54 ++-- .../factory/ImpersonationWarningsTests.scala | 2 + .../rules/auth/JwtAuthRuleSettingsTests.scala | 144 +++++---- .../JwtAuthenticationRuleSettingsTests.scala | 107 ++---- .../JwtAuthorizationRuleSettingsTests.scala | 34 +- .../auth/RorKbnAuthRuleSettingsTests.scala | 6 +- ...orKbnAuthenticationRuleSettingsTests.scala | 8 +- ...RorKbnAuthorizationRuleSettingsTests.scala | 6 +- 23 files changed, 676 insertions(+), 647 deletions(-) rename core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/definitions/{JwtDef.scala => JwtDefForAuthRule.scala} (57%) delete mode 100644 core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtPseudoAuthorizationRule.scala diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/definitions/JwtDef.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/definitions/JwtDefForAuthRule.scala similarity index 57% rename from core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/definitions/JwtDef.scala rename to core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/definitions/JwtDefForAuthRule.scala index 58b821ab02..40b78922c3 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/definitions/JwtDef.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/definitions/JwtDefForAuthRule.scala @@ -24,16 +24,16 @@ import tech.beshu.ror.accesscontrol.factory.decoders.definitions.Definitions.Ite import java.security.PublicKey -final case class JwtDef(override val id: Name, - authorizationTokenDef: AuthorizationTokenDef, - checkMethod: SignatureCheckMethod, - userClaim: Option[Jwt.ClaimName], - groupsConfig: Option[GroupsConfig]) - extends Item { - +sealed trait JwtDef extends Item { override type Id = Name override val idShow: Show[Name] = Show.show(_.value.value) + + override def id: Name + + def authorizationTokenDef: AuthorizationTokenDef + def checkMethod: SignatureCheckMethod } + object JwtDef { final case class Name(value: NonEmptyString) @@ -48,4 +48,31 @@ object JwtDef { final case class GroupsConfig(idsClaim: Jwt.ClaimName, namesClaim: Option[Jwt.ClaimName]) implicit val nameEq: Eq[Name] = Eq.fromUniversalEquals -} \ No newline at end of file +} + +trait JwtDefForAuthentication extends JwtDef { + def userClaim: Jwt.ClaimName +} + +trait JwtDefForAuthorization extends JwtDef { + def groupsConfig: GroupsConfig +} + +trait JwtDefForAuth extends JwtDefForAuthentication with JwtDefForAuthorization + + +final case class AuthenticationJwtDef(override val id: Name, + authorizationTokenDef: AuthorizationTokenDef, + checkMethod: SignatureCheckMethod, + userClaim: Jwt.ClaimName) extends JwtDefForAuthentication + +final case class AuthorizationJwtDef(override val id: Name, + authorizationTokenDef: AuthorizationTokenDef, + checkMethod: SignatureCheckMethod, + groupsConfig: GroupsConfig) extends JwtDefForAuthorization + +final case class AuthJwtDef(override val id: Name, + authorizationTokenDef: AuthorizationTokenDef, + checkMethod: SignatureCheckMethod, + userClaim: Jwt.ClaimName, + groupsConfig: GroupsConfig) extends JwtDefForAuth diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthRule.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthRule.scala index 3f392623dd..d316e401c6 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthRule.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthRule.scala @@ -23,7 +23,7 @@ import tech.beshu.ror.accesscontrol.blocks.rules.auth.base.BaseComposedAuthentic import tech.beshu.ror.accesscontrol.domain.* final class JwtAuthRule(val authentication: JwtAuthenticationRule, - val authorization: JwtAuthorizationRule | JwtPseudoAuthorizationRule) + val authorization: JwtAuthorizationRule) extends BaseComposedAuthenticationAndAuthorizationRule( authenticationRule = authentication.withDisabledCallsToExternalAuthenticationService, authorizationRule = authorization diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthenticationRule.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthenticationRule.scala index 9510d6cda7..68888f8e42 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthenticationRule.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthenticationRule.scala @@ -16,8 +16,9 @@ */ package tech.beshu.ror.accesscontrol.blocks.rules.auth +import cats.implicits.toShow import monix.eval.Task -import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef +import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDefForAuthentication import tech.beshu.ror.accesscontrol.blocks.rules.Rule import tech.beshu.ror.accesscontrol.blocks.rules.Rule.AuthenticationRule.EligibleUsersSupport import tech.beshu.ror.accesscontrol.blocks.rules.Rule.{AuthenticationRule, RuleName, RuleResult} @@ -27,8 +28,10 @@ import tech.beshu.ror.accesscontrol.blocks.rules.auth.base.impersonation.Authent import tech.beshu.ror.accesscontrol.blocks.{BlockContext, BlockContextUpdater} import tech.beshu.ror.accesscontrol.domain.* import tech.beshu.ror.accesscontrol.domain.LoggedUser.DirectlyLoggedUser -import tech.beshu.ror.accesscontrol.utils.ClaimsOps.ClaimSearchResult import tech.beshu.ror.accesscontrol.utils.ClaimsOps.ClaimSearchResult.{Found, NotFound} +import tech.beshu.ror.accesscontrol.utils.ClaimsOps.{ClaimSearchResult, toClaimsOps} +import tech.beshu.ror.implicits.* + final class JwtAuthenticationRule(val settings: Settings, override val userIdCaseSensitivity: CaseSensitivity, @@ -42,21 +45,27 @@ final class JwtAuthenticationRule(val settings: Settings, override val eligibleUsers: EligibleUsersSupport = EligibleUsersSupport.NotAvailable override protected[rules] def authenticate[B <: BlockContext : BlockContextUpdater](blockContext: B): Task[RuleResult[B]] = { - processUsingJwtToken(blockContext, settings.jwt, disabledCallsToExternalAuthenticationService) { tokenData => - authenticate(blockContext, tokenData.userId, tokenData.payload) + processUsingJwtToken(blockContext, settings.jwt, disabledCallsToExternalAuthenticationService) { payload => + authenticate(blockContext, payload) } } private def authenticate[B <: BlockContext : BlockContextUpdater](blockContext: B, - result: Option[ClaimSearchResult[User.Id]], payload: Jwt.Payload) = { + val result = payload.claims.userIdClaim(settings.jwt.userClaim) + logClaimSearchResults(blockContext, result) (result match { - case None => Right(blockContext) - case Some(NotFound) => Left(()) - case Some(Found(userId)) => Right(blockContext.withUserMetadata(_.withLoggedUser(DirectlyLoggedUser(userId)))) + case NotFound => Left(()) + case Found(userId) => Right(blockContext.withUserMetadata(_.withLoggedUser(DirectlyLoggedUser(userId)))) }).map(_.withUserMetadata(_.withJwtToken(payload))) } + private def logClaimSearchResults[B <: BlockContext](blockContext: B, + user: ClaimSearchResult[User.Id]): Unit = { + implicit val requestId: RequestId = blockContext.requestContext.id.toRequestId + logger.debug(s"[${requestId.show}] JWT resolved user for claim ${settings.jwt.userClaim.name.rawPath}: ${user.show}") + } + def withDisabledCallsToExternalAuthenticationService = new JwtAuthenticationRule(settings, userIdCaseSensitivity, disabledCallsToExternalAuthenticationService = true) @@ -68,5 +77,5 @@ object JwtAuthenticationRule { override val name = Rule.Name("jwt_authentication") } - final case class Settings(jwt: JwtDef) + final case class Settings(jwt: JwtDefForAuthentication) } diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthorizationRule.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthorizationRule.scala index 9d405039a4..408ec85966 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthorizationRule.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtAuthorizationRule.scala @@ -17,16 +17,17 @@ package tech.beshu.ror.accesscontrol.blocks.rules.auth import monix.eval.Task -import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef +import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDefForAuthorization import tech.beshu.ror.accesscontrol.blocks.rules.Rule import tech.beshu.ror.accesscontrol.blocks.rules.Rule.{AuthorizationRule, RuleName, RuleResult} import tech.beshu.ror.accesscontrol.blocks.rules.auth.JwtAuthorizationRule.Settings import tech.beshu.ror.accesscontrol.blocks.rules.auth.base.BaseJwtRule import tech.beshu.ror.accesscontrol.blocks.rules.auth.base.impersonation.AuthorizationImpersonationCustomSupport import tech.beshu.ror.accesscontrol.blocks.{BlockContext, BlockContextUpdater} -import tech.beshu.ror.accesscontrol.domain.{Group, GroupIds, GroupsLogic} -import tech.beshu.ror.accesscontrol.utils.ClaimsOps.ClaimSearchResult +import tech.beshu.ror.accesscontrol.domain.* import tech.beshu.ror.accesscontrol.utils.ClaimsOps.ClaimSearchResult.* +import tech.beshu.ror.accesscontrol.utils.ClaimsOps.{ClaimSearchResult, toClaimsOps} +import tech.beshu.ror.implicits.* import tech.beshu.ror.utils.uniquelist.{UniqueList, UniqueNonEmptyList} final class JwtAuthorizationRule(val settings: Settings) @@ -39,8 +40,8 @@ final class JwtAuthorizationRule(val settings: Settings) override protected[rules] def authorize[B <: BlockContext : BlockContextUpdater](blockContext: B): Task[RuleResult[B]] = { settings.groupsLogic match { case groupsLogic if blockContext.isCurrentGroupPotentiallyEligible(groupsLogic) => - processUsingJwtToken(blockContext, settings.jwt) { tokenData => - authorize(blockContext, tokenData.groups, groupsLogic) + processUsingJwtToken(blockContext, settings.jwt) { payload => + authorize(blockContext, payload, groupsLogic) } case _ => Task.now(RuleResult.Rejected()) @@ -48,12 +49,15 @@ final class JwtAuthorizationRule(val settings: Settings) } private def authorize[B <: BlockContext : BlockContextUpdater](blockContext: B, - result: Option[ClaimSearchResult[UniqueList[Group]]], + payload: Jwt.Payload, groupsLogic: GroupsLogic) = { + val groupsConfig = settings.jwt.groupsConfig + val result = payload.claims.groupsClaim(groupsConfig.idsClaim, groupsConfig.namesClaim) + logClaimSearchResults(blockContext, result) result match { - case None | Some(NotFound) => + case NotFound => Left(()) - case Some(Found(groups)) => + case Found(groups) => (for { nonEmptyGroups <- UniqueNonEmptyList.from(groups) matchedGroups <- groupsLogic.availableGroupsFrom(nonEmptyGroups) @@ -62,6 +66,16 @@ final class JwtAuthorizationRule(val settings: Settings) } } + private def logClaimSearchResults[B <: BlockContext](blockContext: B, + groups: ClaimSearchResult[UniqueList[Group]]): Unit = { + implicit val requestId: RequestId = blockContext.requestContext.id.toRequestId + val claimsDescription = settings.jwt.groupsConfig.namesClaim match { + case Some(namesClaim) => s"claims (id:'${settings.jwt.groupsConfig.idsClaim.name.show}',name:'${namesClaim.name.show}')" + case None => s"claim '${settings.jwt.groupsConfig.idsClaim.name.show}'" + } + logger.debug(s"[${requestId.show}] JWT resolved groups for ${claimsDescription.show}: ${groups.show}") + } + } object JwtAuthorizationRule { @@ -70,5 +84,5 @@ object JwtAuthorizationRule { override val name = Rule.Name("jwt_authorization") } - final case class Settings(jwt: JwtDef, groupsLogic: GroupsLogic) + final case class Settings(jwt: JwtDefForAuthorization, groupsLogic: GroupsLogic) } diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtPseudoAuthorizationRule.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtPseudoAuthorizationRule.scala deleted file mode 100644 index 8153029e39..0000000000 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/JwtPseudoAuthorizationRule.scala +++ /dev/null @@ -1,64 +0,0 @@ -/* - * This file is part of ReadonlyREST. - * - * ReadonlyREST is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ReadonlyREST is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ - */ -package tech.beshu.ror.accesscontrol.blocks.rules.auth - -import monix.eval.Task -import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef -import tech.beshu.ror.accesscontrol.blocks.rules.Rule -import tech.beshu.ror.accesscontrol.blocks.rules.Rule.{AuthorizationRule, RuleResult} -import tech.beshu.ror.accesscontrol.blocks.rules.auth.JwtPseudoAuthorizationRule.Settings -import tech.beshu.ror.accesscontrol.blocks.rules.auth.base.BaseJwtRule -import tech.beshu.ror.accesscontrol.blocks.rules.auth.base.impersonation.AuthorizationImpersonationCustomSupport -import tech.beshu.ror.accesscontrol.blocks.{BlockContext, BlockContextUpdater} -import tech.beshu.ror.accesscontrol.domain.{Group, GroupIds} -import tech.beshu.ror.accesscontrol.utils.ClaimsOps.ClaimSearchResult -import tech.beshu.ror.accesscontrol.utils.ClaimsOps.ClaimSearchResult.* -import tech.beshu.ror.utils.uniquelist.{UniqueList, UniqueNonEmptyList} - -// Pseudo-authorization rule should be used exclusively as part of the JwtAuthRule, when there are is no groups logic defined. -// It preserves the kbn_auth rule behavior from before introducing separate authn and authz rules. -final class JwtPseudoAuthorizationRule(val settings: Settings) - extends AuthorizationRule - with AuthorizationImpersonationCustomSupport - with BaseJwtRule { - - override val name: Rule.Name = JwtAuthorizationRule.Name.name - - override protected[rules] def authorize[B <: BlockContext : BlockContextUpdater](blockContext: B): Task[RuleResult[B]] = { - processUsingJwtToken(blockContext, settings.jwt) { tokenData => - pseudoAuthorize(blockContext, tokenData.groups) - } - } - - private def pseudoAuthorize[B <: BlockContext](blockContext: B, - result: Option[ClaimSearchResult[UniqueList[Group]]]) = { - result match { - case None | Some(NotFound) => - Right(blockContext) - case Some(Found(groups)) => - (for { - nonEmptyGroups <- UniqueNonEmptyList.from(groups) - if blockContext.isCurrentGroupEligible(GroupIds.from(nonEmptyGroups)) - } yield blockContext).toRight(()) - } - } - -} - -object JwtPseudoAuthorizationRule { - final case class Settings(jwt: JwtDef) -} diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/base/BaseJwtRule.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/base/BaseJwtRule.scala index a502737137..b9a1ff9d14 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/base/BaseJwtRule.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/rules/auth/base/BaseJwtRule.scala @@ -26,38 +26,34 @@ import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef.SignatureCheckMethod.{Ec, Hmac, NoCheck, Rsa} import tech.beshu.ror.accesscontrol.blocks.rules.Rule.RuleResult import tech.beshu.ror.accesscontrol.blocks.rules.Rule.RuleResult.{Fulfilled, Rejected} -import tech.beshu.ror.accesscontrol.blocks.rules.auth.base.BaseJwtRule.* import tech.beshu.ror.accesscontrol.domain.* import tech.beshu.ror.accesscontrol.request.RequestContext import tech.beshu.ror.accesscontrol.request.RequestContextOps.from -import tech.beshu.ror.accesscontrol.utils.ClaimsOps.* import tech.beshu.ror.implicits.* import tech.beshu.ror.utils.RefinedUtils.nes -import tech.beshu.ror.utils.uniquelist.UniqueList import scala.util.Try trait BaseJwtRule extends Logging { - protected def processUsingJwtToken[B <: BlockContext](blockContext: B, - jwt: JwtDef, - disabledCallsToExternalAuthenticationService: Boolean = false) - (operation: JwtData => Either[Unit, B]): Task[RuleResult[B]] = { - implicit val jwtImpl: JwtDef = jwt + protected def processUsingJwtToken[ + B <: BlockContext, JWT_DEF <: JwtDef + ](blockContext: B, + jwt: JWT_DEF, + disabledCallsToExternalAuthenticationService: Boolean = false) + (operation: Jwt.Payload => Either[Unit, B]): Task[RuleResult[B]] = { + implicit val jwtImpl: JWT_DEF = jwt jwtTokenFrom(blockContext.requestContext) match { case None => logger.debug(s"[${blockContext.requestContext.id.show}] Authorization header '${jwt.authorizationTokenDef.headerName.show}' is missing or does not contain a JWT token") Task.now(Rejected()) case Some(token) => implicit val requestId: RequestId = blockContext.requestContext.id.toRequestId - userAndGroupsFromJwtToken(token) match { + claimsFrom(token) match { case Left(_) => Task.now(Rejected()) - case Right(jwtData) => - if (logger.delegate.isDebugEnabled) { - logClaimSearchResults(jwtData.userId, jwtData.groups) - } - val claimProcessingResult = operation(jwtData) + case Right(jwtPayload) => + val claimProcessingResult = operation(jwtPayload) claimProcessingResult match { case Left(_) => Task.now(Rejected()) @@ -75,39 +71,12 @@ trait BaseJwtRule extends Logging { } } - private def jwtTokenFrom(requestContext: RequestContext)(implicit jwt: JwtDef) = { + private def jwtTokenFrom[JWT_DEF <: JwtDef](requestContext: RequestContext)(implicit jwt: JWT_DEF) = { requestContext .authorizationToken(jwt.authorizationTokenDef) .map(t => Jwt.Token(t.value)) } - private def logClaimSearchResults(user: Option[ClaimSearchResult[User.Id]], - groups: Option[ClaimSearchResult[UniqueList[Group]]]) - (implicit requestId: RequestId, jwt: JwtDef): Unit = { - (jwt.userClaim, user) match { - case (Some(userClaim), Some(u)) => - logger.debug(s"[${requestId.show}] JWT resolved user for claim ${userClaim.name.rawPath}: ${u.show}") - case _ => - } - (jwt.groupsConfig, groups) match { - case (Some(groupsConfig), Some(g)) => - val claimsDescription = groupsConfig.namesClaim match { - case Some(namesClaim) => s"claims (id:'${groupsConfig.idsClaim.name.show}',name:'${namesClaim.name.show}')" - case None => s"claim '${groupsConfig.idsClaim.name.show}'" - } - logger.debug(s"[${requestId.show}] JWT resolved groups for ${claimsDescription.show}: ${g.show}") - case _ => - } - } - - private def userAndGroupsFromJwtToken(token: Jwt.Token) - (implicit requestId: RequestId, - jwt: JwtDef): Either[Unit, JwtData] = { - claimsFrom(token).map { decodedJwtToken => - JwtData(decodedJwtToken, userIdFrom(decodedJwtToken), groupsFrom(decodedJwtToken)) - } - } - private def logBadToken(ex: Throwable, token: Jwt.Token) (implicit requestId: RequestId): Unit = { val tokenParts = token.show.split("\\.") @@ -121,9 +90,9 @@ trait BaseJwtRule extends Logging { logger.debug(s"[${requestId.show}] JWT token '${printableToken.show}' parsing error: ${ex.getClass.getSimpleName.show} ${ex.getMessage.show}") } - private def claimsFrom(token: Jwt.Token) - (implicit requestId: RequestId, - jwt: JwtDef) = { + private def claimsFrom[JWT_DEF <: JwtDef](token: Jwt.Token) + (implicit requestId: RequestId, + jwt: JWT_DEF) = { val parser = jwt.checkMethod match { case NoCheck(_) => Jwts.parser().unsecured().build() case Hmac(rawKey) => Jwts.parser().verifyWith(Keys.hmacShaKeyFor(rawKey)).build() @@ -149,21 +118,4 @@ trait BaseJwtRule extends Logging { } } - private def userIdFrom(payload: Jwt.Payload)(implicit jwt: JwtDef): Option[ClaimSearchResult[User.Id]] = { - jwt.userClaim.map(payload.claims.userIdClaim) - } - - private def groupsFrom(payload: Jwt.Payload)(implicit jwt: JwtDef): Option[ClaimSearchResult[UniqueList[Group]]] = { - jwt.groupsConfig.map(groupsConfig => - payload.claims.groupsClaim(groupsConfig.idsClaim, groupsConfig.namesClaim) - ) - } -} - -object BaseJwtRule { - - protected final case class JwtData(payload: Jwt.Payload, - userId: Option[ClaimSearchResult[User.Id]], - groups: Option[ClaimSearchResult[UniqueList[Group]]]) - } diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/definitions/DefinitionsPack.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/definitions/DefinitionsPack.scala index ef7430eaf2..0fb7809624 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/definitions/DefinitionsPack.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/definitions/DefinitionsPack.scala @@ -31,7 +31,7 @@ final case class DefinitionsPack(proxies: Definitions[ProxyAuth], impersonators: Definitions[ImpersonatorDef], variableTransformationAliases: Definitions[VariableTransformationAliasDef]) -final case class Definitions[ITEM <: Item](items: List[ITEM]) extends AnyVal +final case class Definitions[+ITEM <: Item](items: List[ITEM]) extends AnyVal object Definitions { trait Item { type Id diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/definitions/JwtDefinitionsDecoder.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/definitions/JwtDefinitionsDecoder.scala index e178cb3893..89206b358e 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/definitions/JwtDefinitionsDecoder.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/definitions/JwtDefinitionsDecoder.scala @@ -18,8 +18,8 @@ package tech.beshu.ror.accesscontrol.factory.decoders.definitions import cats.Id import io.circe.{Decoder, HCursor, Json} +import tech.beshu.ror.accesscontrol.blocks.definitions.* import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef.{GroupsConfig, Name, SignatureCheckMethod} -import tech.beshu.ror.accesscontrol.blocks.definitions.{ExternalAuthenticationService, JwtDef} import tech.beshu.ror.accesscontrol.blocks.variables.runtime.RuntimeResolvableVariableCreator import tech.beshu.ror.accesscontrol.domain.{AuthorizationTokenDef, Header, Jwt} import tech.beshu.ror.accesscontrol.factory.HttpClientsFactory @@ -29,7 +29,7 @@ import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCre import tech.beshu.ror.accesscontrol.factory.decoders.common.* import tech.beshu.ror.accesscontrol.factory.decoders.definitions.ExternalAuthenticationServicesDecoder.jwtExternalAuthenticationServiceDecoder import tech.beshu.ror.accesscontrol.utils.CirceOps.* -import tech.beshu.ror.accesscontrol.utils.CirceOps.DecodingFailureOps.fromError +import tech.beshu.ror.accesscontrol.utils.CirceOps.DecodingFailureUtils.fromError import tech.beshu.ror.accesscontrol.utils.CryptoOps.keyStringToPublicKey import tech.beshu.ror.accesscontrol.utils.{ADecoder, SyncDecoder, SyncDecoderCreator} import tech.beshu.ror.implicits.* @@ -47,24 +47,57 @@ object JwtDefinitionsDecoder { private def jwtDefDecoder(implicit httpClientFactory: HttpClientsFactory, variableCreator: RuntimeResolvableVariableCreator): Decoder[JwtDef] = { SyncDecoderCreator - .instance { c => + .instance[JwtDef] { c => for { name <- c.downField("name").as[Name] checkMethod <- signatureCheckMethod(c) headerName <- c.downField("header_name").as[Option[Header.Name]] authTokenPrefix <- c.downField("header_prefix").as[Option[String]] - userClaim <- c.downField("user_claim").as[Option[Jwt.ClaimName]] - groupsConfig <- c.as[Option[GroupsConfig]] - } yield JwtDef( - id = name, - authorizationTokenDef = AuthorizationTokenDef( - headerName.getOrElse(Header.Name.authorization), - authTokenPrefix.getOrElse("Bearer ") - ), - checkMethod = checkMethod, - userClaim = userClaim, - groupsConfig = groupsConfig - ) + userClaimOpt <- c.downField("user_claim").as[Option[Jwt.ClaimName]] + groupsConfigOpt <- c.as[Option[GroupsConfig]](groupsConfigOptDecoder) + jwtDef <- (userClaimOpt, groupsConfigOpt) match { + case (Some(userClaim), Some(groupsConfig)) => + Right( + AuthJwtDef( + id = name, + authorizationTokenDef = AuthorizationTokenDef( + headerName.getOrElse(Header.Name.authorization), + authTokenPrefix.getOrElse("Bearer ") + ), + checkMethod = checkMethod, + userClaim = userClaim, + groupsConfig = groupsConfig, + ): JwtDef + ) + case (Some(userClaim), None) => + Right( + AuthenticationJwtDef( + id = name, + authorizationTokenDef = AuthorizationTokenDef( + headerName.getOrElse(Header.Name.authorization), + authTokenPrefix.getOrElse("Bearer ") + ), + checkMethod = checkMethod, + userClaim = userClaim, + ): JwtDef + ) + case (None, Some(groupsConfig)) => + Right( + AuthorizationJwtDef( + id = name, + authorizationTokenDef = AuthorizationTokenDef( + headerName.getOrElse(Header.Name.authorization), + authTokenPrefix.getOrElse("Bearer ") + ), + checkMethod = checkMethod, + groupsConfig = groupsConfig, + ): JwtDef + ) + case (None, None) => + val message = s"JWT definition ${name.show} must contain 'user_claim' setting to be used with jwt_authentication rule, 'group_ids_claim' to be used with jwt_authorization rule, or both of them in order to be used with jwt_auth rule." + Left(fromError(CoreCreationError.DefinitionsLevelCreationError(Message(message)))) + } + } yield jwtDef } .mapError(DefinitionsLevelCreationError.apply) .decoder @@ -137,7 +170,7 @@ object JwtDefinitionsDecoder { private implicit val claimDecoder: Decoder[Jwt.ClaimName] = jsonPathDecoder.map(Jwt.ClaimName.apply) - private implicit val groupsConfigDecoder: Decoder[Option[GroupsConfig]] = Decoder.instance { c => + val groupsConfigOptDecoder: Decoder[Option[GroupsConfig]] = Decoder.instance { c => for { groupIdsClaim <- c.downFields("roles_claim", "groups_claim", "group_ids_claim").as[Option[Jwt.ClaimName]] diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/ruleDecoders.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/ruleDecoders.scala index 36042b2ae2..96ec44a6e3 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/ruleDecoders.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/ruleDecoders.scala @@ -128,25 +128,28 @@ object ruleDecoders { impersonatorsDefinitions: Option[Definitions[ImpersonatorDef]], mocksProvider: MocksProvider, globalSettings: GlobalSettings): Option[RuleDecoder[Rule]] = { - val optionalRuleDecoder = name match { + lazy val optionalRuleDecoder = name match { case ExternalAuthorizationRule.Name.name => Some(new ExternalAuthorizationRuleDecoder(authorizationServiceDefinitions, impersonatorsDefinitions, mocksProvider, globalSettings)) case JwtAuthRule.Name.name => - Some(new JwtAuthRulesDecoders.AuthRuleDecoder(jwtDefinitions, globalSettings)) + val definitions = jwtDefinitions.items.collect {case definition: JwtDefForAuth => definition} + Some(new JwtAuthRulesDecoders.AuthRuleDecoder(jwtDefinitions.items, definitions, globalSettings)) case JwtAuthenticationRule.Name.name => - Some(new JwtAuthRulesDecoders.AuthenticationRuleDecoder(jwtDefinitions, globalSettings)) + val definitions = jwtDefinitions.items.collect {case definition: JwtDefForAuthentication => definition} + Some(new JwtAuthRulesDecoders.AuthenticationRuleDecoder(jwtDefinitions.items, definitions, globalSettings)) case JwtAuthorizationRule.Name.name => - Some(new JwtAuthRulesDecoders.AuthorizationRuleDecoder(jwtDefinitions)) + val definitions = jwtDefinitions.items.collect {case definition: JwtDefForAuthorization => definition} + Some(new JwtAuthRulesDecoders.AuthorizationRuleDecoder(jwtDefinitions.items, definitions)) case LdapAuthorizationRule.Name.name => Some(new LdapAuthorizationRuleDecoder(ldapServiceDefinitions, impersonatorsDefinitions, mocksProvider, globalSettings)) case LdapAuthRule.Name.name => Some(new LdapAuthRuleDecoder(ldapServiceDefinitions, impersonatorsDefinitions, mocksProvider, globalSettings)) case RorKbnAuthRule.Name.name => - Some(new RorKbnRulesDecoders.AuthRuleDecoder(rorKbnDefinitions, globalSettings)) + Some(new RorKbnRulesDecoders.AuthRuleDecoder(rorKbnDefinitions.items, rorKbnDefinitions.items, globalSettings)) case RorKbnAuthenticationRule.Name.name => - Some(new RorKbnRulesDecoders.AuthenticationRuleDecoder(rorKbnDefinitions, globalSettings)) + Some(new RorKbnRulesDecoders.AuthenticationRuleDecoder(rorKbnDefinitions.items, rorKbnDefinitions.items, globalSettings)) case RorKbnAuthorizationRule.Name.name => - Some(new RorKbnRulesDecoders.AuthorizationRuleDecoder(rorKbnDefinitions)) + Some(new RorKbnRulesDecoders.AuthorizationRuleDecoder(rorKbnDefinitions.items, rorKbnDefinitions.items)) case _ => authenticationRuleDecoderBy( name, diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtAuthRulesDecoders.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtAuthRulesDecoders.scala index 2c0aeab65f..d5493d7a12 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtAuthRulesDecoders.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtAuthRulesDecoders.scala @@ -17,38 +17,38 @@ package tech.beshu.ror.accesscontrol.factory.decoders.rules.auth import org.apache.logging.log4j.scala.Logging -import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef -import tech.beshu.ror.accesscontrol.blocks.rules.auth.{JwtAuthRule, JwtAuthenticationRule, JwtAuthorizationRule, JwtPseudoAuthorizationRule} +import tech.beshu.ror.accesscontrol.blocks.definitions.* +import tech.beshu.ror.accesscontrol.blocks.rules.auth.{JwtAuthRule, JwtAuthenticationRule, JwtAuthorizationRule} import tech.beshu.ror.accesscontrol.domain.GroupsLogic import tech.beshu.ror.accesscontrol.factory.GlobalSettings object JwtAuthRulesDecoders extends JwtLikeRulesDecoders[ + JwtDef, + JwtDefForAuthentication, + JwtDefForAuthorization, + JwtDefForAuth, JwtAuthenticationRule, JwtAuthorizationRule, - JwtPseudoAuthorizationRule, JwtAuthRule, - JwtDef, ] with Logging { - override def humanReadableName: String = "JWT" + override protected def ruleTypePrefix: String = "jwt" + + override protected def docsUrl: String = "https://docs.readonlyrest.com/elasticsearch#json-web-token-jwt-auth" - override def createAuthenticationRule(definition: JwtDef, globalSettings: GlobalSettings): JwtAuthenticationRule = + override protected def createAuthenticationRule(definition: JwtDefForAuthentication, + globalSettings: GlobalSettings): JwtAuthenticationRule = new JwtAuthenticationRule(JwtAuthenticationRule.Settings(definition), globalSettings.userIdCaseSensitivity) - override def createAuthorizationRule(definition: JwtDef, groupsLogic: GroupsLogic): JwtAuthorizationRule = + override protected def createAuthorizationRule(definition: JwtDefForAuthorization, + groupsLogic: GroupsLogic): JwtAuthorizationRule = new JwtAuthorizationRule(JwtAuthorizationRule.Settings(definition, groupsLogic)) - override def createAuthorizationRuleWithoutGroups(definition: JwtDef): JwtPseudoAuthorizationRule = - new JwtPseudoAuthorizationRule(JwtPseudoAuthorizationRule.Settings(definition)) - override def createAuthRule(authnRule: JwtAuthenticationRule, authzRule: JwtAuthorizationRule): JwtAuthRule = new JwtAuthRule(authnRule, authzRule) - override def createAuthRuleWithoutGroups(authnRule: JwtAuthenticationRule, authzRule: JwtPseudoAuthorizationRule): JwtAuthRule = - new JwtAuthRule(authnRule, authzRule) - - override def serializeDefinitionId(definition: JwtDef): String = + override protected def serializeDefinitionId(definition: JwtDef): String = definition.id.value.value } diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtLikeRulesDecoders.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtLikeRulesDecoders.scala index d269fffa19..3ff11bc495 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtLikeRulesDecoders.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtLikeRulesDecoders.scala @@ -25,7 +25,8 @@ import tech.beshu.ror.accesscontrol.blocks.rules.Rule import tech.beshu.ror.accesscontrol.blocks.rules.Rule.{AuthRule, AuthenticationRule, AuthorizationRule, RuleName} import tech.beshu.ror.accesscontrol.blocks.users.LocalUsersContext.LocalUsersSupport import tech.beshu.ror.accesscontrol.blocks.variables.runtime.VariableContext.VariableUsage -import tech.beshu.ror.accesscontrol.domain.GroupsLogic +import tech.beshu.ror.accesscontrol.domain.GroupIdLike.GroupIdPattern +import tech.beshu.ror.accesscontrol.domain.{GroupIds, GroupsLogic} import tech.beshu.ror.accesscontrol.factory.GlobalSettings import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.Reason.Message import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.RulesLevelCreationError @@ -36,109 +37,139 @@ import tech.beshu.ror.accesscontrol.factory.decoders.rules.auth.groups.GroupsLog import tech.beshu.ror.accesscontrol.factory.decoders.rules.auth.groups.GroupsLogicRepresentationDecoder.GroupsLogicDecodingResult import tech.beshu.ror.accesscontrol.utils.CirceOps.* import tech.beshu.ror.implicits.* +import tech.beshu.ror.utils.RefinedUtils.nes +import tech.beshu.ror.utils.uniquelist.UniqueNonEmptyList // Common decoder for JWT rules and ROR KBN rules. They are very similar, and their decoding logic is mostly the same. trait JwtLikeRulesDecoders[ + DEF <: Definitions.Item, + AUTHN_DEF <: DEF, + AUTHZ_DEF <: DEF, + AUTH_DEF <: AUTHN_DEF & AUTHZ_DEF, AUTHN_RULE <: AuthenticationRule : RuleName : VariableUsage : LocalUsersSupport : ImpersonationWarningSupport, AUTHZ_RULE <: AuthorizationRule : RuleName : VariableUsage : LocalUsersSupport : ImpersonationWarningSupport, - AUTHZ_WITHOUT_GROUPS_RULE <: AuthorizationRule, AUTH_RULE <: AuthRule : RuleName : VariableUsage : LocalUsersSupport : ImpersonationWarningSupport, - DEFINITION <: Definitions.Item, ] { this: Logging => - def humanReadableName: String + protected def ruleTypePrefix: String - def createAuthenticationRule(definition: DEFINITION, - globalSettings: GlobalSettings): AUTHN_RULE + protected def docsUrl: String - def createAuthorizationRule(definition: DEFINITION, - groupsLogic: GroupsLogic): AUTHZ_RULE + protected def createAuthenticationRule(definition: AUTHN_DEF, + globalSettings: GlobalSettings): AUTHN_RULE - def createAuthRule(authnRule: AUTHN_RULE, - authzRule: AUTHZ_RULE): AUTH_RULE + protected def createAuthorizationRule(definition: AUTHZ_DEF, + groupsLogic: GroupsLogic): AUTHZ_RULE - def createAuthorizationRuleWithoutGroups(definition: DEFINITION): AUTHZ_WITHOUT_GROUPS_RULE + protected def createAuthRule(authnRule: AUTHN_RULE, + authzRule: AUTHZ_RULE): AUTH_RULE - def createAuthRuleWithoutGroups(authnRule: AUTHN_RULE, - authzRule: AUTHZ_WITHOUT_GROUPS_RULE): AUTH_RULE + protected def serializeDefinitionId(definition: DEF): String - def serializeDefinitionId(definition: DEFINITION): String - - class AuthenticationRuleDecoder(definitions: Definitions[DEFINITION], + class AuthenticationRuleDecoder(allDefs: List[DEF], + authnDefs: List[AUTHN_DEF], globalSettings: GlobalSettings) extends RuleBaseDecoderWithoutAssociatedFields[AUTHN_RULE] { override protected def decoder: Decoder[RuleDefinition[AUTHN_RULE]] = nameAndGroupsSimpleDecoder .or(nameAndGroupsExtendedDecoder[AUTHN_RULE]) .toSyncDecoder .emapE { case (name, groupsLogicOpt) => - val definitionOpt = definitions.items.find(d => serializeDefinitionId(d) == name) - (definitionOpt, groupsLogicOpt) match { - case (Some(_), Some(_)) => + val definitionE = findDefinition[AUTHN_RULE, AUTHN_DEF](authnDefs, allDefs.diff(authnDefs), name) + (definitionE, groupsLogicOpt) match { + case (Right(_), Some(_)) => Left(RulesLevelCreationError(Message(s"Cannot create ${RuleName[AUTHN_RULE].name.show}, because there are superfluous groups settings. Remove the groups settings, or use ${RuleName[AUTHZ_RULE].name.show} or ${RuleName[AUTH_RULE].name.show} rule, if group settings are required."))) - case (Some(definition), None) => + case (Right(definition), None) => val rule = createAuthenticationRule(definition, globalSettings) Right(RuleDefinition.create(rule)) - case (None, _) => - Left(cannotFindDefinition(name)) + case (Left(error), _) => + Left(error) } } .decoder } - class AuthorizationRuleDecoder(definitions: Definitions[DEFINITION]) extends RuleBaseDecoderWithoutAssociatedFields[AUTHZ_RULE] { + class AuthorizationRuleDecoder(allDefs: List[DEF], + authzDefs: List[AUTHZ_DEF]) extends RuleBaseDecoderWithoutAssociatedFields[AUTHZ_RULE] { override protected def decoder: Decoder[RuleDefinition[AUTHZ_RULE]] = nameAndGroupsSimpleDecoder .or(nameAndGroupsExtendedDecoder[AUTHZ_RULE]) .toSyncDecoder .emapE { case (name, groupsLogicOpt) => - val definitionOpt = definitions.items.find(d => serializeDefinitionId(d) == name) - (definitionOpt, groupsLogicOpt) match { - case (Some(definition), Some(groupsLogic)) => + val definitionE = findDefinition[AUTHZ_RULE, AUTHZ_DEF](authzDefs, allDefs.diff(authzDefs), name) + (definitionE, groupsLogicOpt) match { + case (Right(definition), Some(groupsLogic)) => val rule = createAuthorizationRule(definition, groupsLogic) Right(RuleDefinition.create[AUTHZ_RULE](rule)) - case (Some(_), None) => + case (Right(_), None) => Left(RulesLevelCreationError(Message(s"Cannot create ${RuleName[AUTHZ_RULE].name.show} - missing groups logic (https://github.com/beshu-tech/readonlyrest-docs/blob/master/details/authorization-rules-details.md#checking-groups-logic)"))) - case (None, _) => - Left(cannotFindDefinition(name)) + case (Left(error), _) => + Left(error) } } .decoder } - class AuthRuleDecoder(definitions: Definitions[DEFINITION], + class AuthRuleDecoder(allDefs: List[DEF], + authDefs: List[AUTH_DEF], globalSettings: GlobalSettings) extends RuleBaseDecoderWithoutAssociatedFields[AUTH_RULE] { override protected def decoder: Decoder[RuleDefinition[AUTH_RULE]] = nameAndGroupsSimpleDecoder .or(nameAndGroupsExtendedDecoder[AUTH_RULE]) .toSyncDecoder .emapE { case (name, groupsLogicOpt) => - val definitionOpt = definitions.items.find(d => serializeDefinitionId(d) == name) - (definitionOpt, groupsLogicOpt) match { - case (Some(definition), Some(groupsLogic)) => + val definitionE = findDefinition[AUTH_RULE, AUTH_DEF](authDefs, allDefs.diff(authDefs), name) + (definitionE, groupsLogicOpt) match { + case (Right(definition), Some(groupsLogic)) => val authentication = createAuthenticationRule(definition, globalSettings) val authorization = createAuthorizationRule(definition, groupsLogic) val rule = createAuthRule(authentication, authorization) Right(RuleDefinition.create(rule)) - case (Some(definition), None) => + case (Right(definition), None) => logger.warn( s"""Missing groups logic settings in ${RuleName[AUTH_RULE].name.show} rule. - |For old configs, ROR treats this as `groups_any_of: ["*"]`. + |For old configs, ROR treats this as `gr oups_any_of: ["*"]`. |This syntax is deprecated. Add groups logic (https://github.com/beshu-tech/readonlyrest-docs/blob/master/details/authorization-rules-details.md#checking-groups-logic), |or use ${RuleName[AUTHN_RULE].name.show} if you only need authentication. |""".stripMargin ) val authentication = createAuthenticationRule(definition, globalSettings) - val authorization = createAuthorizationRuleWithoutGroups(definition) - val rule = createAuthRuleWithoutGroups(authentication, authorization) + val groupsLogic = GroupsLogic.AnyOf(GroupIds(UniqueNonEmptyList.of(GroupIdPattern.fromNes(nes("*"))))) + val authorization = createAuthorizationRule(definition, groupsLogic) + val rule = createAuthRule(authentication, authorization) Right(RuleDefinition.create(rule)) - case (None, _) => - Left(cannotFindDefinition(name)) + case (Left(error), _) => + Left(error) } } .decoder } + private def findDefinition[T <: Rule, CURRENT_DEF <: DEF](definitionsOfCurrentType: List[CURRENT_DEF], + definitionsOfOtherType: List[DEF], + name: String) + (implicit ruleName: RuleName[T]): Either[RulesLevelCreationError, CURRENT_DEF] = { + val definitionOfCurrentTypeOpt = findByName(definitionsOfCurrentType, name) + lazy val definitionOfOtherTypeOpt = findByName(definitionsOfOtherType, name) + definitionOfCurrentTypeOpt match { + case Some(definition) => + Right(definition) + case None => + val message = definitionOfOtherTypeOpt match { + case Some(_) => + s"The $ruleTypePrefix definition with name $name exists, but cannot be used for ${ruleName.name.show} rule." + + s"Please check in the documentation ($docsUrl) how to adjust the $ruleTypePrefix definition to use it for both authentication and authorization" + case None => + s"Cannot find $ruleTypePrefix definition with name: $name" + } + Left(RulesLevelCreationError(Message(message))) + } + } + + private def findByName[T <: DEF](definitions: List[T], name: String): Option[T] = { + definitions.find(d => serializeDefinitionId(d) == name) + } + private def nameAndGroupsSimpleDecoder: Decoder[(String, Option[GroupsLogic])] = DecoderHelpers .decodeStringLikeNonEmpty @@ -164,13 +195,10 @@ trait JwtLikeRulesDecoders[ case GroupsLogicDecodingResult.MultipleGroupsLogicsDefined(_, fields) => val fieldsStr = fields.map(f => s"'$f'").mkString(" or ") Left(RulesLevelCreationError(Message( - s"Please specify either $fieldsStr for $humanReadableName authorization rule '$name'" + s"Please specify either $fieldsStr for ${ruleTypePrefix}_authorization rule '$name'" ))) } } .decoder - private def cannotFindDefinition(name: String) = - RulesLevelCreationError(Message(s"Cannot find $humanReadableName definition with name: $name")) - } diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/RorKbnRulesDecoders.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/RorKbnRulesDecoders.scala index 2f4e3b7860..ad2f96b2c8 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/RorKbnRulesDecoders.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/RorKbnRulesDecoders.scala @@ -19,39 +19,34 @@ package tech.beshu.ror.accesscontrol.factory.decoders.rules.auth import org.apache.logging.log4j.scala.Logging import tech.beshu.ror.accesscontrol.blocks.definitions.RorKbnDef import tech.beshu.ror.accesscontrol.blocks.rules.auth.{RorKbnAuthRule, RorKbnAuthenticationRule, RorKbnAuthorizationRule} -import tech.beshu.ror.accesscontrol.domain.GroupIdLike.GroupIdPattern -import tech.beshu.ror.accesscontrol.domain.{GroupIds, GroupsLogic} +import tech.beshu.ror.accesscontrol.domain.GroupsLogic import tech.beshu.ror.accesscontrol.factory.GlobalSettings -import tech.beshu.ror.utils.RefinedUtils.nes -import tech.beshu.ror.utils.uniquelist.UniqueNonEmptyList object RorKbnRulesDecoders extends JwtLikeRulesDecoders[ + RorKbnDef, + RorKbnDef, + RorKbnDef, + RorKbnDef, RorKbnAuthenticationRule, RorKbnAuthorizationRule, - RorKbnAuthorizationRule, RorKbnAuthRule, - RorKbnDef, ] with Logging { - override def humanReadableName: String = "ROR Kibana" + override protected def ruleTypePrefix: String = "ror_kbn" + + override protected def docsUrl: String = "https://docs.readonlyrest.com/elasticsearch?q=ror#ror_kbn_auth" - override def createAuthenticationRule(definition: RorKbnDef, globalSettings: GlobalSettings): RorKbnAuthenticationRule = + override protected def createAuthenticationRule(definition: RorKbnDef, globalSettings: GlobalSettings): RorKbnAuthenticationRule = new RorKbnAuthenticationRule(RorKbnAuthenticationRule.Settings(definition), globalSettings.userIdCaseSensitivity) - override def createAuthorizationRule(definition: RorKbnDef, groupsLogic: GroupsLogic): RorKbnAuthorizationRule = + override protected def createAuthorizationRule(definition: RorKbnDef, groupsLogic: GroupsLogic): RorKbnAuthorizationRule = new RorKbnAuthorizationRule(RorKbnAuthorizationRule.Settings(definition, groupsLogic)) - override def createAuthorizationRuleWithoutGroups(definition: RorKbnDef): RorKbnAuthorizationRule = - createAuthorizationRule(definition, GroupsLogic.AnyOf(GroupIds(UniqueNonEmptyList.of(GroupIdPattern.fromNes(nes("*")))))) - - override def createAuthRule(authnRule: RorKbnAuthenticationRule, authzRule: RorKbnAuthorizationRule): RorKbnAuthRule = - new RorKbnAuthRule(authnRule, authzRule) - - override def createAuthRuleWithoutGroups(authnRule: RorKbnAuthenticationRule, authzRule: RorKbnAuthorizationRule): RorKbnAuthRule = + override protected def createAuthRule(authnRule: RorKbnAuthenticationRule, authzRule: RorKbnAuthorizationRule): RorKbnAuthRule = new RorKbnAuthRule(authnRule, authzRule) - override def serializeDefinitionId(definition: RorKbnDef): String = + override protected def serializeDefinitionId(definition: RorKbnDef): String = definition.id.value.value } diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/utils/CirceOps.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/utils/CirceOps.scala index d7d2d53c4e..33e1ecfa5b 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/utils/CirceOps.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/utils/CirceOps.scala @@ -315,7 +315,11 @@ object CirceOps { } object DecodingFailureOps { + def fromError(error: CoreCreationError): DecodingFailure = + DecodingFailureUtils.fromError(error) + } + object DecodingFailureUtils { import AclCreationErrorCoders.* def fromError(error: CoreCreationError): DecodingFailure = diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthRuleTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthRuleTests.scala index 22dcd22a26..6428e64c8d 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthRuleTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthRuleTests.scala @@ -33,14 +33,15 @@ import tech.beshu.ror.accesscontrol.blocks.definitions.ExternalAuthenticationSer import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef.{GroupsConfig, SignatureCheckMethod} import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata import tech.beshu.ror.accesscontrol.blocks.rules.Rule.RuleResult.{Fulfilled, Rejected} -import tech.beshu.ror.accesscontrol.blocks.rules.auth.{JwtAuthRule, JwtAuthenticationRule, JwtAuthorizationRule, JwtPseudoAuthorizationRule} +import tech.beshu.ror.accesscontrol.blocks.rules.auth.{JwtAuthRule, JwtAuthenticationRule, JwtAuthorizationRule} import tech.beshu.ror.accesscontrol.domain -import tech.beshu.ror.accesscontrol.domain.GroupIdLike.GroupId +import tech.beshu.ror.accesscontrol.domain.GroupIdLike.{GroupId, GroupIdPattern} import tech.beshu.ror.accesscontrol.domain.LoggedUser.DirectlyLoggedUser import tech.beshu.ror.accesscontrol.domain.{Jwt as _, *} import tech.beshu.ror.mocks.MockRequestContext import tech.beshu.ror.syntax.* import tech.beshu.ror.utils.DurationOps.* +import tech.beshu.ror.utils.RefinedUtils.nes import tech.beshu.ror.utils.TestsUtils.* import tech.beshu.ror.utils.WithDummyRequestIdSupport import tech.beshu.ror.utils.misc.JwtUtils.* @@ -59,70 +60,91 @@ class JwtAuthRuleTests "match" when { "token has valid HS256 signature" in { val secret: Key = Jwts.SIG.HS256.key().build() - val jwt = Jwt(secret, claims = List.empty) + val jwt = Jwt(secret, claims = List( + "userId" := "user1", + "groups" := List("group1", "group2") + )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(secret.getEncoded), - userClaim = None, - groupsConfig = None + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), + groupsConfig = GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None) ), tokenHeader = bearerHeader(jwt) ) { blockContext => assertBlockContext( - jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())) + jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())), + loggedUser = Some(DirectlyLoggedUser(User.Id("user1"))), + currentGroup = Some(GroupId("group1")), )(blockContext) } } "token has valid RS256 signature" in { val (pub, secret) = Random.generateRsaRandomKeys - val jwt = Jwt(secret, claims = List.empty) + val jwt = Jwt(secret, claims = List( + "userId" := "user1", + "groups" := List("group1", "group2") + )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Rsa(pub), - userClaim = None, - groupsConfig = None + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), + groupsConfig = GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None) ), tokenHeader = bearerHeader(jwt) ) { blockContext => assertBlockContext( - jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())) + jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())), + loggedUser = Some(DirectlyLoggedUser(User.Id("user1"))), + currentGroup = Some(GroupId("group1")), )(blockContext) } } "token has no signature and external auth service returns true" in { - val jwt = Jwt(claims = List.empty) + val jwt = Jwt(claims = List( + "userId" := "user1", + "groups" := List("group1", "group2") + )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.NoCheck(authService(jwt.stringify(), authenticated = true)), - userClaim = None, - groupsConfig = None + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), + groupsConfig = GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None) ), tokenHeader = bearerHeader(jwt) ) { blockContext => assertBlockContext( - jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())) + jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())), + loggedUser = Some(DirectlyLoggedUser(User.Id("user1"))), + currentGroup = Some(GroupId("group1")), )(blockContext) } } "token has no signature and external auth service state is cached" in { - val validJwt = Jwt(claims = List.empty) - val invalidJwt = Jwt(claims = List("user" := "testuser")) + val validJwt = Jwt(claims = List( + "userId" := "testuser", + "groups" := List("group1", "group2") + )) + val invalidJwt = Jwt(claims = List( + "userId" := "invalid_user", + "groups" := List("group1", "group2") + )) val authService = cachedAuthService(validJwt.stringify(), invalidJwt.stringify()) - val jwtDef = JwtDef( + val jwtDef = AuthJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.NoCheck(authService), - userClaim = None, - groupsConfig = None + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), + groupsConfig = GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None) ) def checkValidToken(): Unit = assertMatchRule( @@ -131,7 +153,9 @@ class JwtAuthRuleTests ) { blockContext => assertBlockContext( - jwt = Some(domain.Jwt.Payload(validJwt.defaultClaims())) + jwt = Some(domain.Jwt.Payload(validJwt.defaultClaims())), + loggedUser = Some(DirectlyLoggedUser(User.Id("testuser"))), + currentGroup = Some(GroupId("group1")), )(blockContext) } @@ -148,22 +172,24 @@ class JwtAuthRuleTests "user claim name is defined and userId is passed in JWT token claim" in { val key: Key = Jwts.SIG.HS256.key().build() val jwt = Jwt(key, claims = List( - "userId" := "user1" + "userId" := "user1", + "groups" := List("group1", "group2"), )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = None, + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), + groupsConfig = GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None), ), tokenHeader = bearerHeader(jwt) ) { blockContext => assertBlockContext( loggedUser = Some(DirectlyLoggedUser(User.Id("user1"))), - jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())) + jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())), + currentGroup = Some(GroupId("group1")), )(blockContext) } } @@ -174,18 +200,19 @@ class JwtAuthRuleTests "groups" := List("group1", "group2") )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), + groupsConfig = GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None) ), tokenHeader = bearerHeader(jwt) ) { blockContext => assertBlockContext( loggedUser = Some(DirectlyLoggedUser(User.Id("user1"))), + currentGroup = Some(GroupId("group1")), jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())) )(blockContext) } @@ -197,12 +224,12 @@ class JwtAuthRuleTests "groups" := List("group1", "group2") )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), + groupsConfig = GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None) ), tokenHeader = bearerHeader(jwt), preferredGroupId = Some(GroupId("group1")) @@ -222,34 +249,36 @@ class JwtAuthRuleTests "https://{domain}/claims/roles" := List("group1", "group2") )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("https://{domain}/claims/roles")), None)) + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), + groupsConfig = GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("https://{domain}/claims/roles")), None) ), tokenHeader = bearerHeader(jwt) ) { blockContext => assertBlockContext( loggedUser = Some(DirectlyLoggedUser(User.Id("user1"))), - jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())) + jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())), + currentGroup = Some(GroupId("group1")) )(blockContext) } } "group IDs claim name is defined and no groups field is passed in JWT token claim" in { val key: Key = Jwts.SIG.HS256.key().build() val jwt = Jwt(key, claims = List( - "userId" := "user1" + "userId" := "user1", + "groups" := List("group1", "group2") )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), + groupsConfig = GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None) ), configuredGroups = None, tokenHeader = bearerHeader(jwt) @@ -257,6 +286,7 @@ class JwtAuthRuleTests blockContext => assertBlockContext( loggedUser = Some(DirectlyLoggedUser(User.Id("user1"))), + currentGroup = Some(GroupId("group1")), jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())), )(blockContext) } @@ -268,18 +298,19 @@ class JwtAuthRuleTests "tech" :-> "beshu" :-> "groups" := List("group1", "group2") )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("tech.beshu.groups")), None)) + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), + groupsConfig = GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("tech.beshu.groups")), None) ), tokenHeader = bearerHeader(jwt) ) { blockContext => assertBlockContext( loggedUser = Some(DirectlyLoggedUser(User.Id("user1"))), + currentGroup = Some(GroupId("group1")), //RORDEV-1639 - this behavior changed, the `currentGroup` was not added to the context in pseudo-authorization before jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())) )(blockContext) } @@ -294,15 +325,15 @@ class JwtAuthRuleTests ).asJava) )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig( + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), + groupsConfig = GroupsConfig( idsClaim = domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.id)].id")), namesClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.name)].name"))) - )) + ) ), tokenHeader = bearerHeader(jwt) ) { @@ -310,6 +341,7 @@ class JwtAuthRuleTests assertBlockContext( loggedUser = Some(DirectlyLoggedUser(User.Id("user1"))), jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())), + currentGroup = Some(GroupId("group1")), //RORDEV-1639 - this behavior changed, the `currentGroup` was not added to the context in pseudo-authorization before )(blockContext) } } @@ -324,15 +356,15 @@ class JwtAuthRuleTests ).asJava) )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig( + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), + groupsConfig = GroupsConfig( idsClaim = domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.id)].id")), namesClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.name)].name"))) - )) + ) ), tokenHeader = bearerHeader(jwt) ) { @@ -340,6 +372,7 @@ class JwtAuthRuleTests assertBlockContext( loggedUser = Some(DirectlyLoggedUser(User.Id("user1"))), jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())), + currentGroup = Some(GroupId("group1")), //RORDEV-1639 - this behavior changed, the `currentGroup` was not added to the context in pseudo-authorization before )(blockContext) } } @@ -354,12 +387,12 @@ class JwtAuthRuleTests ).asJava) )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.id)].id")), Some(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.name)].name"))))) + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), + groupsConfig = GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.id)].id")), Some(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.name)].name")))) ), tokenHeader = bearerHeader(jwt) ) { @@ -367,6 +400,7 @@ class JwtAuthRuleTests assertBlockContext( loggedUser = Some(DirectlyLoggedUser(User.Id("user1"))), jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())), + currentGroup = Some(GroupId("group1")), //RORDEV-1639 - this behavior changed, the `currentGroup` was not added to the context in pseudo-authorization before )(blockContext) } } @@ -381,15 +415,15 @@ class JwtAuthRuleTests ).asJava) )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig( + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), + groupsConfig = GroupsConfig( idsClaim = domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.id)].id")), namesClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.name)].name"))) - )) + ) ), configuredGroups = Some(GroupsLogic.AnyOf(GroupIds( UniqueNonEmptyList.of(GroupId("group3"), GroupId("group2")) @@ -415,15 +449,15 @@ class JwtAuthRuleTests ).asJava) )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig( + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), + groupsConfig = GroupsConfig( idsClaim = domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.id)].id")), namesClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.name)].name"))) - )) + ) ), configuredGroups = Some(GroupsLogic.AnyOf(GroupIds( UniqueNonEmptyList.of(GroupId("group3"), GroupIdLike.from("*2")) @@ -449,15 +483,15 @@ class JwtAuthRuleTests ).asJava) )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig( + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), + groupsConfig = GroupsConfig( idsClaim = domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.id)].id")), namesClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.name)].name"))) - )) + ) ), configuredGroups = Some(GroupsLogic.AllOf(GroupIds( UniqueNonEmptyList.of(GroupId("group1"), GroupId("group2")) @@ -483,15 +517,15 @@ class JwtAuthRuleTests ).asJava) )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig( + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), + groupsConfig = GroupsConfig( idsClaim = domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.id)].id")), namesClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.name)].name"))) - )) + ) ), configuredGroups = Some(GroupsLogic.AllOf(GroupIds( UniqueNonEmptyList.of(GroupIdLike.from("*1"), GroupIdLike.from("*2")) @@ -509,33 +543,41 @@ class JwtAuthRuleTests } "custom authorization header is used" in { val key: Key = Jwts.SIG.HS256.key().build() - val jwt = Jwt(key, claims = List.empty) + val jwt = Jwt(key, claims = List( + "userId" := "user1", + "groups" := List("group1", "group2") + )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name("x-jwt-custom-header"), "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = None, - groupsConfig = None + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), + groupsConfig = GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None) ), tokenHeader = bearerHeader("x-jwt-custom-header", jwt) ) { blockContext => assertBlockContext( - jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())) + jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())), + loggedUser = Some(DirectlyLoggedUser(User.Id("user1"))), + currentGroup = Some(GroupId("group1")), )(blockContext) } } "custom authorization token prefix is used" in { val key: Key = Jwts.SIG.HS256.key().build() - val jwt = Jwt(key, claims = List.empty) + val jwt = Jwt(key, claims = List( + "userId" := "user1", + "groups" := List("group1", "group2") + )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name("x-jwt-custom-header"), "MyPrefix "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = None, - groupsConfig = None + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), + groupsConfig = GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None) ), tokenHeader = new Header( Header.Name("x-jwt-custom-header"), @@ -544,7 +586,9 @@ class JwtAuthRuleTests ) { blockContext => assertBlockContext( - jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())) + jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())), + loggedUser = Some(DirectlyLoggedUser(User.Id("user1"))), + currentGroup = Some(GroupId("group1")), )(blockContext) } } @@ -555,12 +599,12 @@ class JwtAuthRuleTests "groups" := List("group1", "group2") )) assertNotMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), + groupsConfig = GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None) ), tokenHeader = bearerHeader(jwt), preferredGroupId = Some(GroupId("group3")) @@ -573,12 +617,12 @@ class JwtAuthRuleTests val key2: Key = Jwts.SIG.HS256.key().build() val jwt2 = Jwt(key2, claims = List.empty) assertNotMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key1.getEncoded), - userClaim = None, - groupsConfig = None + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), + groupsConfig = GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None) ), tokenHeader = bearerHeader(jwt2) ) @@ -588,53 +632,60 @@ class JwtAuthRuleTests val (_, secret) = Random.generateRsaRandomKeys val jwt = Jwt(secret, claims = List.empty) assertNotMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Rsa(pub), - userClaim = None, - groupsConfig = None + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), + groupsConfig = GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None) ), tokenHeader = bearerHeader(jwt) ) } "token has no signature but external auth service returns false" in { - val jwt = Jwt(claims = List.empty) + val jwt = Jwt(claims = List( + "userId" := "user1", + "groups" := List("group1", "group2") + )) assertNotMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.NoCheck(authService(jwt.stringify(), authenticated = false)), - userClaim = None, - groupsConfig = None + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), + groupsConfig = GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None) ), tokenHeader = bearerHeader(jwt) ) } "user claim name is defined but userId isn't passed in JWT token claim" in { val key: Key = Jwts.SIG.HS256.key().build() - val jwt = Jwt(key, claims = List.empty) + val jwt = Jwt(key, claims = List( + "groups" := List("group1", "group2") + )) assertNotMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = None + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), + groupsConfig = GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None) ), tokenHeader = bearerHeader(jwt) ) } "group IDs claim name is defined but groups aren't passed in JWT token claim" in { val key: Key = Jwts.SIG.HS256.key().build() - val jwt = Jwt(key, claims = List.empty) + val jwt = Jwt(key, claims = List( + "userId" := "user1", + )) assertNotMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), + groupsConfig = GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None) ), tokenHeader = bearerHeader(jwt) ) @@ -646,12 +697,12 @@ class JwtAuthRuleTests "groups" := List("group1", "group2") )) assertNotMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("tech.beshu.groups.subgroups")), None)) + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), + groupsConfig = GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("tech.beshu.groups.subgroups")), None) ), configuredGroups = Some(GroupsLogic.AnyOf(GroupIds( UniqueNonEmptyList.of(GroupId("group1")) @@ -666,12 +717,12 @@ class JwtAuthRuleTests "groups" := List("group1", "group2") )) assertNotMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), + groupsConfig = GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None) ), configuredGroups = Some(GroupsLogic.AnyOf(GroupIds( UniqueNonEmptyList.of(GroupId("group3"), GroupId("group4")) @@ -686,12 +737,12 @@ class JwtAuthRuleTests "groups" := List("group1", "group2") )) assertNotMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), + groupsConfig = GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None) ), configuredGroups = Some(GroupsLogic.AllOf(GroupIds( UniqueNonEmptyList.of(GroupId("group2"), GroupId("group3")) @@ -706,12 +757,12 @@ class JwtAuthRuleTests "groups" := List("group1", "group2") )) assertNotMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), + groupsConfig = GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None) ), configuredGroups = Some(GroupsLogic.AnyOf(GroupIds( UniqueNonEmptyList.of(GroupId("group2")) @@ -723,33 +774,38 @@ class JwtAuthRuleTests } } - private def assertMatchRule(configuredJwtDef: JwtDef, + private def assertMatchRule(configuredJwtDef: AuthJwtDef, configuredGroups: Option[GroupsLogic] = None, tokenHeader: Header, preferredGroupId: Option[GroupId] = None) (blockContextAssertion: BlockContext => Unit): Unit = assertRule(configuredJwtDef, configuredGroups, tokenHeader, preferredGroupId, Some(blockContextAssertion)) - private def assertNotMatchRule(configuredJwtDef: JwtDef, + private def assertNotMatchRule(configuredJwtDef: AuthJwtDef, configuredGroups: Option[GroupsLogic] = None, tokenHeader: Header, preferredGroupId: Option[GroupId] = None): Unit = assertRule(configuredJwtDef, configuredGroups, tokenHeader, preferredGroupId, blockContextAssertion = None) - private def assertRule(configuredJwtDef: JwtDef, + private def assertRule(configuredJwtDef: AuthJwtDef, configuredGroups: Option[GroupsLogic], tokenHeader: Header, preferredGroup: Option[GroupId], blockContextAssertion: Option[BlockContext => Unit]) = { - val authorization: JwtAuthorizationRule | JwtPseudoAuthorizationRule = configuredGroups match { - case Some(groupsLogic) => new JwtAuthorizationRule(JwtAuthorizationRule.Settings(configuredJwtDef, groupsLogic)) - case None => new JwtPseudoAuthorizationRule(JwtPseudoAuthorizationRule.Settings(configuredJwtDef)) - } + val groupsLogic = configuredGroups.getOrElse( + GroupsLogic.AnyOf(GroupIds(UniqueNonEmptyList.of(GroupIdPattern.fromNes(nes("*"))))) + ) + val authzSettings = JwtAuthorizationRule.Settings( + AuthorizationJwtDef(configuredJwtDef.id, configuredJwtDef.authorizationTokenDef, configuredJwtDef.checkMethod, configuredJwtDef.groupsConfig), + groupsLogic, + ) + val authnSettings = JwtAuthenticationRule.Settings( + AuthenticationJwtDef(configuredJwtDef.id, configuredJwtDef.authorizationTokenDef, configuredJwtDef.checkMethod, configuredJwtDef.userClaim) + ) val rule = new JwtAuthRule( - new JwtAuthenticationRule(JwtAuthenticationRule.Settings(configuredJwtDef), CaseSensitivity.Enabled), - authorization, + new JwtAuthenticationRule(authnSettings, CaseSensitivity.Enabled), + new JwtAuthorizationRule(authzSettings), ) - val requestContext = MockRequestContext.indices.withHeaders( preferredGroup.map(_.toCurrentGroupHeader).toSeq :+ tokenHeader ) diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthenticationRuleTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthenticationRuleTests.scala index 2be82d9372..0da896e3a5 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthenticationRuleTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthenticationRuleTests.scala @@ -30,7 +30,7 @@ import tech.beshu.ror.accesscontrol.blocks.BlockContext import tech.beshu.ror.accesscontrol.blocks.BlockContext.GeneralIndexRequestBlockContext import tech.beshu.ror.accesscontrol.blocks.definitions.* import tech.beshu.ror.accesscontrol.blocks.definitions.ExternalAuthenticationService.Name -import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef.{GroupsConfig, SignatureCheckMethod} +import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef.SignatureCheckMethod import tech.beshu.ror.accesscontrol.blocks.metadata.UserMetadata import tech.beshu.ror.accesscontrol.blocks.rules.Rule.RuleResult.{Fulfilled, Rejected} import tech.beshu.ror.accesscontrol.blocks.rules.auth.JwtAuthenticationRule @@ -58,70 +58,69 @@ class JwtAuthenticationRuleTests "match" when { "token has valid HS256 signature" in { val secret: Key = Jwts.SIG.HS256.key().build() - val jwt = Jwt(secret, claims = List.empty) + val jwt = Jwt(secret, claims = List("userId" := "user")) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthenticationJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(secret.getEncoded), - userClaim = None, - groupsConfig = None + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), ), tokenHeader = bearerHeader(jwt) ) { blockContext => assertBlockContext( - jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())) + jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())), + loggedUser = Some(LoggedUser.DirectlyLoggedUser(User.Id("user"))), )(blockContext) } } "token has valid RS256 signature" in { val (pub, secret) = Random.generateRsaRandomKeys - val jwt = Jwt(secret, claims = List.empty) + val jwt = Jwt(secret, claims = List("userId" := "user")) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthenticationJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Rsa(pub), - userClaim = None, - groupsConfig = None + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), ), tokenHeader = bearerHeader(jwt) ) { blockContext => assertBlockContext( - jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())) + jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())), + loggedUser = Some(LoggedUser.DirectlyLoggedUser(User.Id("user"))), )(blockContext) } } "token has no signature and external auth service returns true" in { - val jwt = Jwt(claims = List.empty) + val jwt = Jwt(claims = List("userId" := "user")) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthenticationJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.NoCheck(authService(jwt.stringify(), authenticated = true)), - userClaim = None, - groupsConfig = None + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), ), tokenHeader = bearerHeader(jwt) ) { blockContext => assertBlockContext( - jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())) + jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())), + loggedUser = Some(LoggedUser.DirectlyLoggedUser(User.Id("user"))), )(blockContext) } } "token has no signature and external auth service state is cached" in { - val validJwt = Jwt(claims = List.empty) - val invalidJwt = Jwt(claims = List("user" := "testuser")) + val validJwt = Jwt(claims = List("userId" := "testuser")) + val invalidJwt = Jwt(claims = List("userId" := "invalid_user")) val authService = cachedAuthService(validJwt.stringify(), invalidJwt.stringify()) - val jwtDef = JwtDef( + val jwtDef = AuthenticationJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.NoCheck(authService), - userClaim = None, - groupsConfig = None + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), ) def checkValidToken(): Unit = assertMatchRule( @@ -130,7 +129,8 @@ class JwtAuthenticationRuleTests ) { blockContext => assertBlockContext( - jwt = Some(domain.Jwt.Payload(validJwt.defaultClaims())) + jwt = Some(domain.Jwt.Payload(validJwt.defaultClaims())), + loggedUser = Some(LoggedUser.DirectlyLoggedUser(User.Id("testuser"))), )(blockContext) } @@ -150,12 +150,11 @@ class JwtAuthenticationRuleTests "userId" := "user1" )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthenticationJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = None, + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), ), tokenHeader = bearerHeader(jwt) ) { @@ -173,12 +172,11 @@ class JwtAuthenticationRuleTests "groups" := List("group1", "group2") )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthenticationJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), ), tokenHeader = bearerHeader(jwt) ) { @@ -196,12 +194,11 @@ class JwtAuthenticationRuleTests "groups" := List("group1", "group2") )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthenticationJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), ), tokenHeader = bearerHeader(jwt), preferredGroupId = Some(GroupId("group1")) @@ -221,12 +218,11 @@ class JwtAuthenticationRuleTests "https://{domain}/claims/roles" := List("group1", "group2") )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthenticationJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("https://{domain}/claims/roles")), None)) + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), ), tokenHeader = bearerHeader(jwt) ) { @@ -243,12 +239,11 @@ class JwtAuthenticationRuleTests "userId" := "user1" )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthenticationJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), ), tokenHeader = bearerHeader(jwt) ) { @@ -266,12 +261,11 @@ class JwtAuthenticationRuleTests "tech" :-> "beshu" :-> "groups" := List("group1", "group2") )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthenticationJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("tech.beshu.groups")), None)) + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), ), tokenHeader = bearerHeader(jwt) ) { @@ -292,15 +286,11 @@ class JwtAuthenticationRuleTests ).asJava) )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthenticationJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig( - idsClaim = domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.id)].id")), - namesClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.name)].name"))) - )) + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), ), tokenHeader = bearerHeader(jwt) ) { @@ -322,15 +312,11 @@ class JwtAuthenticationRuleTests ).asJava) )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthenticationJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig( - idsClaim = domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.id)].id")), - namesClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.name)].name"))) - )) + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), ), tokenHeader = bearerHeader(jwt) ) { @@ -352,12 +338,11 @@ class JwtAuthenticationRuleTests ).asJava) )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthenticationJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.id)].id")), Some(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.name)].name"))))) + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), ), tokenHeader = bearerHeader(jwt) ) { @@ -371,33 +356,32 @@ class JwtAuthenticationRuleTests } "custom authorization header is used" in { val key: Key = Jwts.SIG.HS256.key().build() - val jwt = Jwt(key, claims = List.empty) + val jwt = Jwt(key, claims = List("userId" := "user")) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthenticationJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name("x-jwt-custom-header"), "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = None, - groupsConfig = None + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), ), tokenHeader = bearerHeader("x-jwt-custom-header", jwt) ) { blockContext => assertBlockContext( - jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())) + jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())), + loggedUser = Some(LoggedUser.DirectlyLoggedUser(User.Id("user"))), )(blockContext) } } "custom authorization token prefix is used" in { val key: Key = Jwts.SIG.HS256.key().build() - val jwt = Jwt(key, claims = List.empty) + val jwt = Jwt(key, claims = List("userId" := "user")) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthenticationJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name("x-jwt-custom-header"), "MyPrefix "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = None, - groupsConfig = None + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), ), tokenHeader = new Header( Header.Name("x-jwt-custom-header"), @@ -406,7 +390,8 @@ class JwtAuthenticationRuleTests ) { blockContext => assertBlockContext( - jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())) + jwt = Some(domain.Jwt.Payload(jwt.defaultClaims())), + loggedUser = Some(LoggedUser.DirectlyLoggedUser(User.Id("user"))), )(blockContext) } } @@ -417,12 +402,11 @@ class JwtAuthenticationRuleTests val key2: Key = Jwts.SIG.HS256.key().build() val jwt2 = Jwt(key2, claims = List.empty) assertNotMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthenticationJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key1.getEncoded), - userClaim = None, - groupsConfig = None + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), ), tokenHeader = bearerHeader(jwt2) ) @@ -432,25 +416,23 @@ class JwtAuthenticationRuleTests val (_, secret) = Random.generateRsaRandomKeys val jwt = Jwt(secret, claims = List.empty) assertNotMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthenticationJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Rsa(pub), - userClaim = None, - groupsConfig = None + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), ), tokenHeader = bearerHeader(jwt) ) } "token has no signature but external auth service returns false" in { - val jwt = Jwt(claims = List.empty) + val jwt = Jwt(claims = List("userId" := "user")) assertNotMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthenticationJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.NoCheck(authService(jwt.stringify(), authenticated = false)), - userClaim = None, - groupsConfig = None + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), ), tokenHeader = bearerHeader(jwt) ) @@ -459,12 +441,11 @@ class JwtAuthenticationRuleTests val key: Key = Jwts.SIG.HS256.key().build() val jwt = Jwt(key, claims = List.empty) assertNotMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthenticationJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = None + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), ), tokenHeader = bearerHeader(jwt) ) @@ -473,12 +454,11 @@ class JwtAuthenticationRuleTests val key: Key = Jwts.SIG.HS256.key().build() val jwt = Jwt(key, claims = List.empty) assertNotMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthenticationJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), ), tokenHeader = bearerHeader(jwt) ) @@ -490,12 +470,11 @@ class JwtAuthenticationRuleTests "groups" := List("group1", "group2") )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthenticationJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + userClaim = domain.Jwt.ClaimName(jsonPathFrom("userId")), ), tokenHeader = bearerHeader(jwt), preferredGroupId = Some(GroupId("group3")) @@ -504,18 +483,18 @@ class JwtAuthenticationRuleTests } } - private def assertMatchRule(configuredJwtDef: JwtDef, + private def assertMatchRule(configuredJwtDef: AuthenticationJwtDef, tokenHeader: Header, preferredGroupId: Option[GroupId] = None) (blockContextAssertion: BlockContext => Unit): Unit = assertRule(configuredJwtDef, tokenHeader, preferredGroupId, Some(blockContextAssertion)) - private def assertNotMatchRule(configuredJwtDef: JwtDef, + private def assertNotMatchRule(configuredJwtDef: AuthenticationJwtDef, tokenHeader: Header, preferredGroupId: Option[GroupId] = None): Unit = assertRule(configuredJwtDef, tokenHeader, preferredGroupId, blockContextAssertion = None) - private def assertRule(configuredJwtDef: JwtDef, + private def assertRule(configuredJwtDef: AuthenticationJwtDef, tokenHeader: Header, preferredGroup: Option[GroupId], blockContextAssertion: Option[BlockContext => Unit]) = { diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthorizationRuleTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthorizationRuleTests.scala index 859251c1b3..eda6ea43c5 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthorizationRuleTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthorizationRuleTests.scala @@ -60,15 +60,14 @@ class JwtAuthorizationRuleTests ).asJava) )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthorizationJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig( + groupsConfig = GroupsConfig( idsClaim = domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.id)].id")), namesClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.name)].name"))) - )) + ) ), configuredGroups = GroupsLogic.AnyOf(GroupIds( UniqueNonEmptyList.of(GroupId("group3"), GroupId("group2")) @@ -92,15 +91,14 @@ class JwtAuthorizationRuleTests ).asJava) )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthorizationJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig( + groupsConfig = GroupsConfig( idsClaim = domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.id)].id")), namesClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.name)].name"))) - )) + ) ), configuredGroups = GroupsLogic.AnyOf(GroupIds( UniqueNonEmptyList.of(GroupId("group3"), GroupIdLike.from("*2")) @@ -124,15 +122,14 @@ class JwtAuthorizationRuleTests ).asJava) )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthorizationJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig( + groupsConfig = GroupsConfig( idsClaim = domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.id)].id")), namesClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.name)].name"))) - )) + ) ), configuredGroups = GroupsLogic.AllOf(GroupIds( UniqueNonEmptyList.of(GroupId("group1"), GroupId("group2")) @@ -156,15 +153,14 @@ class JwtAuthorizationRuleTests ).asJava) )) assertMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthorizationJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig( + groupsConfig = GroupsConfig( idsClaim = domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.id)].id")), namesClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("groups[?(@.name)].name"))) - )) + ) ), configuredGroups = GroupsLogic.AllOf(GroupIds( UniqueNonEmptyList.of(GroupIdLike.from("*1"), GroupIdLike.from("*2")) @@ -187,12 +183,11 @@ class JwtAuthorizationRuleTests "groups" := List("group1", "group2") )) assertNotMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthorizationJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("tech.beshu.groups.subgroups")), None)) + groupsConfig = GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("tech.beshu.groups.subgroups")), None) ), configuredGroups = GroupsLogic.AnyOf(GroupIds( UniqueNonEmptyList.of(GroupId("group1")) @@ -207,12 +202,11 @@ class JwtAuthorizationRuleTests "groups" := List("group1", "group2") )) assertNotMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthorizationJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + groupsConfig = GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None) ), configuredGroups = GroupsLogic.AnyOf(GroupIds( UniqueNonEmptyList.of(GroupId("group3"), GroupId("group4")) @@ -227,12 +221,11 @@ class JwtAuthorizationRuleTests "groups" := List("group1", "group2") )) assertNotMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthorizationJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + groupsConfig = GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None) ), configuredGroups = GroupsLogic.AllOf(GroupIds( UniqueNonEmptyList.of(GroupId("group2"), GroupId("group3")) @@ -247,12 +240,11 @@ class JwtAuthorizationRuleTests "groups" := List("group1", "group2") )) assertNotMatchRule( - configuredJwtDef = JwtDef( + configuredJwtDef = AuthorizationJwtDef( JwtDef.Name("test"), AuthorizationTokenDef(Header.Name.authorization, "Bearer "), SignatureCheckMethod.Hmac(key.getEncoded), - userClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("userId"))), - groupsConfig = Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + groupsConfig = GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None) ), configuredGroups = GroupsLogic.AnyOf(GroupIds( UniqueNonEmptyList.of(GroupId("group2")) @@ -264,20 +256,20 @@ class JwtAuthorizationRuleTests } } - private def assertMatchRule(configuredJwtDef: JwtDef, + private def assertMatchRule(configuredJwtDef: AuthorizationJwtDef, configuredGroups: GroupsLogic, tokenHeader: Header, preferredGroupId: Option[GroupId] = None) (blockContextAssertion: BlockContext => Unit): Unit = assertRule(configuredJwtDef, configuredGroups, tokenHeader, preferredGroupId, Some(blockContextAssertion)) - private def assertNotMatchRule(configuredJwtDef: JwtDef, + private def assertNotMatchRule(configuredJwtDef: AuthorizationJwtDef, configuredGroups: GroupsLogic, tokenHeader: Header, preferredGroupId: Option[GroupId] = None): Unit = assertRule(configuredJwtDef, configuredGroups, tokenHeader, preferredGroupId, blockContextAssertion = None) - private def assertRule(configuredJwtDef: JwtDef, + private def assertRule(configuredJwtDef: AuthorizationJwtDef, configuredGroups: GroupsLogic, tokenHeader: Header, preferredGroup: Option[GroupId], diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/ImpersonationWarningsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/ImpersonationWarningsTests.scala index 277b8894ef..997ae0fe82 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/ImpersonationWarningsTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/ImpersonationWarningsTests.scala @@ -290,6 +290,8 @@ class ImpersonationWarningsTests extends AnyWordSpec with Inside { | jwt: | | - name: jwt1 + | user_claim: "user" + | group_ids_claim: groups | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | |""".stripMargin diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthRuleSettingsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthRuleSettingsTests.scala index 446da843c6..d6ab1b63cf 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthRuleSettingsTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthRuleSettingsTests.scala @@ -20,10 +20,11 @@ import org.scalamock.scalatest.MockFactory import org.scalatest.matchers.should.Matchers.* import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef.{GroupsConfig, SignatureCheckMethod} import tech.beshu.ror.accesscontrol.blocks.definitions.{CacheableExternalAuthenticationServiceDecorator, JwtDef} -import tech.beshu.ror.accesscontrol.blocks.rules.auth.{JwtAuthRule, JwtAuthorizationRule, JwtPseudoAuthorizationRule} +import tech.beshu.ror.accesscontrol.blocks.rules.auth.{JwtAuthRule, JwtAuthorizationRule} import tech.beshu.ror.accesscontrol.domain import tech.beshu.ror.accesscontrol.domain.* import tech.beshu.ror.accesscontrol.domain.GroupIdLike.GroupId +import tech.beshu.ror.accesscontrol.domain.Jwt.ClaimName import tech.beshu.ror.accesscontrol.factory.HttpClientsFactory import tech.beshu.ror.accesscontrol.factory.HttpClientsFactory.HttpClient import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.Reason.{MalformedValue, Message} @@ -33,6 +34,7 @@ import tech.beshu.ror.providers.EnvVarProvider.EnvVarName import tech.beshu.ror.providers.EnvVarsProvider import tech.beshu.ror.unit.acl.factory.decoders.rules.BaseRuleSettingsDecoderTest import tech.beshu.ror.utils.TestsUtils.* +import tech.beshu.ror.utils.json.JsonPath import tech.beshu.ror.utils.uniquelist.UniqueNonEmptyList import java.security.KeyPairGenerator @@ -58,6 +60,8 @@ class JwtAuthRuleSettingsTests | jwt: | | - name: jwt1 + | user_claim: "user" + | group_ids_claim: groups | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | |""".stripMargin, @@ -65,9 +69,8 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] - rule.authentication.settings.jwt.userClaim should be(None) - rule.authentication.settings.jwt.groupsConfig should be(None) - rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] + rule.authentication.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) + rule.authorization.settings.jwt.groupsConfig should be(GroupsConfig(ClaimName(JsonPath("groups").get), None)) } ) } @@ -86,6 +89,8 @@ class JwtAuthRuleSettingsTests | jwt: | | - name: jwt1 + | user_claim: "user" + | group_ids_claim: groups | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | |""".stripMargin, @@ -93,9 +98,8 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] - rule.authentication.settings.jwt.userClaim should be(None) - rule.authentication.settings.jwt.groupsConfig should be(None) - rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] + rule.authentication.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) + rule.authorization.settings.jwt.groupsConfig should be(GroupsConfig(ClaimName(JsonPath("groups").get), None)) } ) } @@ -116,6 +120,8 @@ class JwtAuthRuleSettingsTests | jwt: | | - name: jwt1 + | user_claim: "user" + | group_ids_claim: groups | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | |""".stripMargin, @@ -123,8 +129,8 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] - rule.authentication.settings.jwt.userClaim should be(None) - rule.authentication.settings.jwt.groupsConfig should be(None) + rule.authentication.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) + rule.authorization.settings.jwt.groupsConfig should be(GroupsConfig(ClaimName(JsonPath("groups").get), None)) rule.authorization.asInstanceOf[JwtAuthorizationRule].settings.groupsLogic should be(GroupsLogic.AnyOf(GroupIds( UniqueNonEmptyList.of(GroupIdLike.from("group1*"), GroupId("group2")) ))) @@ -149,6 +155,8 @@ class JwtAuthRuleSettingsTests | jwt: | | - name: jwt1 + | user_claim: "user" + | group_ids_claim: groups | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | |""".stripMargin, @@ -156,8 +164,8 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] - rule.authentication.settings.jwt.userClaim should be(None) - rule.authentication.settings.jwt.groupsConfig should be(None) + rule.authentication.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) + rule.authorization.settings.jwt.groupsConfig should be(GroupsConfig(ClaimName(JsonPath("groups").get), None)) rule.authorization.asInstanceOf[JwtAuthorizationRule].settings.groupsLogic should be(GroupsLogic.AllOf(GroupIds( UniqueNonEmptyList.of(GroupIdLike.from("group1*"), GroupId("group2")) ))) @@ -180,6 +188,8 @@ class JwtAuthRuleSettingsTests | jwt: | | - name: jwt1 + | user_claim: "user" + | group_ids_claim: groups | header_name: X-JWT-Custom-Header | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | @@ -188,9 +198,8 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(headerNameFrom("X-JWT-Custom-Header"), "Bearer ")) rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] - rule.authentication.settings.jwt.userClaim should be(None) - rule.authentication.settings.jwt.groupsConfig should be(None) - rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] + rule.authentication.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) + rule.authorization.settings.jwt.groupsConfig should be(GroupsConfig(ClaimName(JsonPath("groups").get), None)) } ) } @@ -208,6 +217,8 @@ class JwtAuthRuleSettingsTests | jwt: | | - name: jwt1 + | user_claim: "user" + | group_ids_claim: groups | header_name: X-JWT-Custom-Header | header_prefix: "MyPrefix " | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" @@ -217,9 +228,8 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(headerNameFrom("X-JWT-Custom-Header"), "MyPrefix ")) rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] - rule.authentication.settings.jwt.userClaim should be(None) - rule.authentication.settings.jwt.groupsConfig should be(None) - rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] + rule.authentication.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) + rule.authorization.settings.jwt.groupsConfig should be(GroupsConfig(ClaimName(JsonPath("groups").get), None)) } ) } @@ -237,6 +247,8 @@ class JwtAuthRuleSettingsTests | jwt: | | - name: jwt1 + | user_claim: "user" + | group_ids_claim: groups | header_prefix: "MyPrefix " | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | @@ -245,9 +257,8 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "MyPrefix ")) rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] - rule.authentication.settings.jwt.userClaim should be(None) - rule.authentication.settings.jwt.groupsConfig should be(None) - rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] + rule.authentication.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) + rule.authorization.settings.jwt.groupsConfig should be(GroupsConfig(ClaimName(JsonPath("groups").get), None)) } ) } @@ -265,6 +276,8 @@ class JwtAuthRuleSettingsTests | jwt: | | - name: jwt1 + | user_claim: "user" + | group_ids_claim: groups | header_name: X-JWT-Custom-Header | header_prefix: "" | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" @@ -274,9 +287,8 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(headerNameFrom("X-JWT-Custom-Header"), "")) rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] - rule.authentication.settings.jwt.userClaim should be(None) - rule.authentication.settings.jwt.groupsConfig should be(None) - rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] + rule.authentication.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) + rule.authorization.settings.jwt.groupsConfig should be(GroupsConfig(ClaimName(JsonPath("groups").get), None)) } ) } @@ -294,7 +306,8 @@ class JwtAuthRuleSettingsTests | jwt: | | - name: jwt1 - | user_claim: user + | user_claim: "user" + | group_ids_claim: groups | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | |""".stripMargin, @@ -302,9 +315,8 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] - rule.authentication.settings.jwt.userClaim should be(Some(domain.Jwt.ClaimName(jsonPathFrom("user")))) - rule.authentication.settings.jwt.groupsConfig should be(None) - rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] + rule.authentication.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) + rule.authorization.settings.jwt.groupsConfig should be(GroupsConfig(ClaimName(JsonPath("groups").get), None)) } ) } @@ -324,6 +336,7 @@ class JwtAuthRuleSettingsTests | jwt: | | - name: jwt1 + | user_claim: "user" | $claimKey: groups | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | @@ -332,9 +345,8 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.authentication.settings.jwt.checkMethod shouldBe a[SignatureCheckMethod.Hmac] - rule.authentication.settings.jwt.userClaim should be(None) - rule.authentication.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) - rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] + rule.authentication.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) + assertGroupsConfig(rule, GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) } ) } @@ -353,6 +365,7 @@ class JwtAuthRuleSettingsTests | jwt: | | - name: jwt1 + | user_claim: "user" | group_ids_claim: groups | group_names_claim: group_names | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" @@ -362,12 +375,11 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.authentication.settings.jwt.checkMethod shouldBe a[SignatureCheckMethod.Hmac] - rule.authentication.settings.jwt.userClaim should be(None) - rule.authentication.settings.jwt.groupsConfig should be(Some(GroupsConfig( + rule.authentication.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) + assertGroupsConfig(rule, GroupsConfig( idsClaim = domain.Jwt.ClaimName(jsonPathFrom("groups")), namesClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("group_names"))) - ))) - rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] + )) } ) } @@ -385,6 +397,7 @@ class JwtAuthRuleSettingsTests | jwt: | | - name: jwt1 + | user_claim: "user" | group_ids_claim: "https://{domain}/claims/roles" | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | @@ -393,9 +406,8 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] - rule.authentication.settings.jwt.userClaim should be(None) - rule.authentication.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("https://{domain}/claims/roles")), None))) - rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] + rule.authentication.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) + assertGroupsConfig(rule, GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("https://{domain}/claims/roles")), None)) } ) } @@ -414,6 +426,7 @@ class JwtAuthRuleSettingsTests | jwt: | | - name: jwt1 + | user_claim: "user" | group_ids_claim: groups | signature_algo: "RSA" | signature_key: "${Base64.getEncoder.encodeToString(pkey.getEncoded)}" @@ -423,9 +436,8 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Rsa] - rule.authentication.settings.jwt.userClaim should be(None) - rule.authentication.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) - rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] + rule.authentication.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) + assertGroupsConfig(rule, GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) } ) } @@ -443,6 +455,7 @@ class JwtAuthRuleSettingsTests | jwt: | | - name: jwt1 + | user_claim: "user" | group_ids_claim: groups | signature_algo: "RSA" | signature_key: "env:SECRET_RSA" @@ -452,9 +465,8 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Rsa] - rule.authentication.settings.jwt.userClaim should be(None) - rule.authentication.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) - rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] + rule.authentication.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) + assertGroupsConfig(rule, GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) } ) } @@ -474,6 +486,7 @@ class JwtAuthRuleSettingsTests | jwt: | | - name: jwt1 + | user_claim: "user" | group_ids_claim: groups | signature_algo: "RSA" | signature_key: "@{env:SECRET_RSA}" @@ -483,9 +496,8 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Rsa] - rule.authentication.settings.jwt.userClaim should be(None) - rule.authentication.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) - rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] + rule.authentication.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) + assertGroupsConfig(rule, GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) } ) } @@ -504,6 +516,7 @@ class JwtAuthRuleSettingsTests | jwt: | | - name: jwt1 + | user_claim: "user" | group_ids_claim: groups | signature_algo: "EC" | signature_key: "text: ${Base64.getEncoder.encodeToString(pkey.getEncoded)}" @@ -513,9 +526,8 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.id should be(JwtDef.Name("jwt1")) rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Ec] - rule.authentication.settings.jwt.userClaim should be(None) - rule.authentication.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) - rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] + rule.authentication.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) + assertGroupsConfig(rule, GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) } ) } @@ -533,6 +545,7 @@ class JwtAuthRuleSettingsTests | jwt: | | - name: jwt1 + | user_claim: "user" | group_ids_claim: groups | signature_algo: "NONE" | external_validator: @@ -548,9 +561,8 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.NoCheck] rule.authentication.settings.jwt.checkMethod.asInstanceOf[SignatureCheckMethod.NoCheck].service shouldBe a[CacheableExternalAuthenticationServiceDecorator] - rule.authentication.settings.jwt.userClaim should be(None) - rule.authentication.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")),None))) - rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] + rule.authentication.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) + assertGroupsConfig(rule, GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")),None)) } ) } @@ -568,6 +580,7 @@ class JwtAuthRuleSettingsTests | jwt: | | - name: jwt1 + | user_claim: "user" | group_ids_claim: groups | signature_algo: "NONE" | external_validator: @@ -586,9 +599,8 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.NoCheck] rule.authentication.settings.jwt.checkMethod.asInstanceOf[SignatureCheckMethod.NoCheck].service shouldBe a[CacheableExternalAuthenticationServiceDecorator] - rule.authentication.settings.jwt.userClaim should be(None) - rule.authentication.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")),None))) - rule.authorization.asInstanceOf[JwtPseudoAuthorizationRule] + rule.authentication.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) + assertGroupsConfig(rule, GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")),None)) } ) } @@ -608,6 +620,8 @@ class JwtAuthRuleSettingsTests | jwt: | | - name: jwt1 + | user_claim: "user" + | group_ids_claim: groups | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | |""".stripMargin, @@ -634,12 +648,14 @@ class JwtAuthRuleSettingsTests | jwt: | | - name: jwt1 + | user_claim: "user" + | group_ids_claim: groups | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | |""".stripMargin, assertion = errors => { errors should have size 1 - errors.head should be(RulesLevelCreationError(Message("Cannot find JWT definition with name: jwt2"))) + errors.head should be(RulesLevelCreationError(Message("Cannot find jwt definition with name: jwt2"))) } ) } @@ -656,7 +672,7 @@ class JwtAuthRuleSettingsTests |""".stripMargin, assertion = errors => { errors should have size 1 - errors.head should be(RulesLevelCreationError(Message("Cannot find JWT definition with name: jwt1"))) + errors.head should be(RulesLevelCreationError(Message("Cannot find jwt definition with name: jwt1"))) } ) } @@ -675,6 +691,8 @@ class JwtAuthRuleSettingsTests | jwt: | | - name: jwt1 + | user_claim: "user" + | group_ids_claim: groups | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | |""".stripMargin, @@ -712,13 +730,15 @@ class JwtAuthRuleSettingsTests | jwt: | | - name: jwt1 + | user_claim: "user" + | group_ids_claim: groups | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | |""".stripMargin, assertion = errors => { errors should have size 1 errors.head should be(RulesLevelCreationError(Message( - s"Please specify either '$groupsAnyOfKey' or '$groupsAllOfKey' for JWT authorization rule 'jwt1'" + s"Please specify either '$groupsAnyOfKey' or '$groupsAllOfKey' for jwt_authorization rule 'jwt1'" ))) } ) @@ -763,9 +783,13 @@ class JwtAuthRuleSettingsTests | jwt: | | - name: jwt1 + | user_claim: "user" + | group_ids_claim: groups | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | | - name: jwt1 + | user_claim: "user" + | group_ids_claim: groups | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | |""".stripMargin, @@ -1029,4 +1053,8 @@ class JwtAuthRuleSettingsTests val httpClientMock = mock[HttpClient] new MockHttpClientsFactoryWithFixedHttpClient(httpClientMock) } + + private def assertGroupsConfig(rule: JwtAuthRule, expected: GroupsConfig) = { + rule.authorization.settings.jwt.groupsConfig should be(expected) + } } diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthenticationRuleSettingsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthenticationRuleSettingsTests.scala index f8deaa4832..0fe0ba6a2c 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthenticationRuleSettingsTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthenticationRuleSettingsTests.scala @@ -18,7 +18,7 @@ package tech.beshu.ror.unit.acl.factory.decoders.rules.auth import org.scalamock.scalatest.MockFactory import org.scalatest.matchers.should.Matchers.* -import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef.{GroupsConfig, SignatureCheckMethod} +import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef.SignatureCheckMethod import tech.beshu.ror.accesscontrol.blocks.definitions.{CacheableExternalAuthenticationServiceDecorator, JwtDef} import tech.beshu.ror.accesscontrol.blocks.rules.auth.JwtAuthenticationRule import tech.beshu.ror.accesscontrol.domain @@ -56,6 +56,7 @@ class JwtAuthenticationRuleSettingsTests | jwt: | | - name: jwt1 + | user_claim: user | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | |""".stripMargin, @@ -63,8 +64,7 @@ class JwtAuthenticationRuleSettingsTests rule.settings.jwt.id should be(JwtDef.Name("jwt1")) rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(None) + rule.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) } ) } @@ -83,6 +83,7 @@ class JwtAuthenticationRuleSettingsTests | jwt: | | - name: jwt1 + | user_claim: user | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | |""".stripMargin, @@ -90,8 +91,7 @@ class JwtAuthenticationRuleSettingsTests rule.settings.jwt.id should be(JwtDef.Name("jwt1")) rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(None) + rule.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) } ) } @@ -117,41 +117,10 @@ class JwtAuthenticationRuleSettingsTests rule.settings.jwt.id should be(JwtDef.Name("jwt1")) rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] - rule.settings.jwt.userClaim should be(Some(domain.Jwt.ClaimName(jsonPathFrom("user")))) - rule.settings.jwt.groupsConfig should be(None) + rule.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) } ) } - "group IDs claim can be enabled in JWT definition" in { - val claimKeys = List("roles_claim", "groups_claim", "group_ids_claim") - claimKeys.foreach { claimKey => - assertDecodingSuccess( - yaml = - s""" - |readonlyrest: - | - | access_control_rules: - | - | - name: test_block1 - | jwt_authentication: jwt1 - | - | jwt: - | - | - name: jwt1 - | $claimKey: groups - | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" - | - |""".stripMargin, - assertion = rule => { - rule.settings.jwt.id should be(JwtDef.Name("jwt1")) - rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) - rule.settings.jwt.checkMethod shouldBe a[SignatureCheckMethod.Hmac] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) - } - ) - } - } "group names claim can be enabled in JWT definition" in { assertDecodingSuccess( yaml = @@ -164,10 +133,9 @@ class JwtAuthenticationRuleSettingsTests | jwt_authentication: jwt1 | | jwt: - | + | | - name: jwt1 - | group_ids_claim: groups - | group_names_claim: group_names + | user_claim: user | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | |""".stripMargin, @@ -175,11 +143,7 @@ class JwtAuthenticationRuleSettingsTests rule.settings.jwt.id should be(JwtDef.Name("jwt1")) rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.settings.jwt.checkMethod shouldBe a[SignatureCheckMethod.Hmac] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(Some(GroupsConfig( - idsClaim = domain.Jwt.ClaimName(jsonPathFrom("groups")), - namesClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("group_names"))) - ))) + rule.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) } ) } @@ -197,7 +161,7 @@ class JwtAuthenticationRuleSettingsTests | jwt: | | - name: jwt1 - | group_ids_claim: "https://{domain}/claims/roles" + | user_claim: user | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | |""".stripMargin, @@ -205,8 +169,7 @@ class JwtAuthenticationRuleSettingsTests rule.settings.jwt.id should be(JwtDef.Name("jwt1")) rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("https://{domain}/claims/roles")), None))) + rule.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) } ) } @@ -227,7 +190,7 @@ class JwtAuthenticationRuleSettingsTests | jwt: | | - name: jwt1 - | group_ids_claim: groups + | user_claim: user | signature_algo: "RSA" | signature_key: "${Base64.getEncoder.encodeToString(pkey.getEncoded)}" | @@ -236,8 +199,7 @@ class JwtAuthenticationRuleSettingsTests rule.settings.jwt.id should be(JwtDef.Name("jwt1")) rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Rsa] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) + rule.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) } ) } @@ -253,9 +215,9 @@ class JwtAuthenticationRuleSettingsTests | jwt_authentication: jwt1 | | jwt: - | + | | - name: jwt1 - | group_ids_claim: groups + | user_claim: user | signature_algo: "RSA" | signature_key: "env:SECRET_RSA" | @@ -264,8 +226,7 @@ class JwtAuthenticationRuleSettingsTests rule.settings.jwt.id should be(JwtDef.Name("jwt1")) rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Rsa] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) + rule.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) } ) } @@ -283,9 +244,9 @@ class JwtAuthenticationRuleSettingsTests | jwt_authentication: jwt1 | | jwt: - | + | | - name: jwt1 - | group_ids_claim: groups + | user_claim: user | signature_algo: "RSA" | signature_key: "@{env:SECRET_RSA}" | @@ -294,8 +255,7 @@ class JwtAuthenticationRuleSettingsTests rule.settings.jwt.id should be(JwtDef.Name("jwt1")) rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Rsa] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) + rule.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) } ) } @@ -312,9 +272,9 @@ class JwtAuthenticationRuleSettingsTests | jwt_authentication: jwt1 | | jwt: - | + | | - name: jwt1 - | group_ids_claim: groups + | user_claim: user | signature_algo: "EC" | signature_key: "text: ${Base64.getEncoder.encodeToString(pkey.getEncoded)}" | @@ -323,8 +283,7 @@ class JwtAuthenticationRuleSettingsTests rule.settings.jwt.id should be(JwtDef.Name("jwt1")) rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Ec] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None))) + rule.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) } ) } @@ -340,9 +299,9 @@ class JwtAuthenticationRuleSettingsTests | jwt_authentication: jwt1 | | jwt: - | + | | - name: jwt1 - | group_ids_claim: groups + | user_claim: user | signature_algo: "NONE" | external_validator: | url: "http://192.168.0.1:8080/jwt" @@ -357,8 +316,7 @@ class JwtAuthenticationRuleSettingsTests rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.NoCheck] rule.settings.jwt.checkMethod.asInstanceOf[SignatureCheckMethod.NoCheck].service shouldBe a[CacheableExternalAuthenticationServiceDecorator] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")),None))) + rule.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) } ) } @@ -374,9 +332,9 @@ class JwtAuthenticationRuleSettingsTests | jwt_authentication: jwt1 | | jwt: - | + | | - name: jwt1 - | group_ids_claim: groups + | user_claim: user | signature_algo: "NONE" | external_validator: | url: "http://192.168.0.1:8080/jwt" @@ -394,8 +352,7 @@ class JwtAuthenticationRuleSettingsTests rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.NoCheck] rule.settings.jwt.checkMethod.asInstanceOf[SignatureCheckMethod.NoCheck].service shouldBe a[CacheableExternalAuthenticationServiceDecorator] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(Some(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")),None))) + rule.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) } ) } @@ -415,6 +372,7 @@ class JwtAuthenticationRuleSettingsTests | jwt: | | - name: jwt1 + | user_claim: user | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | |""".stripMargin, @@ -441,12 +399,13 @@ class JwtAuthenticationRuleSettingsTests | jwt: | | - name: jwt1 + | user_claim: user | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | |""".stripMargin, assertion = errors => { errors should have size 1 - errors.head should be(RulesLevelCreationError(Message("Cannot find JWT definition with name: jwt2"))) + errors.head should be(RulesLevelCreationError(Message("Cannot find jwt definition with name: jwt2"))) } ) } @@ -463,7 +422,7 @@ class JwtAuthenticationRuleSettingsTests |""".stripMargin, assertion = errors => { errors should have size 1 - errors.head should be(RulesLevelCreationError(Message("Cannot find JWT definition with name: jwt1"))) + errors.head should be(RulesLevelCreationError(Message("Cannot find jwt definition with name: jwt1"))) } ) } @@ -492,7 +451,7 @@ class JwtAuthenticationRuleSettingsTests } ) } - "RSA algorithm is defined but on signature key" in { + "RSA algorithm is defined but no signature key" in { assertDecodingFailure( yaml = """ diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthorizationRuleSettingsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthorizationRuleSettingsTests.scala index 28b5785b53..fedd3dfdac 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthorizationRuleSettingsTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthorizationRuleSettingsTests.scala @@ -19,16 +19,18 @@ package tech.beshu.ror.unit.acl.factory.decoders.rules.auth import org.scalamock.scalatest.MockFactory import org.scalatest.matchers.should.Matchers.* import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef -import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef.SignatureCheckMethod +import tech.beshu.ror.accesscontrol.blocks.definitions.JwtDef.{GroupsConfig, SignatureCheckMethod} import tech.beshu.ror.accesscontrol.blocks.rules.auth.JwtAuthorizationRule import tech.beshu.ror.accesscontrol.domain.* import tech.beshu.ror.accesscontrol.domain.GroupIdLike.GroupId +import tech.beshu.ror.accesscontrol.domain.Jwt.ClaimName import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.Reason.{MalformedValue, Message} import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.{DefinitionsLevelCreationError, RulesLevelCreationError} import tech.beshu.ror.providers.EnvVarProvider.EnvVarName import tech.beshu.ror.providers.EnvVarsProvider import tech.beshu.ror.unit.acl.factory.decoders.rules.BaseRuleSettingsDecoderTest import tech.beshu.ror.utils.TestsUtils.* +import tech.beshu.ror.utils.json.JsonPath import tech.beshu.ror.utils.uniquelist.UniqueNonEmptyList import java.security.KeyPairGenerator @@ -58,6 +60,7 @@ class JwtAuthorizationRuleSettingsTests | jwt: | | - name: jwt1 + | group_ids_claim: groups | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | |""".stripMargin, @@ -65,8 +68,7 @@ class JwtAuthorizationRuleSettingsTests rule.settings.jwt.id should be(JwtDef.Name("jwt1")) rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.settings.jwt.checkMethod shouldBe a[SignatureCheckMethod.Hmac] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(None) + rule.settings.jwt.groupsConfig should be(GroupsConfig(ClaimName(JsonPath("groups").get), None)) rule.settings.groupsLogic should be(GroupsLogic.AnyOf(GroupIds( UniqueNonEmptyList.of(GroupIdLike.from("group1*"), GroupId("group2")) ))) @@ -92,6 +94,7 @@ class JwtAuthorizationRuleSettingsTests | jwt: | | - name: jwt1 + | group_ids_claim: groups | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | |""".stripMargin, @@ -99,8 +102,7 @@ class JwtAuthorizationRuleSettingsTests rule.settings.jwt.id should be(JwtDef.Name("jwt1")) rule.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.settings.jwt.checkMethod shouldBe a[SignatureCheckMethod.Hmac] - rule.settings.jwt.userClaim should be(None) - rule.settings.jwt.groupsConfig should be(None) + rule.settings.jwt.groupsConfig should be(GroupsConfig(ClaimName(JsonPath("groups").get), None)) rule.settings.groupsLogic should be(GroupsLogic.AllOf(GroupIds( UniqueNonEmptyList.of(GroupIdLike.from("group1*"), GroupId("group2")) ))) @@ -124,6 +126,7 @@ class JwtAuthorizationRuleSettingsTests | jwt: | | - name: jwt1 + | group_ids_claim: groups | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | |""".stripMargin, @@ -150,12 +153,13 @@ class JwtAuthorizationRuleSettingsTests | jwt: | | - name: jwt1 + | user_claim: "userId" | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | |""".stripMargin, assertion = errors => { errors should have size 1 - errors.head should be(RulesLevelCreationError(Message("Cannot find JWT definition with name: jwt2"))) + errors.head should be(RulesLevelCreationError(Message("Cannot find jwt definition with name: jwt2"))) } ) } @@ -172,7 +176,7 @@ class JwtAuthorizationRuleSettingsTests |""".stripMargin, assertion = errors => { errors should have size 1 - errors.head should be(RulesLevelCreationError(Message("Cannot find JWT definition with name: jwt1"))) + errors.head should be(RulesLevelCreationError(Message("Cannot find jwt definition with name: jwt1"))) } ) } @@ -191,6 +195,7 @@ class JwtAuthorizationRuleSettingsTests | jwt: | | - name: jwt1 + | group_ids_claim: groups | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | |""".stripMargin, @@ -228,13 +233,14 @@ class JwtAuthorizationRuleSettingsTests | jwt: | | - name: jwt1 + | group_ids_claim: groups | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | |""".stripMargin, assertion = errors => { errors should have size 1 errors.head should be(RulesLevelCreationError(Message( - s"Please specify either '$groupsAnyOfKey' or '$groupsAllOfKey' for JWT authorization rule 'jwt1'" + s"Please specify either '$groupsAnyOfKey' or '$groupsAllOfKey' for jwt_authorization rule 'jwt1'" ))) } ) @@ -249,7 +255,8 @@ class JwtAuthorizationRuleSettingsTests | access_control_rules: | | - name: test_block1 - | jwt_authorization: jwt1 + | jwt: + | - name jwt1 | | jwt: | - signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" @@ -284,13 +291,14 @@ class JwtAuthorizationRuleSettingsTests | $groupsAllOfKey: ["groups1", "groups2"] | jwt: | - name: jwt2 + | group_ids_claim: groups | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | |""".stripMargin, assertion = errors => { errors should have size 1 errors.head should be(RulesLevelCreationError(Message( - s"Please specify either '$groupsAnyOfKey' or '$groupsAllOfKey' for JWT authorization rule 'jwt1'") + s"Please specify either '$groupsAnyOfKey' or '$groupsAllOfKey' for jwt_authorization rule 'jwt1'") )) } ) @@ -305,14 +313,18 @@ class JwtAuthorizationRuleSettingsTests | access_control_rules: | | - name: test_block1 - | jwt_authorization: jwt1 + | jwt_authorization: + | name: "jwt1" + | groups_any: ["group1","group2"] | | jwt: | | - name: jwt1 + | group_ids_claim: groups | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | | - name: jwt1 + | group_ids_claim: groups | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" | |""".stripMargin, diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/RorKbnAuthRuleSettingsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/RorKbnAuthRuleSettingsTests.scala index 7e9a293833..4ed2aaf6df 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/RorKbnAuthRuleSettingsTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/RorKbnAuthRuleSettingsTests.scala @@ -154,7 +154,7 @@ class RorKbnAuthRuleSettingsTests |""".stripMargin, assertion = errors => { errors should have size 1 - errors.head should be(RulesLevelCreationError(Message("Cannot find ROR Kibana definition with name: kbn1"))) + errors.head should be(RulesLevelCreationError(Message("Cannot find ror_kbn definition with name: kbn1"))) } ) } @@ -171,7 +171,7 @@ class RorKbnAuthRuleSettingsTests |""".stripMargin, assertion = errors => { errors should have size 1 - errors.head should be(RulesLevelCreationError(Message("Cannot find ROR Kibana definition with name: kbn1"))) + errors.head should be(RulesLevelCreationError(Message("Cannot find ror_kbn definition with name: kbn1"))) } ) } @@ -257,7 +257,7 @@ class RorKbnAuthRuleSettingsTests assertion = errors => { errors should have size 1 errors.head should be(RulesLevelCreationError(Message( - s"Please specify either '$groupsAnyOfKey' or '$groupsAllOfKey' for ROR Kibana authorization rule 'kbn1'") + s"Please specify either '$groupsAnyOfKey' or '$groupsAllOfKey' for ror_kbn_authorization rule 'kbn1'") )) } ) diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/RorKbnAuthenticationRuleSettingsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/RorKbnAuthenticationRuleSettingsTests.scala index b79f810c2b..1cebfa5f7f 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/RorKbnAuthenticationRuleSettingsTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/RorKbnAuthenticationRuleSettingsTests.scala @@ -259,7 +259,7 @@ class RorKbnAuthenticationRuleSettingsTests |""".stripMargin, assertion = errors => { errors should have size 1 - errors.head should be(RulesLevelCreationError(Message("Cannot find ROR Kibana definition with name: kbn1"))) + errors.head should be(RulesLevelCreationError(Message("Cannot find ror_kbn definition with name: kbn1"))) } ) } @@ -276,7 +276,7 @@ class RorKbnAuthenticationRuleSettingsTests |""".stripMargin, assertion = errors => { errors should have size 1 - errors.head should be(RulesLevelCreationError(Message("Cannot find ROR Kibana definition with name: kbn1"))) + errors.head should be(RulesLevelCreationError(Message("Cannot find ror_kbn definition with name: kbn1"))) } ) } @@ -362,7 +362,7 @@ class RorKbnAuthenticationRuleSettingsTests assertion = errors => { errors should have size 1 errors.head should be(RulesLevelCreationError(Message( - s"Please specify either '$groupsAnyOfKey' or '$groupsAllOfKey' for ROR Kibana authorization rule 'kbn1'") + s"Please specify either '$groupsAnyOfKey' or '$groupsAllOfKey' for ror_kbn_authorization rule 'kbn1'") )) } ) @@ -421,7 +421,7 @@ class RorKbnAuthenticationRuleSettingsTests } ) } - "RSA algorithm is defined but on signature key" in { + "RSA algorithm is defined but no signature key" in { assertDecodingFailure( yaml = """ diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/RorKbnAuthorizationRuleSettingsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/RorKbnAuthorizationRuleSettingsTests.scala index 8fc61d2c1b..e3755a557f 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/RorKbnAuthorizationRuleSettingsTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/RorKbnAuthorizationRuleSettingsTests.scala @@ -153,7 +153,7 @@ class RorKbnAuthorizationRuleSettingsTests |""".stripMargin, assertion = errors => { errors should have size 1 - errors.head should be(RulesLevelCreationError(Message("Cannot find ROR Kibana definition with name: kbn1"))) + errors.head should be(RulesLevelCreationError(Message("Cannot find ror_kbn definition with name: kbn1"))) } ) } @@ -170,7 +170,7 @@ class RorKbnAuthorizationRuleSettingsTests |""".stripMargin, assertion = errors => { errors should have size 1 - errors.head should be(RulesLevelCreationError(Message("Cannot find ROR Kibana definition with name: kbn1"))) + errors.head should be(RulesLevelCreationError(Message("Cannot find ror_kbn definition with name: kbn1"))) } ) } @@ -256,7 +256,7 @@ class RorKbnAuthorizationRuleSettingsTests assertion = errors => { errors should have size 1 errors.head should be(RulesLevelCreationError(Message( - s"Please specify either '$groupsAnyOfKey' or '$groupsAllOfKey' for ROR Kibana authorization rule 'kbn1'") + s"Please specify either '$groupsAnyOfKey' or '$groupsAllOfKey' for ror_kbn_authorization rule 'kbn1'") )) } ) From bf17fc149bf15ecab9a02abc87ad0719a652a533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Sat, 13 Dec 2025 22:23:42 +0100 Subject: [PATCH 11/15] qs --- .../rules/auth/JwtLikeRulesDecoders.scala | 2 +- .../rules/auth/JwtAuthRuleSettingsTests.scala | 48 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtLikeRulesDecoders.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtLikeRulesDecoders.scala index 3ff11bc495..5fc8aed29d 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtLikeRulesDecoders.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/rules/auth/JwtLikeRulesDecoders.scala @@ -157,7 +157,7 @@ trait JwtLikeRulesDecoders[ case None => val message = definitionOfOtherTypeOpt match { case Some(_) => - s"The $ruleTypePrefix definition with name $name exists, but cannot be used for ${ruleName.name.show} rule." + + s"The $ruleTypePrefix definition with name $name exists, but cannot be used for ${ruleName.name.show} rule. " + s"Please check in the documentation ($docsUrl) how to adjust the $ruleTypePrefix definition to use it for both authentication and authorization" case None => s"Cannot find $ruleTypePrefix definition with name: $name" diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthRuleSettingsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthRuleSettingsTests.scala index d6ab1b63cf..2addb64aa2 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthRuleSettingsTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthRuleSettingsTests.scala @@ -659,6 +659,54 @@ class JwtAuthRuleSettingsTests } ) } + "JWT definition found, but it is definition only for authentication and cannot be used for auth" in { + assertDecodingFailure( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_auth: jwt1 + | + | jwt: + | + | - name: jwt1 + | user_claim: "user" + | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" + | + |""".stripMargin, + assertion = errors => { + errors should have size 1 + errors.head should be(RulesLevelCreationError(Message("The jwt definition with name jwt1 exists, but cannot be used for jwt_auth rule. Please check in the documentation (https://docs.readonlyrest.com/elasticsearch#json-web-token-jwt-auth) how to adjust the jwt definition to use it for both authentication and authorization"))) + } + ) + } + "JWT definition found, but it is definition only for authorization and cannot be used for auth" in { + assertDecodingFailure( + yaml = + """ + |readonlyrest: + | + | access_control_rules: + | + | - name: test_block1 + | jwt_auth: jwt1 + | + | jwt: + | + | - name: jwt1 + | group_ids_claim: groups + | signature_key: "123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456.123456" + | + |""".stripMargin, + assertion = errors => { + errors should have size 1 + errors.head should be(RulesLevelCreationError(Message("The jwt definition with name jwt1 exists, but cannot be used for jwt_auth rule. Please check in the documentation (https://docs.readonlyrest.com/elasticsearch#json-web-token-jwt-auth) how to adjust the jwt definition to use it for both authentication and authorization"))) + } + ) + } "no JWT definition is defined" in { assertDecodingFailure( yaml = From 7e0eed638a006fc287787ac2bac573c082db4992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Sat, 13 Dec 2025 23:24:24 +0100 Subject: [PATCH 12/15] qs --- .../definitions/DefinitionsPack.scala | 4 ++-- .../definitions/JwtDefinitionsDecoder.scala | 2 +- .../factory/decoders/ruleDecoders.scala | 2 +- .../rules/auth/JwtAuthRuleSettingsTests.scala | 22 ++++++++----------- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/definitions/DefinitionsPack.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/definitions/DefinitionsPack.scala index 0fb7809624..9499dffba2 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/definitions/DefinitionsPack.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/definitions/DefinitionsPack.scala @@ -19,7 +19,6 @@ package tech.beshu.ror.accesscontrol.factory.decoders.definitions import cats.Show import tech.beshu.ror.accesscontrol.blocks.definitions.* import tech.beshu.ror.accesscontrol.blocks.definitions.ldap.LdapService -import tech.beshu.ror.accesscontrol.factory.decoders.definitions.Definitions.Item final case class DefinitionsPack(proxies: Definitions[ProxyAuth], users: Definitions[UserDef], @@ -31,8 +30,9 @@ final case class DefinitionsPack(proxies: Definitions[ProxyAuth], impersonators: Definitions[ImpersonatorDef], variableTransformationAliases: Definitions[VariableTransformationAliasDef]) -final case class Definitions[+ITEM <: Item](items: List[ITEM]) extends AnyVal +final case class Definitions[Item](items: List[Item]) extends AnyVal object Definitions { + trait Item { type Id def id: Id diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/definitions/JwtDefinitionsDecoder.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/definitions/JwtDefinitionsDecoder.scala index 89206b358e..e688c5dd25 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/definitions/JwtDefinitionsDecoder.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/definitions/JwtDefinitionsDecoder.scala @@ -47,7 +47,7 @@ object JwtDefinitionsDecoder { private def jwtDefDecoder(implicit httpClientFactory: HttpClientsFactory, variableCreator: RuntimeResolvableVariableCreator): Decoder[JwtDef] = { SyncDecoderCreator - .instance[JwtDef] { c => + .instance { c => for { name <- c.downField("name").as[Name] checkMethod <- signatureCheckMethod(c) diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/ruleDecoders.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/ruleDecoders.scala index 96ec44a6e3..268fa3b748 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/ruleDecoders.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/ruleDecoders.scala @@ -128,7 +128,7 @@ object ruleDecoders { impersonatorsDefinitions: Option[Definitions[ImpersonatorDef]], mocksProvider: MocksProvider, globalSettings: GlobalSettings): Option[RuleDecoder[Rule]] = { - lazy val optionalRuleDecoder = name match { + val optionalRuleDecoder = name match { case ExternalAuthorizationRule.Name.name => Some(new ExternalAuthorizationRuleDecoder(authorizationServiceDefinitions, impersonatorsDefinitions, mocksProvider, globalSettings)) case JwtAuthRule.Name.name => diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthRuleSettingsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthRuleSettingsTests.scala index 2addb64aa2..d973bb1bd4 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthRuleSettingsTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/auth/JwtAuthRuleSettingsTests.scala @@ -346,7 +346,7 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.authentication.settings.jwt.checkMethod shouldBe a[SignatureCheckMethod.Hmac] rule.authentication.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) - assertGroupsConfig(rule, GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + rule.authorization.settings.jwt.groupsConfig should be(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) } ) } @@ -376,7 +376,7 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.authentication.settings.jwt.checkMethod shouldBe a[SignatureCheckMethod.Hmac] rule.authentication.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) - assertGroupsConfig(rule, GroupsConfig( + rule.authorization.settings.jwt.groupsConfig should be(GroupsConfig( idsClaim = domain.Jwt.ClaimName(jsonPathFrom("groups")), namesClaim = Some(domain.Jwt.ClaimName(jsonPathFrom("group_names"))) )) @@ -407,7 +407,7 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Hmac] rule.authentication.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) - assertGroupsConfig(rule, GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("https://{domain}/claims/roles")), None)) + rule.authorization.settings.jwt.groupsConfig should be(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("https://{domain}/claims/roles")), None)) } ) } @@ -437,7 +437,7 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Rsa] rule.authentication.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) - assertGroupsConfig(rule, GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + rule.authorization.settings.jwt.groupsConfig should be(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) } ) } @@ -466,7 +466,7 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Rsa] rule.authentication.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) - assertGroupsConfig(rule, GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + rule.authorization.settings.jwt.groupsConfig should be(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) } ) } @@ -497,7 +497,7 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Rsa] rule.authentication.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) - assertGroupsConfig(rule, GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + rule.authorization.settings.jwt.groupsConfig should be(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) } ) } @@ -527,7 +527,7 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.authorizationTokenDef should be(AuthorizationTokenDef(Header.Name.authorization, "Bearer ")) rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.Ec] rule.authentication.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) - assertGroupsConfig(rule, GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) + rule.authorization.settings.jwt.groupsConfig should be(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) } ) } @@ -562,7 +562,7 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.NoCheck] rule.authentication.settings.jwt.checkMethod.asInstanceOf[SignatureCheckMethod.NoCheck].service shouldBe a[CacheableExternalAuthenticationServiceDecorator] rule.authentication.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) - assertGroupsConfig(rule, GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")),None)) + rule.authorization.settings.jwt.groupsConfig should be(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) } ) } @@ -600,7 +600,7 @@ class JwtAuthRuleSettingsTests rule.authentication.settings.jwt.checkMethod shouldBe a [SignatureCheckMethod.NoCheck] rule.authentication.settings.jwt.checkMethod.asInstanceOf[SignatureCheckMethod.NoCheck].service shouldBe a[CacheableExternalAuthenticationServiceDecorator] rule.authentication.settings.jwt.userClaim should be(domain.Jwt.ClaimName(jsonPathFrom("user"))) - assertGroupsConfig(rule, GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")),None)) + rule.authorization.settings.jwt.groupsConfig should be(GroupsConfig(domain.Jwt.ClaimName(jsonPathFrom("groups")), None)) } ) } @@ -1101,8 +1101,4 @@ class JwtAuthRuleSettingsTests val httpClientMock = mock[HttpClient] new MockHttpClientsFactoryWithFixedHttpClient(httpClientMock) } - - private def assertGroupsConfig(rule: JwtAuthRule, expected: GroupsConfig) = { - rule.authorization.settings.jwt.groupsConfig should be(expected) - } } From bac4597d4e6f34caf1a0619b67b2054d39a044a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Sat, 13 Dec 2025 23:29:20 +0100 Subject: [PATCH 13/15] qs --- .../ror/unit/acl/blocks/rules/auth/JwtAuthRuleTests.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthRuleTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthRuleTests.scala index 6428e64c8d..fdd2cb51e2 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthRuleTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/blocks/rules/auth/JwtAuthRuleTests.scala @@ -592,6 +592,8 @@ class JwtAuthRuleTests )(blockContext) } } + } + "not match" when { "preferred group is not on the groups list from JWT" in { val key: Key = Jwts.SIG.HS256.key().build() val jwt = Jwt(key, claims = List( @@ -610,8 +612,6 @@ class JwtAuthRuleTests preferredGroupId = Some(GroupId("group3")) ) } - } - "not match" when { "token has invalid HS256 signature" in { val key1: Key = Jwts.SIG.HS256.key().build() val key2: Key = Jwts.SIG.HS256.key().build() From b96205233a0e69da5ae4bc5f574986a97ff0879c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Sun, 14 Dec 2025 13:45:35 +0100 Subject: [PATCH 14/15] qs --- .../accesscontrol/blocks/RuleOrdering.scala | 2 + .../variables/runtime/VariableContext.scala | 2 + ...esolvingYamlLoadedAccessControlTests.scala | 62 +++++++++++++++++-- 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/RuleOrdering.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/RuleOrdering.scala index 29ff885cb2..adfc74344d 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/RuleOrdering.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/RuleOrdering.scala @@ -65,6 +65,7 @@ object RuleOrdering { // then we could check potentially slow async rules classOf[LdapAuthRule], classOf[LdapAuthenticationRule], + classOf[JwtAuthenticationRule], classOf[RorKbnAuthenticationRule], classOf[ExternalAuthenticationRule], classOf[AnyOfGroupsRule], @@ -74,6 +75,7 @@ object RuleOrdering { classOf[CombinedLogicGroupsRule], // all authorization rules should be placed after any authentication rule classOf[LdapAuthorizationRule], + classOf[JwtAuthorizationRule], classOf[RorKbnAuthorizationRule], classOf[ExternalAuthorizationRule], // Inspection rules next; these act based on properties of the request. diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/variables/runtime/VariableContext.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/variables/runtime/VariableContext.scala index f26455a01f..42c4e8a2d6 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/variables/runtime/VariableContext.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/blocks/variables/runtime/VariableContext.scala @@ -144,6 +144,8 @@ object VariableContext { rulesBefore .collect { case rule: JwtAuthRule => rule + case rule: JwtAuthenticationRule => rule + case rule: JwtAuthorizationRule => rule case rule: RorKbnAuthRule => rule case rule: RorKbnAuthenticationRule => rule case rule: RorKbnAuthorizationRule => rule diff --git a/core/src/test/scala/tech/beshu/ror/integration/VariableResolvingYamlLoadedAccessControlTests.scala b/core/src/test/scala/tech/beshu/ror/integration/VariableResolvingYamlLoadedAccessControlTests.scala index a25542054b..2bf76cc3ab 100644 --- a/core/src/test/scala/tech/beshu/ror/integration/VariableResolvingYamlLoadedAccessControlTests.scala +++ b/core/src/test/scala/tech/beshu/ror/integration/VariableResolvingYamlLoadedAccessControlTests.scala @@ -69,7 +69,7 @@ class VariableResolvingYamlLoadedAccessControlTests extends AnyWordSpec | type: allow | auth_key: admin:container | - | - name: "Kibana metadata resolving test" + | - name: "Kibana metadata resolving test (with jwt_auth)" | type: allow | users: ["user9"] | jwt_auth: @@ -81,6 +81,16 @@ class VariableResolvingYamlLoadedAccessControlTests extends AnyWordSpec | b: "@{jwt:user_id_list}" | c: "jwt_value_transformed_@{jwt:tech.beshu.mainGroupsString}#{replace_first(\\"j\\",\\"g\\").to_uppercase}" | + | - name: "Kibana metadata resolving test (with jwt_authentication)" + | type: allow + | users: ["user9"] + | jwt_authentication: + | name: "jwt3" + | kibana: + | access: ro + | metadata: + | b: "@{jwt:user_id_list}" + | | - name: "Group id from header variable" | type: allow | groups: ["g4", "@{X-my-group-id-1}", "@{header:X-my-group-id-2}#{func(custom_replace)}" ] @@ -99,6 +109,12 @@ class VariableResolvingYamlLoadedAccessControlTests extends AnyWordSpec | jwt_auth: "jwt3" | users: ["user5"] | + | - name: "Variables usage in filter - authentication" + | type: allow + | filter: '{"bool": { "must": { "terms": { "user_id": [@{jwt:user_id_list}] }}}}' + | jwt_authentication: "jwt3" + | users: ["user5"] + | | - name: "Group id from jwt variable (array)" | type: allow | jwt_auth: @@ -266,6 +282,8 @@ class VariableResolvingYamlLoadedAccessControlTests extends AnyWordSpec .empty .withLoggedUser(DirectlyLoggedUser(User.Id("user3"))) .withJwtToken(domain.Jwt.Payload(jwt.defaultClaims())) + .withCurrentGroupId(GroupId("j1")) + .withAvailableGroups(UniqueList.of(group("j1"), group("j2"))) ) blockContext.filteredIndices should be(Set(requestedIndex("gjj1"))) blockContext.responseHeaders should be(Set.empty) @@ -293,6 +311,8 @@ class VariableResolvingYamlLoadedAccessControlTests extends AnyWordSpec .from(request) .withLoggedUser(DirectlyLoggedUser(User.Id("user4"))) .withJwtToken(domain.Jwt.Payload(jwt.defaultClaims())) + .withCurrentGroupId(GroupId("j0,j3")) + .withAvailableGroups(UniqueList.of(group("j0,j3"))) ) blockContext.filteredIndices should be(Set(requestedIndex("gj0"))) blockContext.responseHeaders should be(Set.empty) @@ -301,7 +321,8 @@ class VariableResolvingYamlLoadedAccessControlTests extends AnyWordSpec "JWT variable in filter query is used" in { val jwt = Jwt(secret, claims = List( "userId" := "user5", - "user_id_list" := List("alice", "bob") + "user_id_list" := List("alice", "bob"), + "tech" :-> "beshu" :-> "mainGroupsString" := "j0,j3" )) val request = MockRequestContext.search @@ -320,6 +341,8 @@ class VariableResolvingYamlLoadedAccessControlTests extends AnyWordSpec .from(request) .withLoggedUser(DirectlyLoggedUser(User.Id("user5"))) .withJwtToken(domain.Jwt.Payload(jwt.defaultClaims())) + .withCurrentGroupId(GroupId("j0,j3")) + .withAvailableGroups(UniqueList.of(group("j0,j3"))) ) blockContext.filteredIndices should be(Set.empty) blockContext.responseHeaders should be(Set.empty) @@ -354,7 +377,36 @@ class VariableResolvingYamlLoadedAccessControlTests extends AnyWordSpec blockContext.filter should be(Some(Filter("""{"bool": { "must": { "terms": { "group_id": ["g1","g3"] }}}}"""))) } } - "kibana.metadata has variables used" in { + "kibana.metadata has variables used - without groups in token" in { + val jwt = Jwt(secret, claims = List( + "userId" := "user9", + "user_id_list" := List("alice", "bob"), + )) + + val request = MockRequestContext.search.withHeaders(bearerHeader(jwt)) + + val result = acl.handleRegularRequest(request).runSyncUnsafe() + + inside(result.result) { + case RegularRequestResult.Allow(blockContext, block) => + block.name should be(Block.Name("Kibana metadata resolving test (with jwt_authentication)")) + blockContext.userMetadata should be( + UserMetadata + .from(request) + .withLoggedUser(DirectlyLoggedUser(User.Id("user9"))) + .withKibanaAccess(KibanaAccess.RO) + .withKibanaIndex(ClusterIndexName.Local.kibanaDefault) + .withKibanaMetadata( + JsonTree.Object(Map( + "b" -> JsonTree.Value(JsonValue.StringValue("\"alice\",\"bob\"")), + )) + ) + .withJwtToken(domain.Jwt.Payload(jwt.defaultClaims())) + ) + blockContext.responseHeaders should be(Set.empty) + } + } + "kibana.metadata has variables used - with groups in token" in { val jwt = Jwt(secret, claims = List( "userId" := "user9", "user_id_list" := List("alice", "bob"), @@ -367,7 +419,7 @@ class VariableResolvingYamlLoadedAccessControlTests extends AnyWordSpec inside(result.result) { case RegularRequestResult.Allow(blockContext, block) => - block.name should be(Block.Name("Kibana metadata resolving test")) + block.name should be(Block.Name("Kibana metadata resolving test (with jwt_auth)")) blockContext.userMetadata should be( UserMetadata .from(request) @@ -382,6 +434,8 @@ class VariableResolvingYamlLoadedAccessControlTests extends AnyWordSpec )) ) .withJwtToken(domain.Jwt.Payload(jwt.defaultClaims())) + .withCurrentGroupId(GroupId("j0,j3")) + .withAvailableGroups(UniqueList.of(group("j0,j3"))) ) blockContext.responseHeaders should be(Set.empty) } From 48b18d882df9203d77b9a86146bb5aa4de1d5acb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Sun, 14 Dec 2025 18:15:52 +0100 Subject: [PATCH 15/15] qs --- core/build.gradle | 2 +- .../src/test/resources/jwt_auth/readonlyrest.yml | 6 +++--- integration-tests/src/test/resources/misc/readonlyrest.yml | 2 +- tests-utils/build.gradle | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/build.gradle b/core/build.gradle index d1c527e78d..cfd69ef94a 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -102,7 +102,7 @@ dependencies { testImplementation group: 'org.scalamock', name: 'scalamock_3', version: '6.0.0' testImplementation group: 'org.scalatestplus', name: 'scalacheck-1-16_3', version: '3.2.14.0' testImplementation group: 'org.scalatest', name: 'scalatest_3', version: '3.2.16' - testImplementation group: 'com.dimafeng', name: 'testcontainers-scala-core_3', version: '0.41.4' + testImplementation group: 'com.dimafeng', name: 'testcontainers-scala-core_3', version: '0.44.0' constraints { api group: 'io.netty', name: 'netty-codec-http2', version: '4.1.126.Final' diff --git a/integration-tests/src/test/resources/jwt_auth/readonlyrest.yml b/integration-tests/src/test/resources/jwt_auth/readonlyrest.yml index ac069b9b08..4ddaaaed66 100644 --- a/integration-tests/src/test/resources/jwt_auth/readonlyrest.yml +++ b/integration-tests/src/test/resources/jwt_auth/readonlyrest.yml @@ -7,11 +7,11 @@ readonlyrest: - name: Valid JWT token is present type: allow - jwt_auth: "jwt1" + jwt_authentication: "jwt1" - name: Valid JWT token is present in custom header type: allow - jwt_auth: "jwt2" + jwt_authentication: "jwt2" - name: Valid JWT token is present with roles type: allow @@ -21,7 +21,7 @@ readonlyrest: - name: Valid JWT token is present in custom header and header prefix type: allow - jwt_auth: "jwt4" + jwt_authentication: "jwt4" jwt: - name: jwt1 diff --git a/integration-tests/src/test/resources/misc/readonlyrest.yml b/integration-tests/src/test/resources/misc/readonlyrest.yml index 91d6bcfc1e..ce727a801e 100644 --- a/integration-tests/src/test/resources/misc/readonlyrest.yml +++ b/integration-tests/src/test/resources/misc/readonlyrest.yml @@ -22,7 +22,7 @@ readonlyrest: - name: "Access for data of selected users" filter: '{"bool": { "must": { "terms": { "user_id": [@{jwt:user_id_list}] }}}}' - jwt_auth: "jwt1" + jwt_authentication: "jwt1" indices: ["index1"] jwt: diff --git a/tests-utils/build.gradle b/tests-utils/build.gradle index 3622843868..803c4c8880 100644 --- a/tests-utils/build.gradle +++ b/tests-utils/build.gradle @@ -74,7 +74,7 @@ dependencies { api group: 'org.scala-lang.modules' , name: 'scala-parallel-collections_3', version: '1.0.4' api group: 'com.typesafe.scala-logging', name: 'scala-logging_3', version: '3.9.5' api group: 'org.scalatest', name: 'scalatest_3', version: '3.2.19' - api group: 'com.dimafeng', name: 'testcontainers-scala-scalatest_3', version: '0.43.0' + api group: 'com.dimafeng', name: 'testcontainers-scala-scalatest_3', version: '0.44.0' api group: 'org.testcontainers', name: 'testcontainers', version: "1.20.6" api group: 'eu.rekawek.toxiproxy', name: 'toxiproxy-java', version: "2.1.7" api group: 'com.unboundid', name: 'unboundid-ldapsdk', version: '6.0.11'