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 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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