Skip to content

Commit

Permalink
[RORDEV-539] additional validation in kibana rule (#931)
Browse files Browse the repository at this point in the history
  • Loading branch information
coutoPL authored Sep 10, 2023
1 parent 11a3211 commit 14aef36
Show file tree
Hide file tree
Showing 121 changed files with 1,565 additions and 519 deletions.
1 change: 1 addition & 0 deletions core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ dependencies {
api group: 'io.monix', name: 'monix_2.13', version: '3.4.1'
api group: 'eu.timepit', name: 'refined_2.13', version: '0.9.5'
api group: 'org.reflections', name: 'reflections', version: '0.9.11'
api group: 'org.mozilla', name: 'rhino', version: '1.7.14'
api group: 'com.github.blemale', name: 'scaffeine_2.13', version: '3.1.0'
api group: 'com.github.tototoshi', name: 'scala-csv_2.13', version: '1.3.6'
api group: 'org.scala-lang.modules', name: 'scala-parallel-collections_2.13', version: '1.0.4'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import cats.implicits._
import tech.beshu.ror.accesscontrol.domain.Json._
import tech.beshu.ror.accesscontrol.domain.KibanaAllowedApiPath.AllowedHttpMethod
import tech.beshu.ror.accesscontrol.domain.KibanaAllowedApiPath.AllowedHttpMethod.HttpMethod
import tech.beshu.ror.accesscontrol.domain.{CorrelationId, KibanaAccess}
import tech.beshu.ror.accesscontrol.domain.{CorrelationId, KibanaAccess, KibanaApp}

import scala.jdk.CollectionConverters._

Expand Down Expand Up @@ -72,7 +72,13 @@ object MetadataValue {
private def hiddenKibanaApps(userMetadata: UserMetadata) = {
NonEmptyList
.fromList(userMetadata.hiddenKibanaApps.toList)
.map(apps => ("x-ror-kibana-hidden-apps", MetadataList(apps.map(_.value.value))))
.map(apps => (
"x-ror-kibana-hidden-apps",
MetadataList(apps.map {
case KibanaApp.FullNameKibanaApp(name) => name.value
case KibanaApp.KibanaAppRegex(regex) => regex.value.value
})
))
.toMap
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,31 @@ package tech.beshu.ror.accesscontrol.domain

import cats.Eq
import eu.timepit.refined.types.string.NonEmptyString
import tech.beshu.ror.accesscontrol.domain.JsRegex.CompilationResult
import tech.beshu.ror.accesscontrol.domain.KibanaAllowedApiPath.AllowedHttpMethod
import tech.beshu.ror.utils.js.JsCompiler

final case class KibanaApp(value: NonEmptyString)
sealed trait KibanaApp
object KibanaApp {
implicit val eqKibanaApps: Eq[KibanaApp] = Eq.fromUniversalEquals
final case class FullNameKibanaApp(name: NonEmptyString) extends KibanaApp
final case class KibanaAppRegex(regex: JsRegex) extends KibanaApp

def from(str: NonEmptyString)
(implicit jsCompiler: JsCompiler): Either[String, KibanaApp] = {
JsRegex.compile(str) match {
case Right(jsRegex) => Right(KibanaApp.KibanaAppRegex(jsRegex))
case Left(CompilationResult.NotRegex) => Right(KibanaApp.FullNameKibanaApp(str))
case Left(CompilationResult.SyntaxError) => Left(s"Cannot compile [${str.value}] as a JS regex (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions)")
}
}

implicit val eqKibanaApps: Eq[KibanaApp] = Eq.by {
case FullNameKibanaApp(name) => name.value
case KibanaAppRegex(regex) => regex.value.value
}
}

final case class KibanaAllowedApiPath(httpMethod: AllowedHttpMethod, pathRegex: Regex)
final case class KibanaAllowedApiPath(httpMethod: AllowedHttpMethod, pathRegex: JavaRegex)
object KibanaAllowedApiPath {

sealed trait AllowedHttpMethod
Expand Down
53 changes: 47 additions & 6 deletions core/src/main/scala/tech/beshu/ror/accesscontrol/domain/misc.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ import cats.data.NonEmptyList
import eu.timepit.refined.auto._
import eu.timepit.refined.types.string.NonEmptyString
import io.lemonlabs.uri.Uri
import org.apache.logging.log4j.scala.Logging
import tech.beshu.ror.accesscontrol.blocks.variables.runtime.RuntimeSingleResolvableVariable
import tech.beshu.ror.utils.js.JsCompiler

import java.util.regex
import scala.util.Try
import scala.util.{Failure, Success, Try}

final case class RorAuditLoggerName(value: NonEmptyString)
object RorAuditLoggerName {
Expand All @@ -44,21 +46,60 @@ object AuditCluster {
final case class RemoteAuditCluster(uris: NonEmptyList[Uri]) extends AuditCluster
}

final case class Regex private(value: String) {
final case class JavaRegex private(value: String) {
val pattern: regex.Pattern = regex.Pattern.compile(value)
}
object Regex {
object JavaRegex {
private val specialChars = """<([{\^-=$!|]})?*+.>"""

def compile(value: String): Try[Regex] = Try(new Regex(value))
def buildFromLiteral(value: String): Regex = {
def compile(value: String): Try[JavaRegex] = Try(new JavaRegex(value))

def buildFromLiteral(value: String): JavaRegex = {
val escapedValue = value
.map {
case c if specialChars.contains(c) => s"""\\$c"""
case c => c
}
.mkString
new Regex(s"^$escapedValue$$")
new JavaRegex(s"^$escapedValue$$")
}
}

final case class JsRegex private(value: NonEmptyString)
object JsRegex extends Logging {
private val extractRawRegex = """\/(.*)\/""".r

def compile(str: NonEmptyString)
(implicit jsCompiler: JsCompiler): Either[CompilationResult, JsRegex] = {
if(validateInput(str)) {
str.value match {
case extractRawRegex(regex) =>
jsCompiler.compile(s"new RegExp('$regex')") match {
case Success(_) =>
Right(new JsRegex(str))
case Failure(ex) =>
logger.error("JS compiler error", ex)
Left(CompilationResult.SyntaxError)
}
case _ =>
Left(CompilationResult.NotRegex)
}
} else {
Left(CompilationResult.SyntaxError)
}
}

private def validateInput(str: NonEmptyString) = {
doesNotContainEndOfFunctionInvocation(str) && isNotMultilineString(str)
}

private def doesNotContainEndOfFunctionInvocation(str: NonEmptyString) = !str.contains(");")
private def isNotMultilineString(str: NonEmptyString) = !str.contains("\n")

sealed trait CompilationResult
object CompilationResult {
case object NotRegex extends CompilationResult
case object SyntaxError extends CompilationResult
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import tech.beshu.ror.accesscontrol.utils.SyncDecoderCreator
import tech.beshu.ror.com.jayway.jsonpath.JsonPath
import tech.beshu.ror.utils.LoggerOps._
import tech.beshu.ror.utils.ScalaOps._
import tech.beshu.ror.utils.js.JsCompiler
import tech.beshu.ror.utils.uniquelist.UniqueNonEmptyList

import java.net.URI
Expand Down Expand Up @@ -309,7 +310,13 @@ object common extends Logging {
}
.decoder

implicit val kibanaApp: Decoder[KibanaApp] = nonEmptyStringDecoder.map(KibanaApp.apply)
implicit def kibanaAppDecoder(implicit jsCompiler: JsCompiler): Decoder[KibanaApp] =
nonEmptyStringDecoder
.toSyncDecoder
.emapE[KibanaApp](str =>
KibanaApp.from(str).left.map(error => CoreCreationError.ValueLevelCreationError(Message(error)))
)
.decoder

implicit val groupsLogicAndDecoder: Decoder[GroupsLogic.And] =
permittedGroupsDecoder.map(GroupsLogic.And.apply)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ object ruleDecoders {
case HeadersOrRule.Name.name => Some(HeadersOrRuleDecoder)
case HostsRule.Name.name => Some(new HostsRuleDecoder(variableCreator))
case IndicesRule.Name.name => Some(new IndicesRuleDecoders(variableCreator, environmentConfig.uniqueIdentifierGenerator))
case KibanaUserDataRule.Name.name => Some(new KibanaUserDataRuleDecoder(globalSettings.configurationIndex, variableCreator))
case KibanaUserDataRule.Name.name => Some(new KibanaUserDataRuleDecoder(globalSettings.configurationIndex, variableCreator)(environmentConfig.jsCompiler))
case KibanaAccessRule.Name.name => Some(new KibanaAccessRuleDecoder(globalSettings.configurationIndex))
case KibanaHideAppsRule.Name.name => Some(KibanaHideAppsRuleDecoder)
case KibanaHideAppsRule.Name.name => Some(new KibanaHideAppsRuleDecoder()(environmentConfig.jsCompiler))
case KibanaIndexRule.Name.name => Some(new KibanaIndexRuleDecoder(variableCreator))
case KibanaTemplateIndexRule.Name.name => Some(new KibanaTemplateIndexRuleDecoder(variableCreator))
case LocalHostsRule.Name.name => Some(new LocalHostsRuleDecoder(variableCreator))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package tech.beshu.ror.accesscontrol.factory.decoders.rules.kibana

import cats.implicits._
import io.circe.Decoder
import tech.beshu.ror.accesscontrol.blocks.Block.RuleDefinition
import tech.beshu.ror.accesscontrol.blocks.rules.kibana.KibanaHideAppsRule.Settings
Expand All @@ -25,14 +26,17 @@ import tech.beshu.ror.accesscontrol.domain.{KibanaAccess, KibanaApp, KibanaIndex
import tech.beshu.ror.accesscontrol.factory.decoders.common._
import tech.beshu.ror.accesscontrol.factory.decoders.rules.RuleBaseDecoder.RuleBaseDecoderWithoutAssociatedFields
import tech.beshu.ror.accesscontrol.utils.CirceOps._
import tech.beshu.ror.utils.js.JsCompiler
import tech.beshu.ror.utils.uniquelist.UniqueNonEmptyList

object KibanaHideAppsRuleDecoder
class KibanaHideAppsRuleDecoder(implicit jsCompiler: JsCompiler)
extends RuleBaseDecoderWithoutAssociatedFields[KibanaHideAppsRule] {

override protected def decoder: Decoder[RuleDefinition[KibanaHideAppsRule]] = {
DecoderHelpers
.decodeNonEmptyStringLikeOrUniqueNonEmptyList(KibanaApp.apply)
.map(apps => new KibanaHideAppsRule(Settings(apps)))
.decodeNonEmptyStringLikeOrUniqueNonEmptyList(identity)
.emap(_.toList.map(KibanaApp.from).traverse(identity))
.map(apps => new KibanaHideAppsRule(Settings(UniqueNonEmptyList.unsafeFromIterable(apps))))
.map(RuleDefinition.create(_))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,20 @@ import tech.beshu.ror.accesscontrol.blocks.variables.runtime.{RuntimeResolvableV
import tech.beshu.ror.accesscontrol.domain.Json.ResolvableJsonRepresentation
import tech.beshu.ror.accesscontrol.domain.KibanaAllowedApiPath.AllowedHttpMethod.HttpMethod
import tech.beshu.ror.accesscontrol.domain.KibanaAllowedApiPath._
import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, KibanaAccess, KibanaAllowedApiPath, KibanaApp, KibanaIndexName, Regex, RorConfigurationIndex}
import tech.beshu.ror.accesscontrol.domain.{ClusterIndexName, JavaRegex, KibanaAccess, KibanaAllowedApiPath, KibanaApp, KibanaIndexName, RorConfigurationIndex}
import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError
import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.Reason.Message
import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.{RulesLevelCreationError, ValueLevelCreationError}
import tech.beshu.ror.accesscontrol.factory.decoders.common._
import tech.beshu.ror.accesscontrol.factory.decoders.rules.RuleBaseDecoder.RuleBaseDecoderWithoutAssociatedFields
import tech.beshu.ror.accesscontrol.utils.CirceOps._
import tech.beshu.ror.utils.js.JsCompiler

import scala.util.{Failure, Success}

class KibanaUserDataRuleDecoder(configurationIndex: RorConfigurationIndex,
variableCreator: RuntimeResolvableVariableCreator)
(implicit jsCompiler: JsCompiler)
extends RuleBaseDecoderWithoutAssociatedFields[KibanaUserDataRule]
with Logging {

Expand Down Expand Up @@ -83,14 +85,14 @@ class KibanaUserDataRuleDecoder(configurationIndex: RorConfigurationIndex,
val extendedKibanaAllowedApiDecoder: Decoder[KibanaAllowedApiPath] = Decoder.instance { c =>
for {
httpMethod <- c.downField("http_method").as[HttpMethod]
httpPath <- c.downField("http_path").as[Regex]
httpPath <- c.downField("http_path").as[JavaRegex]
} yield KibanaAllowedApiPath(AllowedHttpMethod.Specific(httpMethod), httpPath)
}

extendedKibanaAllowedApiDecoder.or(simpleKibanaAllowedApiPathDecoder)
}

private implicit lazy val pathRegexDecoder: Decoder[Regex] =
private implicit lazy val pathRegexDecoder: Decoder[JavaRegex] =
Decoder
.decodeString
.toSyncDecoder
Expand Down Expand Up @@ -120,15 +122,15 @@ class KibanaUserDataRuleDecoder(configurationIndex: RorConfigurationIndex,

private def pathRegexFrom(str: NonEmptyString) = {
if (str.value.startsWith("^") && str.value.endsWith("$")) {
Regex.compile(str.value) match {
JavaRegex.compile(str.value) match {
case Success(regex) =>
Right(regex)
case Failure(exception) =>
logger.error(s"Cannot compile regex from string: [$str]", exception)
Left(ValueLevelCreationError(Message(s"Cannot create Kibana allowed API path regex from [$str]")))
}
} else {
Right(Regex.buildFromLiteral(str.value))
Right(JavaRegex.buildFromLiteral(str.value))
}
}
}
10 changes: 8 additions & 2 deletions core/src/main/scala/tech/beshu/ror/accesscontrol/ops.scala
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,10 @@ object orders {
implicit val idPatternOrder: Order[UserIdPattern] = Order.by(_.value)
implicit val methodOrder: Order[Method] = Order.by(_.m)
implicit val apiKeyOrder: Order[ApiKey] = Order.by(_.value)
implicit val kibanaAppOrder: Order[KibanaApp] = Order.by(_.value)
implicit val kibanaAppOrder: Order[KibanaApp] = Order.by {
case KibanaApp.FullNameKibanaApp(name) => name.value
case KibanaApp.KibanaAppRegex(regex) => regex.value.value
}
implicit val documentFieldOrder: Order[DocumentField] = Order.by(_.value)
implicit val actionOrder: Order[Action] = Order.by(_.value)
implicit val authKeyOrder: Order[PlainTextSecret] = Order.by(_.value)
Expand Down Expand Up @@ -167,7 +170,10 @@ object show {
implicit val uriShow: Show[Uri] = Show.show(_.toJavaUri.toString())
implicit val lemonUriShow: Show[LemonUri] = Show.show(_.toString())
implicit val headerNameShow: Show[Header.Name] = Show.show(_.value.value)
implicit val kibanaAppShow: Show[KibanaApp] = Show.show(_.value.value)
implicit val kibanaAppShow: Show[KibanaApp] = Show.show {
case KibanaApp.FullNameKibanaApp(name) => name.value
case KibanaApp.KibanaAppRegex(regex) => regex.value.value
}
implicit val kibanaAllowedApiPathShow: Show[KibanaAllowedApiPath] = Show.show { p =>
val httpMethodStr = p.httpMethod match {
case AllowedHttpMethod.Any => "*"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,12 @@ sealed abstract class AsyncDecoder[A] extends ADecoder[Task, A] {

object AsyncDecoderCreator extends ADecoderCreator[Task, AsyncDecoder] {

def from[A](value: => Task[A]): AsyncDecoder[A] = new AsyncDecoder[A] {
override def apply(c: HCursor): Task[Either[DecodingFailure, A]] = {
value.map(Right.apply)
}
}

def from[A](decoder: ADecoder[Id, A]): AsyncDecoder[A] = new AsyncDecoder[A] {
override def apply(c: HCursor): Task[Either[DecodingFailure, A]] = Task(decoder.apply(c))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package tech.beshu.ror.configuration
import tech.beshu.ror.accesscontrol.blocks.variables.transformation.SupportedVariablesFunctions
import tech.beshu.ror.accesscontrol.matchers.{RandomBasedUniqueIdentifierGenerator, UniqueIdentifierGenerator}
import tech.beshu.ror.providers._
import tech.beshu.ror.utils.js.{JsCompiler, MozillaJsCompiler}

import java.time.Clock

Expand All @@ -27,15 +28,18 @@ final case class EnvironmentConfig(clock: Clock,
propertiesProvider: PropertiesProvider,
uniqueIdentifierGenerator: UniqueIdentifierGenerator,
uuidProvider: UuidProvider,
jsCompiler: JsCompiler,
variablesFunctions: SupportedVariablesFunctions)

object EnvironmentConfig {

val default: EnvironmentConfig = EnvironmentConfig(
clock = Clock.systemUTC(),
envVarsProvider = OsEnvVarsProvider,
propertiesProvider = JvmPropertiesProvider,
uniqueIdentifierGenerator = RandomBasedUniqueIdentifierGenerator,
uuidProvider = JavaUuidProvider,
jsCompiler = MozillaJsCompiler,
variablesFunctions = SupportedVariablesFunctions.default,
)
}
28 changes: 28 additions & 0 deletions core/src/main/scala/tech/beshu/ror/utils/js/JsCompiler.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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.utils.js

import scala.concurrent.duration.{DurationInt, FiniteDuration}
import scala.language.postfixOps
import scala.util.Try

trait JsCompiler {

def compile(jsCodeString: String)
(implicit timeout: FiniteDuration = 10 seconds): Try[Unit]
}

Loading

0 comments on commit 14aef36

Please sign in to comment.