From 67b1d532caa52af848fb8e776114863a37d50101 Mon Sep 17 00:00:00 2001 From: "Francois @fanf42 Armand" Date: Wed, 6 Jul 2016 16:00:57 +0200 Subject: [PATCH] Fixes #8629: Allows generation-time javascript eval in directive parameters --- .../domain/appconfig/RudderWebProperty.scala | 31 +- .../services/policies/DeploymentService.scala | 134 ++-- .../services/policies/JavascriptEngine.scala | 708 ++++++++++++++++++ .../services/policies/TestJsEngine.scala | 336 +++++++++ .../scala/bootstrap/liftweb/AppConfig.scala | 1 + .../rudder/appconfig/ConfigService.scala | 36 + .../administration/PropertiesManagement.scala | 49 ++ .../policyServerManagement.html | 80 +- 8 files changed, 1293 insertions(+), 82 deletions(-) create mode 100644 rudder-core/src/main/scala/com/normation/rudder/services/policies/JavascriptEngine.scala create mode 100644 rudder-core/src/test/scala/com/normation/rudder/services/policies/TestJsEngine.scala diff --git a/rudder-core/src/main/scala/com/normation/rudder/domain/appconfig/RudderWebProperty.scala b/rudder-core/src/main/scala/com/normation/rudder/domain/appconfig/RudderWebProperty.scala index 78d85feacbc..a53ad708605 100644 --- a/rudder-core/src/main/scala/com/normation/rudder/domain/appconfig/RudderWebProperty.scala +++ b/rudder-core/src/main/scala/com/normation/rudder/domain/appconfig/RudderWebProperty.scala @@ -39,6 +39,10 @@ package com.normation.rudder.domain.appconfig import com.normation.utils.HashcodeCaching import java.util.regex.Pattern +import net.liftweb.common.Full +import net.liftweb.common.Failure +import net.liftweb.common.Box +import ca.mrvisser.sealerate case class RudderWebPropertyName(value:String) extends HashcodeCaching @@ -53,4 +57,29 @@ case class RudderWebProperty( name : RudderWebPropertyName , value : String , description: String -) \ No newline at end of file +) + + +/** + * A little domain language for feature switches + * (just enabled/disabled with the parsing) + */ +sealed trait FeatureSwitch { def name: String } +object FeatureSwitch { + + final case object Enabled extends FeatureSwitch { override val name = "enabled" } + final case object Disabled extends FeatureSwitch { override val name = "disabled" } + + final val all: Set[FeatureSwitch] = sealerate.values[FeatureSwitch] + + def parse(value: String): Box[FeatureSwitch] = { + value match { + case null|"" => Failure("An empty or null string can not be parsed as a feature switch status") + case s => s.trim.toLowerCase match { + case Enabled.name => Full(Enabled) + case Disabled.name => Full(Disabled) + case _ => Failure(s"Cannot parse the given value as a valid feature switch status: '${value}'. Authorised values are: '${all.map( _.name).mkString(", ")}'") + } + } + } +} diff --git a/rudder-core/src/main/scala/com/normation/rudder/services/policies/DeploymentService.scala b/rudder-core/src/main/scala/com/normation/rudder/services/policies/DeploymentService.scala index 48a3c132185..d29fdf6204c 100644 --- a/rudder-core/src/main/scala/com/normation/rudder/services/policies/DeploymentService.scala +++ b/rudder-core/src/main/scala/com/normation/rudder/services/policies/DeploymentService.scala @@ -78,6 +78,10 @@ import com.normation.rudder.reports.AgentRunInterval import com.normation.rudder.domain.logger.ComplianceDebugLogger import com.normation.rudder.services.reports.CachedFindRuleNodeStatusReports import com.normation.cfclerk.domain.BundleOrder +import javax.script.ScriptEngine +import javax.script.ScriptEngineManager +import com.normation.rudder.domain.appconfig.FeatureSwitch +import com.normation.inventory.domain.AixOS @@ -123,6 +127,7 @@ trait DeploymentService extends Loggable { agentRunSplaytime <- getAgentRunSplaytime() ?~! "Could not get agent run splaytime" agentRunStartMinute <- getAgentRunStartMinute() ?~! "Could not get agent run start time (minute)" agentRunStartHour <- getAgentRunStartHour() ?~! "Could not get agent run start time (hour)" + scriptEngineEnabled <- getScriptEngineEnabled() ?~! "Could not get if we should use the script engine to evaluate directive parameters" fetch6Time = System.currentTimeMillis _ = logger.trace(s"Fetched run infos in ${fetch4Time-fetch3Time}ms") @@ -155,6 +160,7 @@ trait DeploymentService extends Loggable { , globalSystemVariables , globalRunInterval , globalComplianceMode + , scriptEngineEnabled ) ?~! "Cannot build target configuration node" timeBuildConfig = (System.currentTimeMillis - buildConfigTime) _ = logger.debug(s"Node's target configuration built in ${timeBuildConfig}, start to update rule values.") @@ -230,6 +236,8 @@ trait DeploymentService extends Loggable { def getAgentRunSplaytime : () => Box[Int] def getAgentRunStartHour : () => Box[Int] def getAgentRunStartMinute : () => Box[Int] + def getScriptEngineEnabled : () => Box[FeatureSwitch] + /** * Find all modified rules. * For them, find all directives with variables @@ -270,6 +278,7 @@ trait DeploymentService extends Loggable { , globalSystemVariable : Map[String, Variable] , globalAgentRun : AgentRunInterval , globalComplianceMode : ComplianceMode + , scriptEngineEnabled : FeatureSwitch ) : Box[(Seq[NodeConfiguration])] /** @@ -383,6 +392,7 @@ class DeploymentServiceImpl ( , override val getAgentRunSplaytime: () => Box[Int] , override val getAgentRunStartHour: () => Box[Int] , override val getAgentRunStartMinute: () => Box[Int] + , override val getScriptEngineEnabled: () => Box[FeatureSwitch] ) extends DeploymentService with DeploymentService_findDependantRules_bruteForce with DeploymentService_buildRuleVals with @@ -424,6 +434,7 @@ trait DeploymentService_findDependantRules_bruteForce extends DeploymentService override def getAllInventories(): Box[Map[NodeId, NodeInventory]] = roInventoryRepository.getAllNodeInventories(AcceptedInventory) override def getGlobalComplianceMode(): Box[ComplianceMode] = complianceModeService.getComplianceMode override def getGlobalAgentRun(): Box[AgentRunInterval] = agentRunService.getGlobalAgentRun() + } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -559,13 +570,14 @@ trait DeploymentService_buildNodeConfigurations extends DeploymentService with L * allNodeInfos *must* contains the nodes info of every nodes */ override def buildNodeConfigurations( - ruleVals : Seq[RuleVal] - , allNodeInfos : Map[NodeId, NodeInfo] - , groupLib : FullNodeGroupCategory - , parameters : Seq[GlobalParameter] - , globalSystemVariables : Map[String, Variable] - , globalAgentRun : AgentRunInterval - , globalComplianceMode : ComplianceMode + ruleVals : Seq[RuleVal] + , allNodeInfos : Map[NodeId, NodeInfo] + , groupLib : FullNodeGroupCategory + , parameters : Seq[GlobalParameter] + , globalSystemVariables: Map[String, Variable] + , globalAgentRun : AgentRunInterval + , globalComplianceMode : ComplianceMode + , scriptEngineEnabled : FeatureSwitch ) : Box[Seq[NodeConfiguration]] = { @@ -656,57 +668,71 @@ trait DeploymentService_buildNodeConfigurations extends DeploymentService with L } //1.3: build node config, binding ${rudder.parameters} parameters - - val nodeConfigs = sequence(interpolationContexts.toSeq) { case (nodeId, context) => - + // open a scope for the JsEngine, because its init is long. + JsEngineProvider.withNewEngine(scriptEngineEnabled) { jsEngine => for { - drafts <- Box(policyDraftByNode.get(nodeId)) ?~! "Promise generation algorithme error: cannot find back the configuration information for a node" - /* - * Clearly, here, we are evaluating parameters, and we are not using that just after in the - * variable expansion, which mean that we are doing the same work again and again and again. - * Moreover, we also are evaluating again and again parameters whose context ONLY depends - * on other parameter, and not node config at all. Bad bad bad bad. - * TODO: two stages parameter evaluation - * - global - * - by node - * + use them in variable expansion (the variable expansion should have a fully evaluated InterpolationContext) - */ - parameters <- sequence(context.parameters.toSeq) { case (name, param) => - for { - p <- param(context) - } yield { - (name, p) - } - } - cf3PolicyDrafts <- sequence(drafts) { draft => - //bind variables - draft.variableMap(context).map{ expandedVariables => - - RuleWithCf3PolicyDraft( - ruleId = draft.ruleId - , directiveId = draft.directiveId - , technique = draft.technique - , variableMap = expandedVariables - , trackerVariable = draft.trackerVariable - , priority = draft.priority - , serial = draft.serial - , ruleOrder = draft.ruleOrder - , directiveOrder = draft.directiveOrder - ) - } - } + nodeConfigs <- sequence(interpolationContexts.toSeq) { case (nodeId, context) => + for { + drafts <- Box(policyDraftByNode.get(nodeId)) ?~! "Promise generation algorithme error: cannot find back the configuration information for a node" + /* + * Clearly, here, we are evaluating parameters, and we are not using that just after in the + * variable expansion, which mean that we are doing the same work again and again and again. + * Moreover, we also are evaluating again and again parameters whose context ONLY depends + * on other parameter, and not node config at all. Bad bad bad bad. + * TODO: two stages parameter evaluation + * - global + * - by node + * + use them in variable expansion (the variable expansion should have a fully evaluated InterpolationContext) + */ + parameters <- sequence(context.parameters.toSeq) { case (name, param) => + for { + p <- param(context) + } yield { + (name, p) + } + } + cf3PolicyDrafts <- sequence(drafts) { draft => + for { + //bind variables with interpolated context + expandedVariables <- draft.variableMap(context) + // And now, for each variable, eval - if needed - the result + evaluatedVars <- sequence(expandedVariables.toSeq) { case (k, v) => + //js lib is specific to the node os, bind here to not leak eval between vars + val jsLib = context.nodeInfo.osDetails.os match { + case AixOS => JsRudderLibBinding.Aix + case _ => JsRudderLibBinding.Crypt + } + jsEngine.eval(v, jsLib).map( x => (k, x) ) + } + } yield { + + RuleWithCf3PolicyDraft( + ruleId = draft.ruleId + , directiveId = draft.directiveId + , technique = draft.technique + , variableMap = evaluatedVars.toMap + , trackerVariable = draft.trackerVariable + , priority = draft.priority + , serial = draft.serial + , ruleOrder = draft.ruleOrder + , directiveOrder = draft.directiveOrder + ) + } + } + } yield { + NodeConfiguration( + nodeInfo = context.nodeInfo + , policyDrafts = cf3PolicyDrafts.toSet + , nodeContext = context.nodeContext + , parameters = parameters.map { case (k,v) => ParameterForConfiguration(k, v) }.toSet + , isRootServer = context.nodeInfo.id == context.policyServerInfo.id + ) + } + } } yield { - NodeConfiguration( - nodeInfo = context.nodeInfo - , policyDrafts = cf3PolicyDrafts.toSet - , nodeContext = context.nodeContext - , parameters = parameters.map { case (k,v) => ParameterForConfiguration(k, v) }.toSet - , isRootServer = context.nodeInfo.id == context.policyServerInfo.id - ) + nodeConfigs } } - - nodeConfigs } } diff --git a/rudder-core/src/main/scala/com/normation/rudder/services/policies/JavascriptEngine.scala b/rudder-core/src/main/scala/com/normation/rudder/services/policies/JavascriptEngine.scala new file mode 100644 index 00000000000..472536d4730 --- /dev/null +++ b/rudder-core/src/main/scala/com/normation/rudder/services/policies/JavascriptEngine.scala @@ -0,0 +1,708 @@ +/* +************************************************************************************* +* Copyright 2016 Normation SAS +************************************************************************************* +* +* This file is part of Rudder. +* +* Rudder 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. +* +* In accordance with the terms of section 7 (7. Additional Terms.) of +* the GNU General Public License version 3, the copyright holders add +* the following Additional permissions: +* Notwithstanding to the terms of section 5 (5. Conveying Modified Source +* Versions) and 6 (6. Conveying Non-Source Forms.) of the GNU General +* Public License version 3, when you create a Related Module, this +* Related Module is not considered as a part of the work and may be +* distributed under the license agreement of your choice. +* A "Related Module" means a set of sources files including their +* documentation that, without modification of the Source Code, enables +* supplementary functions or services in addition to those offered by +* the Software. +* +* Rudder 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 Rudder. If not, see . + +* +************************************************************************************* +*/ + +package com.normation.rudder.services.policies + +import java.security.Permission +import java.util.PropertyPermission +import java.util.concurrent._ +import java.util.concurrent.Callable +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException +import java.util.concurrent.atomic.AtomicInteger +import java.io.File + +import scala.language.implicitConversions + +import com.normation.cfclerk.domain.HashAlgoConstraint._ + +import com.normation.cfclerk.domain.Variable +import com.normation.rudder.domain.appconfig.FeatureSwitch +import com.normation.rudder.services.policies.HashOsType._ +import com.normation.rudder.services.policies.JsEngine._ +import com.normation.utils.Control. _ + +import ca.mrvisser.sealerate +import javax.script.Bindings +import javax.script.ScriptEngine +import javax.script.ScriptEngineManager +import javax.script.ScriptException +import net.liftweb.common.Box +import net.liftweb.common.Empty +import net.liftweb.common.Failure +import net.liftweb.common.Full +import java.io.FilePermission +import java.lang.reflect.ReflectPermission +import java.security.SecurityPermission +import org.apache.commons.codec.digest.Md5Crypt +import org.apache.commons.codec.digest.Sha2Crypt +import com.normation.cfclerk.domain.AixPasswordHashAlgo +import com.normation.cfclerk.domain.HashAlgoConstraint +import com.normation.cfclerk.domain.AbstactPassword +import java.security.NoSuchAlgorithmException + +sealed trait HashOsType + +final object HashOsType { + final case object AixHash extends HashOsType + final case object CryptHash extends HashOsType //linux, bsd,... + + def all = sealerate.values[HashOsType] +} + + + +/* + * Define implicit bytes from string methods + */ +abstract class ImplicitGetBytes { + protected implicit def getBytes(s: String): Array[Byte] = { + s.getBytes("UTF-8") + } +} + +/* + * we need to define each class of the lib as class, because + * nashorn can't access static field. + */ +class JsLibHash() extends ImplicitGetBytes { + def md5 (s: String): String = MD5.hash(s) + def sha1 (s: String): String = SHA1.hash(s) + def sha256 (s: String): String = SHA256.hash(s) + def sha512 (s: String): String = SHA512.hash(s) +} + +trait JsLibPassword extends ImplicitGetBytes { + + /// Standard Unix (crypt) specific + + def cryptMd5 (s: String): String = Md5Crypt.md5Crypt(s) + def cryptSha256 (s: String): String = Sha2Crypt.sha256Crypt(s) + def cryptSha512 (s: String): String = Sha2Crypt.sha512Crypt(s) + + def cryptMd5 (s: String, salt: String): String = Md5Crypt.md5Crypt(s, salt) + def cryptSha256 (s: String, salt: String): String = Sha2Crypt.sha256Crypt(s, "$5$" + salt) + def cryptSha512 (s: String, salt: String): String = Sha2Crypt.sha512Crypt(s, "$6$" + salt) + + /// Aix specific + + def aixMd5 (s: String): String = AixPasswordHashAlgo.smd5(s) + def aixSha256 (s: String): String = AixPasswordHashAlgo.ssha256(s) + def aixSha512 (s: String): String = AixPasswordHashAlgo.ssha512(s) + + def aixMd5 (s: String, salt: String): String = AixPasswordHashAlgo.smd5(s, Some(salt)) + def aixSha256 (s: String, salt: String): String = AixPasswordHashAlgo.ssha256(s, Some(salt)) + def aixSha512 (s: String, salt: String): String = AixPasswordHashAlgo.ssha512(s, Some(salt)) + + /// one automatically choosing correct hash also based of the kind of HashOsType + /// method accessible from JS + + def md5 (s: String): String + def sha256 (s: String): String + def sha512 (s: String): String + + def md5 (s: String, salt: String): String + def sha256 (s: String, salt: String): String + def sha512 (s: String, salt: String): String + + /// Advertised methods + def unix(algo: String, s: String): String = { + algo.toLowerCase match { + case "md5" => cryptMd5(s) + case sha:String if (sha=="sha256" || sha=="sha-256") => cryptSha256(s) + case sha:String if (sha=="sha512" || sha=="sha-512") => cryptSha512(s) + case _ => // unknown value, fail + throw new NoSuchAlgorithmException(s"Evaluating script 'unix(${algo}, ${s})' failed, as algorithm ${algo} is not recognized") + } + } + + def unix(algo: String, s: String, salt: String): String = { + algo.toLowerCase match { + case "md5" => cryptMd5(s) + case sha:String if (sha=="sha256" || sha=="sha-256") => aixSha256(s, salt) + case sha:String if (sha=="sha512" || sha=="sha-512") => cryptSha512(s, salt) + case _ => // unknown value, fail + throw new NoSuchAlgorithmException(s"Evaluating script 'unix(${algo}, ${s}, ${salt})' failed, as algorithm ${algo} is not recognized") + } + } + + def aix(algo: String, s: String): String = { + algo.toLowerCase match { + case "md5" => aixMd5(s) + case sha:String if (sha=="sha256" || sha=="sha-256") => cryptSha256(s) + case sha:String if (sha=="sha512" || sha=="sha-512") => aixSha512(s) + case _ => // unknown value, fail + throw new NoSuchAlgorithmException(s"Evaluating script 'aix(${algo}, ${s})' failed, as algorithm ${algo} is not recognized") + } + } + + def aix(algo: String, s: String, salt: String): String = { + algo.toLowerCase match { + case "md5" => aixMd5(s) + case sha:String if (sha=="sha256" || sha=="sha-256") => cryptSha256(s, salt) + case sha:String if (sha=="sha512" || sha=="sha-512") => aixSha512(s, salt) + case _ => // unknown value, fail + throw new NoSuchAlgorithmException(s"Evaluating script 'unix(${algo}, ${s}, ${salt})' failed, as algorithm ${algo} is not recognized") + } + } + + def auto(algo: String, s: String): String = { + algo.toLowerCase match { + case "md5" => md5(s) + case sha:String if (sha=="sha256" || sha=="sha-256") => sha256(s) + case sha:String if (sha=="sha512" || sha=="sha-512") => sha512(s) + case _ => // unknown value, fail + throw new NoSuchAlgorithmException(s"Evaluating script 'auto(${algo}, ${s})' failed, as algorithm ${algo} is not recognized") + } + } + + def auto(algo: String, s: String, salt: String): String = { + algo.toLowerCase match { + case "md5" => aixMd5(s) + case sha:String if (sha=="sha256" || sha=="sha-256") => sha256(s, salt) + case sha:String if (sha=="sha512" || sha=="sha-512") => sha512(s, salt) + case _ => // unknown value, fail + throw new NoSuchAlgorithmException(s"Evaluating script 'auto(${algo}, ${s}, ${salt})' failed, as algorithm ${algo} is not recognized") + } + } +} + +/* + * This class provides the Rudder JS lib. + * All method signatures are available in JS. + * + * This lib is intended to be bound to the "rudder" namespace, + * so that one can access: + * ## Under the "rudder.hash" namespace, simple hashing methods: + * - rudder.hash.md5(value) + * - rudder.hash.sha256(value) + * - rudder.hash.sha512(value) + * + * ## Under the "rudder.password" namespace, salted hashing functions + * compatible with Unix crypt (Linux, BSD...) or AIX + * ### Automatically chosen based on the node type: + * - rudder.password.md5(value [, salt]) + * - rudder.password.sha256(value [, salt]) + * - rudder.password.sha512(value [, salt]) + * + * ### Generates Unix crypt password compatible hashes: + * - rudder.password.cryptMd5(value [, salt]) + * - rudder.password.cryptSha256(value [, salt]) + * - rudder.password.cryptSha512(value [, salt]) + * + * ### Generates AIX password compatible hashes: + * - rudder.password.aixMd5(value [, salt]) + * - rudder.password.aixSha256(value [, salt]) + * - rudder.password.aixSha512(value [, salt]) + * + * ### Public methods (advertised to the users, fallback to the previous methods) + * - rudder.password.auto(algo, password [, salt]) + * - rudder.password.unix(algo, password [, salt]) + * - rudder.password.aix(algo, password [, salt]) + * + * where algo can be MD5, SHA-512, SHA-256 (case insensitive, with or without -) + * * auto automatically choose the encryption based on the node type + * * unix generated Unix crypt password compatible hashes (Linux, BSD, ...) + * * aix generates AIX password compatible hashes + */ +final class JsRudderLibImpl( + hashKind: HashOsType +) { + + ///// simple hash algorithms ///// + private val hash = new JsLibHash() + // with the getter, it will be accessed with rudder.hash... + def getHash() = hash + + ///// unix password hash ///// + private val password = hashKind match { + case CryptHash => + new JsLibPassword() { + /// method accessible from JS + def md5 (s: String): String = super.cryptMd5(s) + def sha256 (s: String): String = super.cryptSha256(s) + def sha512 (s: String): String = super.cryptSha512(s) + + def md5 (s: String, salt: String): String = super.cryptMd5(s, salt) + def sha256 (s: String, salt: String): String = super.cryptSha256(s, salt) + def sha512 (s: String, salt: String): String = super.cryptSha512(s, salt) + + } + case AixHash => + new JsLibPassword() { + /// method accessible from JS + def md5 (s: String): String = super.aixMd5(s) + def sha256 (s: String): String = super.aixSha256(s) + def sha512 (s: String): String = super.aixSha512(s) + + def md5 (s: String, salt: String): String = super.aixMd5(s, salt) + def sha256 (s: String, salt: String): String = super.aixSha256(s, salt) + def sha512 (s: String, salt: String): String = super.aixSha512(s, salt) + + } + } + + // with the getter, it will be accessed with rudder.password... + def getPassword = password +} + +sealed trait JsRudderLibBinding { def bindings: Bindings } + +object JsRudderLibBinding { + + import java.util.{ HashMap => JHMap } + import javax.script.SimpleBindings + + private[this] def toBindings(k: String, v: JsRudderLibImpl): Bindings = { + val m = new JHMap[String, Object]() + m.put(k, v) + new SimpleBindings(m) + } + + /* + * Be carefull, as bindings are mutable, we can't have + * a val for bindings, else the same context is shared... + */ + final object Aix extends JsRudderLibBinding { + def bindings = toBindings("rudder", new JsRudderLibImpl(AixHash)) + } + + final object Crypt extends JsRudderLibBinding { + def bindings = toBindings("rudder", new JsRudderLibImpl(CryptHash)) + } +} + + +/** + * This class provides the Rhino (java 7) or Longhorn (Java 8 & up) + * java script engine. + * + * It allows to eval parameters in directives which are starting + * with $eval. + * + */ +final object JsEngineProvider { + + + /** + * Initialize a new JsEngine with the correct bindings. + * + * Not all Libs (bindings in JSR-223 name) can't be provided here, + * because we want to make them specific to each eval + * (i.e: different eval may have to different bindings). + * So we just provide eval-indep lib here, but we don't + * have any for now. + */ + def withNewEngine[T](feature: FeatureSwitch)(script: JsEngine => Box[T]): Box[T] = { + feature match { + case FeatureSwitch.Enabled => + SandboxedJsEngine.sandboxed { engine => script(engine) } + case FeatureSwitch.Disabled => + script(DisabledEngine) + } + } + +} + +sealed trait JsEngine { + /** + * + * Parse a value looking for EVAL keyword. + * Return the correct NodeContextualizedValue type. + * + * Note that the eval result is ALWAY cast to + * string. So computation, object, etc must be + * correctly defined by the user to get and + * interesting value. + */ + def eval(variable: Variable, lib: JsRudderLibBinding): Box[Variable] +} + +/* + * Our JsEngine. + */ +final object JsEngine { + // Several evals: one default and one JS (in the future, we may have several language) + final val DEFAULT_EVAL = "eval" + final val EVALJS = "evaljs" + + final val default_pattern = """(?ms)(.*)\$\{eval\s+(.*)}(.*)""".r + final val js_pattern = """(?ms)(.*)\$\{evaljs\s+(.*)}(.*)""".r + + final object DisabledEngine extends JsEngine { + /* + * Eval does nothing on variable without the EVAL keyword, and + * fails on variable with the keyword. + */ + def eval(variable: Variable, lib: JsRudderLibBinding): Box[Variable] = { + sequence(variable.values) { v => v match { + /* + * Here, we need to chose between: + * - fails when the feature is disabled, but the string starts with $eval, + * meaning that maybe the user wanted to use it anyway. + * But that means that we are changing GENERATION behavior on existing prod, + * for a feature the user don't know anything. + * - not fails, because perhaps the user had that in its parameter. But in + * that case, it will fails when feature is enabled by default. + * And we risk to let the user spread sensitive information into nodes + * (because he thought the will be hashed, but in fact no). + * + * For now, failing because it seems to be the safe bet. + */ + case default_pattern(_, _, _) => + Failure(s"Value '${v}' contains with the ${DEFAULT_EVAL} keyword, but the 'parameter evaluation feature' " + +"is disable. Please, either don't use the keyword or enable the feature") + case js_pattern(_,_,_) => + Failure(s"Value '${v}' contains the ${EVALJS} keyword, but the 'parameter evaluation feature' " + +"is disable. Please, either don't use the keyword or enable the feature") + case _ => Full(variable) + + } }.map(x => variable) // if we only have Full, return the variable as success + } + } + + final object SandboxedJsEngine { + /* + * The value is purelly arbitrary. We expects that a normal use case ends in tens of ms. + * But we don't want the user to have a whole generation fails because it scripts took 2 seconds + * for reasons. As it is something rather exceptionnal, and which will ends the + * Policy Generation, the value can be rather hight. + * + * Note: maybe make that a parameter so that we can put an even higher value here, + * but only put 1s in tests so that they end quickly + */ + val MAX_EVAL_DURATION = (5, TimeUnit.SECONDS) + + /** + * Get a new JS Engine. + * This is expensive, several seconds on a 8-core i7 @ 3.5Ghz. + * So you should minimize the number of time it is done. + */ + def sandboxed[T](script: SandboxedJsEngine => Box[T]): Box[T] = { + var sandbox = new SandboxSecurityManager() + var threadFactory = new RudderJsEngineThreadFactory(sandbox) + var pool = Executors.newSingleThreadExecutor(threadFactory) + System.setSecurityManager(sandbox) + + getJsEngine().flatMap { jsEngine => + val engine = new SandboxedJsEngine(jsEngine, sandbox, pool, MAX_EVAL_DURATION) + + try { + script(engine) + } catch { + case RudderFatalScriptException(message, cause) => + Failure(message) + } finally { + //clear everything + pool = null + threadFactory = null + sandbox = null + //check & clear interruption of the calling thread + Thread.currentThread().isInterrupted() + //restore the "none" security manager + System.setSecurityManager(null) + } + } + } + + protected[policies] def getJsEngine(): Box[ScriptEngine] = { + val message = s"Error when trying to get the java script engine. Check with your system administrator that you JVM support JSR-223 with javascript" + try { + // create a script engine manager + val factory = new ScriptEngineManager() + // create a JavaScript engine + factory.getEngineByName("JavaScript") match { + case null => Failure(message) + case engine => Full(engine) + } + } catch { + case ex: Exception => + Failure(s"${message}. Exception message was: ${ex.getMessage}") + } + } + } + + /* + * The whole idea is to give a throwable Sandox class whose only + * goal is to run a thread in a contained environment. + * Notice that using the sandox HAS a global effect, because it + * changes the security manager. + * So generally, you want to manage that in a contained scope. + */ + + final class SandboxedJsEngine private (jsEngine: ScriptEngine, sm: SecurityManager, pool: ExecutorService, maxTime: (Int, TimeUnit)) extends JsEngine { + + private[this] trait KillingThread { + /** + * Force stop the thread, throws a the ThreadDeath error. + * + * As explained in Thread#stop(), that method has consequences and + * can cause random error in all the object interracting with it + * (because monitor released without any protection). + * So caller of that method should ensure to 1/ contains + * the thread to abort and 2/ clean as best as possible object + * which interrected with it. + * + */ + def abortWithConsequences(): Unit = { + Thread.currentThread().stop() + } + } + + /** + * This is the user-accessible eval. + * It is expected to throws, and should always be used in + * a SandboxedJsEngine.sandboxed wrapping call. + * + * Nothing fancy here. + */ + def eval(variable: Variable, lib: JsRudderLibBinding): Box[Variable] = { + + // We only have one engine, so use it for default algo and js type + def scriptEvaluation(before: String, script: String, after: String) : Box[String] = { + // Evaluate the script, and concatenate back the result + singleEval(script, lib.bindings).map ( x => before ++ x ++ after) + } + for { + values <- sequence(variable.values) { value => + (value match { + case default_pattern(before, script, after) => + scriptEvaluation(before, script, after) + case js_pattern(before, script, after) => + scriptEvaluation(before, script, after) + case _ => + Full(value) + }) ?~! s"Invalid script '${value}' for Variable ${variable.spec.name} - please check method call and/or syntax" + } + } yield { + variable.copyWithSavedValues(values) + } + } + + /** + * The single eval method encapsulate the evaluation in a dedicated thread. + * We check that the evaluation doesn't take to much time, and if so, we + * kill the thread (gently, and then with a force stop). + * The thread is also restricted, and can't do dangerous things (access + * to FS, Network, System (jvm), class loader, etc). + */ + def singleEval(value: String, bindings: Bindings): Box[String] = { + val res = safeExec(value) { + try { + jsEngine.eval(value, bindings) match { + case null => Failure(s"The script '${value}' was evaluated to disallowed value 'null'") + case x => Full(x.toString) + } + } catch { + case ex: ScriptException => Failure(ex.getMessage) + } + } + res + } + + /** + * The safe eval method encapsulate the evaluation of a block of code + * in a dedicated thread. + * We check that the evaluation doesn't take to much time, and if so, we + * kill the thread (gently, and then with a force stop). + * The thread is also restricted, and can't do dangerous things (access + * to FS, Network, System (jvm), class loader, etc). + */ + def safeExec[T](name: String)(block: => Box[T]): Box[T] = { + //create the callable object + val scriptCallable = new Callable[Box[T]] with KillingThread() { + def call() = { + try { + block + } catch { + case ex: Exception => Failure(s"Error when evaluating value '${name}': ${ex.getMessage}", Full(ex), Empty) + } + } + } + + try { + // submit to the pool and synchroniously retrieve the value with a timeout + pool.submit(scriptCallable).get(maxTime._1, maxTime._2) + } catch { + case ex: ExecutionException => //this can happen when rhino get security exception... Yeah... + throw RudderFatalScriptException(s"Evaluating script '${name}' was forced interrupted due to ${ex.getMessage}, aborting.", ex) + + case ex: TimeoutException => + //try to interrupt the thread + try { + // try to gently terminate the thread + pool.shutdownNow() + Thread.sleep(200) + if(pool.isTerminated()) { + Failure(s"Evaluating script '${name}' took more than ${maxTime._1}s, aborting") + } else { + //not interrupted - force kill + scriptCallable.abortWithConsequences() //that throws TreadDead, the following is never reached + Failure(s"Evaluating script '${name}' took more than ${maxTime._1}s, and " + + "we were force to kill the thread. Check for infinite loop or uninterruptible system calls") + } + } catch { + case ex: ThreadDeath => + throw RudderFatalScriptException(s"Evaluating script '${name}' took more than ${maxTime._1}s, and " + + "we were force to kill the thread. Check for infinite loop or uninterruptible system calls", ex) + + case ex: InterruptedException => + throw RudderFatalScriptException(s"Evaluating script '${name}' was forced interrupted, aborting.", ex) + } + } + } + } + + /* + * An exception marker class to handle thread related error cases + */ + protected[policies] case class RudderFatalScriptException(message: String, cause: Throwable) extends Exception(message, cause) + + /* + * A sandboxed security manager, allowing only restricted + * set of operation for restricted thread. + * The RudderJsEngineThreadFactory take care of correctly initializing + * threads. + * + * BE CAREFULL - this sandbox won't prevent an attacker to get/do things. + * It's goal is to prevent a user to do obviously bad things on the servers, + * but more by mistake (because he didn't understood when the script is evaled, + * for example). + * + */ + protected[policies] class SandboxSecurityManager extends SecurityManager { + //by default, we don't sand box. Thread will do it in their factory + val isSandboxed = { + val itl = new InheritableThreadLocal[Boolean]() + itl.set(false) + itl + } + + //authorized / needed runtime permissions + private[this] val runtimePerms = Set( + "modifyThread" //needed by thread factory to try to kill the thread + , "createClassLoader" //needed by rhino to run almost anything, like creating a var - else NPE + , "nashorn.createGlobal" //needed by Nashorn to do stuff + , "accessDeclaredMembers" //needed by Nashorn to do stuff + , "loadLibrary.sunec" //needed by Rudder JS Lib + , "loadLibrary.j2pkcs11" //needed by Rudder JS Lib + , "accessClassInPackage.org.jcp.xml.dsig.internal.dom" + , "getProtectionDomain" + ) + private[this] val reflectPerms = Set( + "suppressAccessChecks" //needed by rhino to run almost anything, like creating a var - else NPE + ) + private[this] val securityPerms = Set( //needed by rudder js lib + //we are checking the start for them + "getProperty.security.provider" + , "putProviderProperty" + , "getProperty.securerandom.source" + , "getProperty.jdk.certpath.disabledAlgorithms" + ) + + private[this] val filePerms = Set( //we only authorize read access + "/dev/random" + , "/dev/urandom" + ) + + override def checkPermission(permission: Permission): Unit = { + if(isSandboxed.get) { + + permission match { + case x: FilePermission if( x.getActions == "read" && filePerms.contains(x.getName) ) => // ok + // We need to authorize access to a lot of jar/classes for crypto (lot of dependencies). However listing all + // classes is impossible, causing a stackoverflow error see: http://stackoverflow.com/questions/2510683/securitymanager-stackoverflowerror + // so we work-around by authorizing a lot of stuff + case x: FilePermission if( x.getActions == "read" && (x.getName.contains(".class") || x.getName.endsWith(".jar") || x.getName.endsWith(".so") || x.getName.endsWith(".cfg") || x.getName.endsWith(".properties") ) ) => // ok + case x: SecurityPermission if( securityPerms.exists( p => x.getName.startsWith(p) ) ) => // ok + case x: RuntimePermission if( x.getName.startsWith("accessClassInPackage.jdk.nashorn") ) => // ok + case x: RuntimePermission if( x.getName.startsWith("accessClassInPackage.sun.security.") ) => // ok + case x: RuntimePermission if( x.getName.startsWith("accessClassInPackage.sun.org.mozilla.javascript") ) => // ok + case x: RuntimePermission if( x.getName.startsWith("accessClassInPackage.jdk.internal.org.objectweb.asm") ) => // ok + case x: RuntimePermission if( runtimePerms.contains(x.getName) ) => // ok + case x: ReflectPermission if( reflectPerms.contains(x.getName) ) => // ok + case x: PropertyPermission if( x.getActions == "read" ) => // ok + case _ => + throw new SecurityException("access denied to: " + permission) // error + } + } else { + // don't check anything - it's the nearest to having a + // null SecurityManager, what is the default in Rudder + } + } + + override def checkPermission(permission: Permission , context: Any): Unit = { + if(isSandboxed.get) this.checkPermission(permission) + else super.checkPermission(permission, context) + } + } + + /** + * A thread factory that works with a SandboxSecurityManager and correctly + * set the sandboxed value of the thread. + */ + protected[policies] class RudderJsEngineThreadFactory(sm: SandboxSecurityManager) extends ThreadFactory { + val RUDDER_JSENGINE_THREAD = "rudder-jsengine" + class SandboxedThread(group: ThreadGroup, target: Runnable, name: String, stackSize: Long) extends Thread(group, target, name, stackSize) { + override def run() { + sm.isSandboxed.set(true) + super.run() + } + } + + val threadNumber = new AtomicInteger(1) + val group = sm.getThreadGroup() + + override def newThread(r: Runnable): Thread = { + val t = new SandboxedThread(group, r, RUDDER_JSENGINE_THREAD + "-" + threadNumber.getAndIncrement(), 0) + + + if(t.isDaemon) { + t.setDaemon(false) + } + if(t.getPriority != Thread.NORM_PRIORITY) { + t.setPriority(Thread.NORM_PRIORITY) + } + + t + } + + } + +} + diff --git a/rudder-core/src/test/scala/com/normation/rudder/services/policies/TestJsEngine.scala b/rudder-core/src/test/scala/com/normation/rudder/services/policies/TestJsEngine.scala new file mode 100644 index 00000000000..2ef7e4cb93f --- /dev/null +++ b/rudder-core/src/test/scala/com/normation/rudder/services/policies/TestJsEngine.scala @@ -0,0 +1,336 @@ +/* +************************************************************************************* +* Copyright 2016 Normation SAS +************************************************************************************* +* +* This file is part of Rudder. +* +* Rudder 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. +* +* In accordance with the terms of section 7 (7. Additional Terms.) of +* the GNU General Public License version 3, the copyright holders add +* the following Additional permissions: +* Notwithstanding to the terms of section 5 (5. Conveying Modified Source +* Versions) and 6 (6. Conveying Non-Source Forms.) of the GNU General +* Public License version 3, when you create a Related Module, this +* Related Module is not considered as a part of the work and may be +* distributed under the license agreement of your choice. +* A "Related Module" means a set of sources files including their +* documentation that, without modification of the Source Code, enables +* supplementary functions or services in addition to those offered by +* the Software. +* +* Rudder 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 Rudder. If not, see . + +* +************************************************************************************* +*/ + +package com.normation.rudder.services.policies + + +import org.junit.runner._ +import org.specs2.mutable._ +import org.specs2.runner._ + +import net.liftweb.common._ +import org.specs2.matcher.MatchResult +import javax.script.ScriptException +import com.normation.rudder.domain.appconfig.FeatureSwitch +import org.specs2.matcher.BeEqualTo +import com.normation.cfclerk.domain.InputVariableSpec +import com.normation.rudder.services.policies.JsEngine.SandboxSecurityManager +import org.specs2.matcher.Matcher +import java.util.regex.Pattern +import scala.util.matching.Regex +import com.normation.cfclerk.domain.InputVariable +import com.normation.cfclerk.domain.Variable + +/* + * This class test the JsEngine. + * It must works identically on Java 7 and Java 8. + * + */ + +@RunWith(classOf[JUnitRunner]) +class TestJsEngine extends Specification { + + val hashPrefix = "test" + val variableSpec = InputVariableSpec(hashPrefix, "") + + + val noscriptVariable = variableSpec.toVariable(Seq("simple ${rudder} value")) + val get4scriptVariable = variableSpec.toVariable(Seq("${evaljs 2+2}")) + val infiniteloopVariable = variableSpec.toVariable(Seq("${evaljs while(true){}}")) + + val setFooVariable = variableSpec.toVariable(Seq("${evaljs var foo = 'some value'; foo}")) + val getFooVariable = variableSpec.toVariable(Seq("${eval foo}")) + + + /* + * For some tests, we need to know if we are on rhino or nashorn + */ + val isNashorn = { +// val javaVersionElements = System.getProperty("java.version").split(""".""") +// val major = Integer.parseInt(javaVersionElements(1)) +// major >= 8 // else rhino, because we don't support java 1.5 and before in all case + true + } + + /** + * A failure matcher utility that pattern matches the result message + */ + def beFailure[T](regex: Regex): Matcher[Box[T]] = { b: Box[T] => + ( + b match { + case Failure(m, _, _) if( regex.pattern.matcher(m).matches() ) => true + case _ => false + } + , s"${b} is not a Failure whose message matches ${regex.toString}" + ) + } + + def beVariableValue[T](cond: String => Boolean): Matcher[Box[Variable]] = { b: Box[Variable] => + ( + b match { + case Full(v) if( v.values.size == 1 && cond(v.values(0)) ) => true + case Full(v) => println("size is " + v.values.size); false + case _ => false + } + , s"${b} is not a Full(InputVariable) that matches condition ${cond} but a '${b}'" + ) + } + + /** + * This is needed, because we set the security manager and then + * remove it. So we don't want two thread to do it concurrently. + * + * Also not that any safe use of that method mustn't be done in + * a concurrent env where several thread create (and destroy) + * sandboxed env. + */ + sequential + + "Getting the scripting engine" should { + "correctly get the engine instance" in { + (JsEngine.SandboxedJsEngine.getJsEngine() match { + case Full(engine) => + ok("ok") + case eb: EmptyBox => + val e = eb ?~! "Error with the JS Engine initialization" + ko(e.messageChain) + }):MatchResult[Any] + } + + + "let test have direct access to the engine" in { + val engine = JsEngine.SandboxedJsEngine.getJsEngine().openOrThrowException("Missing jsengine") + engine.eval("1 + 1") === 2.0 + } + } + + + "When feature is disabled, one " should { + + def context[T] = JsEngineProvider.withNewEngine[T](FeatureSwitch.Disabled) _ + + "have an identical results without eval" in { + context(engine => Full(true)) must beEqualTo(Full(true)) + } + + "have an identical result when variable isn't a script" in { + context( _.eval(noscriptVariable, JsRudderLibBinding.Crypt)) must beEqualTo(noscriptVariable) + } + + "failed with a message when the variable is a script" in { + context( _.eval(get4scriptVariable, JsRudderLibBinding.Crypt)) must beFailure(".*contains the eval.*".r) + } + } + + + "When getting the sandboxed environement, one " should { + + "still be able to do dangerous things, because it's only the JsEngine which is sandboxed" in { + JsEngine.SandboxedJsEngine.sandboxed { box => + Full((new java.io.File(s"/tmp/rudder-test-${System.currentTimeMillis}")).createNewFile()) + } must beEqualTo(Full(true)) + } + + "not be able to access FS with safeExec" in { + JsEngine.SandboxedJsEngine.sandboxed { box => + box.safeExec("write to fs")( Full((new java.io.File("/tmp/rudder-test-fromjsengine")).createNewFile()) ) + } must beFailure(".*access denied to.*/tmp/rudder-test-fromjsengine.*".r) + } + + "be able to do simple operation with JS" in { + JsEngine.SandboxedJsEngine.sandboxed { box => + box.singleEval("'thestring'.substring(0,3)", JsRudderLibBinding.Crypt.bindings) + } must beEqualTo(Full("the")) + } + + "get a scripting error if the script is eval to null" in { + JsEngine.SandboxedJsEngine.sandboxed { box => + box.singleEval("null", JsRudderLibBinding.Crypt.bindings) + } must beFailure(".*null.*".r) + } + + "not be able to access FS with JS" in { + val failMsg = if(isNashorn) { + ".*access denied to.*".r + } else { + ".*forced interrupted.*".r + } + + JsEngine.SandboxedJsEngine.sandboxed { box => + box.singleEval("""(new java.io.File("/tmp/rudder-test-fromjsengine")).createNewFile();""", JsRudderLibBinding.Crypt.bindings) + } must beFailure(failMsg) + } + + "not be able to kill the system with JS" in { + val failMsg = if(isNashorn) { + ".*access denied to.*".r + } else { + ".*forced interrupted.*".r + } + + JsEngine.SandboxedJsEngine.sandboxed { box => + box.singleEval("""java.lang.System.exit(0);""", JsRudderLibBinding.Crypt.bindings) + } must beFailure(failMsg) + } + + "not be able to loop for ever with JS" in { + JsEngine.SandboxedJsEngine.sandboxed { box => + box.singleEval("""while(true){}""", JsRudderLibBinding.Crypt.bindings) + } must beFailure(".*force to kill.*".r) + } + + } + + "When feature is enabled, one " should { + + def context[T] = JsEngineProvider.withNewEngine[T](FeatureSwitch.Enabled) _ + + "have an identical results without eval" in { + context(engine => Full(true)) must beEqualTo(Full(true)) + } + + "get the correct STRING value for simple expression" in { + context( _.eval(get4scriptVariable, JsRudderLibBinding.Crypt)) must beVariableValue(s => (s == "4" || s =="4.0")) // JS may return 4 or 4.0 + } + + "not be able to loop for ever with JS" in { + context(engine => + engine.eval(infiniteloopVariable, JsRudderLibBinding.Crypt) + ) must beFailure(".*force to kill.*".r) + } + + "not be able to access the content of a previously setted var" in { + val (res1, res2) = context { engine => + val b1 = JsRudderLibBinding.Crypt.bindings.entrySet() + val x = engine.eval(setFooVariable, JsRudderLibBinding.Crypt) + val b2 = JsRudderLibBinding.Crypt.bindings.entrySet() + val y = engine.eval(getFooVariable, JsRudderLibBinding.Crypt) + Full((x,y)) + }.openOrThrowException("test") + + (res1 must beEqualTo(Full(variableSpec.toVariable(Seq("some value"))))) and + (res2 must beFailure("Invalid script.*foo.*Variable test.*".r)) + } + + } + + "When using the Rudder JS Library, one" should { + + val sha256Variable = variableSpec.toVariable(Seq("${evaljs rudder.password.sha256('secret')}")) + val sha512Variable = variableSpec.toVariable(Seq("${eval rudder.password.sha512('secret', '01234567')}")) + + val md5VariableAIX = variableSpec.toVariable(Seq("${evaljs rudder.password.aixMd5('secret')}")) + + def context[T] = JsEngineProvider.withNewEngine[T](FeatureSwitch.Enabled) _ + + "get the correct hashed value for Linux sha256" in { + context { engine => + engine.eval(sha256Variable, JsRudderLibBinding.Crypt) + } must beVariableValue( _.startsWith("$5$") ) + } + + "get the correct hashed value for Aix sha256" in { + context { engine => + engine.eval(sha256Variable, JsRudderLibBinding.Aix) + } must beVariableValue( x => (x.startsWith("{ssha256}") | x.startsWith("{ssha1}")) ) // PBKDF2WithHmacSHA256 may not be available + } + + "get the correct hashed value for Linux sha512" in { + context { engine => + engine.eval(sha512Variable, JsRudderLibBinding.Crypt) + } must beVariableValue( _.startsWith("$6$") ) + } + + "get the correct hashed value for Aix sha512" in { + context { engine => + engine.eval(sha512Variable, JsRudderLibBinding.Aix) + } must beVariableValue( x => (x.startsWith("{ssha512}") | x.startsWith("{ssha1}")) ) // PBKDF2WithHmacSHA512 may not be available + } + + "get the correct hashed value for Aix md5" in { + context { engine => + engine.eval(md5VariableAIX, JsRudderLibBinding.Aix) + } must beVariableValue( _.startsWith("{smd5}") ) + } + } + + "When using the Rudder JS Library and advertised method, one" should { + + val sha256Variable = variableSpec.toVariable(Seq("${evaljs rudder.password.auto('sha256', 'secret')}")) + val sha512Variable = variableSpec.toVariable(Seq("${eval rudder.password.auto('SHA-512', 'secret', '01234567')}")) + val md5VariableAIX = variableSpec.toVariable(Seq("${evaljs rudder.password.aix('MD5', 'secret')}")) + val invalidAlgo = variableSpec.toVariable(Seq("${evaljs rudder.password.auto('foo', 'secret')}")) + + def context[T] = JsEngineProvider.withNewEngine[T](FeatureSwitch.Enabled) _ + + "get the correct hashed value for Linux sha256" in { + context { engine => + engine.eval(sha256Variable, JsRudderLibBinding.Crypt) + } must beVariableValue( _.startsWith("$5$") ) + } + + "get the correct hashed value for Aix sha256" in { + context { engine => + engine.eval(sha256Variable, JsRudderLibBinding.Aix) + } must beVariableValue( x => (x.startsWith("{ssha256}") | x.startsWith("{ssha1}")) ) // PBKDF2WithHmacSHA256 may not be available + } + + "get the correct hashed value for Linux sha512" in { + context { engine => + engine.eval(sha512Variable, JsRudderLibBinding.Crypt) + } must beVariableValue( _.startsWith("$6$") ) + } + + "get the correct hashed value for Aix sha512" in { + context { engine => + engine.eval(sha512Variable, JsRudderLibBinding.Aix) + } must beVariableValue( x => (x.startsWith("{ssha512}") | x.startsWith("{ssha1}")) ) // PBKDF2WithHmacSHA512 may not be available + } + + "get the correct hashed value for Aix md5" in { + context { engine => + engine.eval(md5VariableAIX, JsRudderLibBinding.Aix) + } must beVariableValue( _.startsWith("{smd5}") ) + } + + "fail when we ask for a wrong password" in { + context { engine => + engine.eval(invalidAlgo, JsRudderLibBinding.Aix) + } must beFailure("Invalid script.*".r) + } + } +} \ No newline at end of file diff --git a/rudder-web/src/main/scala/bootstrap/liftweb/AppConfig.scala b/rudder-web/src/main/scala/bootstrap/liftweb/AppConfig.scala index 7a9da04b6f6..eda8678c725 100644 --- a/rudder-web/src/main/scala/bootstrap/liftweb/AppConfig.scala +++ b/rudder-web/src/main/scala/bootstrap/liftweb/AppConfig.scala @@ -1344,6 +1344,7 @@ object RudderConfig extends Loggable { , configService.agent_run_splaytime , configService.agent_run_start_hour , configService.agent_run_start_minute + , configService.rudder_featureSwitch_directiveScriptEngine )} val agent = new AsyncDeploymentAgent( deploymentService diff --git a/rudder-web/src/main/scala/com/normation/rudder/appconfig/ConfigService.scala b/rudder-web/src/main/scala/com/normation/rudder/appconfig/ConfigService.scala index f45dab5af2b..9320b6adcdd 100644 --- a/rudder-web/src/main/scala/com/normation/rudder/appconfig/ConfigService.scala +++ b/rudder-web/src/main/scala/com/normation/rudder/appconfig/ConfigService.scala @@ -63,6 +63,10 @@ import com.normation.rudder.domain.eventlog.ModifyAgentRunSplaytimeEventType import com.normation.rudder.reports._ import com.normation.rudder.domain.eventlog.ModifyRudderSyslogProtocolEventType import scala.language.implicitConversions +import ca.mrvisser.sealerate +import com.normation.rudder.web.components.popup.ModificationValidationPopup.Disable +import com.normation.rudder.domain.appconfig.FeatureSwitch + /** * A service that Read mutable (runtime) configuration properties @@ -152,6 +156,11 @@ trait ReadConfigService { * Should we send backward compatible data from API */ def api_compatibility_mode(): Box[Boolean] + + /** + * Should we activate the script engine bar ? + */ + def rudder_featureSwitch_directiveScriptEngine(): Box[FeatureSwitch] } /** @@ -235,6 +244,10 @@ trait UpdateConfigService { */ def set_api_compatibility_mode(value: Boolean): Box[Unit] + /** + * Should we evaluate scripts in variable values? + */ + def set_rudder_featureSwitch_directiveScriptEngine(status: FeatureSwitch): Box[Unit] } class LDAPBasedConfigService(configFile: Config, repos: ConfigRepository, workflowUpdate: AsyncWorkflowInfo) extends ReadConfigService with UpdateConfigService with Loggable { @@ -266,6 +279,7 @@ class LDAPBasedConfigService(configFile: Config, repos: ConfigRepository, workfl rudder.syslog.protocol=UDP display.changes.graph=true api.compatibility.mode=false + rudder.featureSwitch.directiveScriptEngine=disabled """ val configWithFallback = configFile.withFallback(ConfigFactory.parseString(defaultConfig)) @@ -324,6 +338,17 @@ class LDAPBasedConfigService(configFile: Config, repos: ConfigRepository, workfl } } + /** + * A feature switch is defaulted to Disabled is parsing fails. + */ + private[this] implicit def toFeatureSwitch(p: RudderWebProperty): FeatureSwitch = FeatureSwitch.parse(p.value) match { + case Full(status) => status + case eb: EmptyBox => + val e = eb ?~! s"Error when trying to parse property '${p.name}' with value '${p.value}' into a feature switch status" + logger.warn(e.messageChain) + FeatureSwitch.Disabled + } + def rudder_ui_changeMessage_enabled() = get("rudder_ui_changeMessage_enabled") def rudder_ui_changeMessage_mandatory() = get("rudder_ui_changeMessage_mandatory") def rudder_ui_changeMessage_explanation() = get("rudder_ui_changeMessage_explanation") @@ -454,4 +479,15 @@ class LDAPBasedConfigService(configFile: Config, repos: ConfigRepository, workfl def api_compatibility_mode(): Box[Boolean] = get("api_compatibility_mode") def set_api_compatibility_mode(value : Boolean): Box[Unit] = save("api_compatibility_mode", value) + ///// + ///// Feature switches ///// + ///// + + + /** + * Should we evaluate scripts in the variables? + */ + def rudder_featureSwitch_directiveScriptEngine(): Box[FeatureSwitch] = get("rudder_featureSwitch_directiveScriptEngine") + def set_rudder_featureSwitch_directiveScriptEngine(status: FeatureSwitch): Box[Unit] = save("rudder_featureSwitch_directiveScriptEngine", status) + } diff --git a/rudder-web/src/main/scala/com/normation/rudder/web/snippet/administration/PropertiesManagement.scala b/rudder-web/src/main/scala/com/normation/rudder/web/snippet/administration/PropertiesManagement.scala index b5529156138..751602317ea 100644 --- a/rudder-web/src/main/scala/com/normation/rudder/web/snippet/administration/PropertiesManagement.scala +++ b/rudder-web/src/main/scala/com/normation/rudder/web/snippet/administration/PropertiesManagement.scala @@ -60,6 +60,7 @@ import com.normation.rudder.web.components.ComplianceModeEditForm import com.normation.rudder.reports.SyslogUDP import com.normation.rudder.reports.SyslogTCP import com.normation.rudder.reports.SyslogProtocol +import com.normation.rudder.web.components.popup.ModificationValidationPopup.Enable /** * This class manage the displaying of user configured properties. @@ -91,6 +92,7 @@ class PropertiesManagement extends DispatchSnippet with Loggable { case "networkProtocolSection" => networkProtocolSection case "displayGraphsConfiguration" => displayGraphsConfiguration case "apiMode" => apiComptabilityMode + case "directiveScriptEngineConfiguration" => directiveScriptEngineConfiguration } def changeMessageConfiguration = { xml : NodeSeq => @@ -787,6 +789,7 @@ class PropertiesManagement extends DispatchSnippet with Loggable { } ) apply xml } + def apiComptabilityMode = { xml : NodeSeq => // initial values, updated on successfull submit @@ -832,4 +835,50 @@ class PropertiesManagement extends DispatchSnippet with Loggable { ) apply (xml ++ Script(Run("correctButtons();") & check())) } + def directiveScriptEngineConfiguration = { xml : NodeSeq => + import com.normation.rudder.domain.appconfig.FeatureSwitch._ + + ( configService.rudder_featureSwitch_directiveScriptEngine() match { + case Full(initialValue) => + + var x = initialValue + def noModif() = x == initialValue + def check() = { + S.notice("directiveScriptEngineMsg","") + Run(s"""$$("#directiveScriptEngineSubmit").button( "option", "disabled",${noModif()});""") + } + + def submit() = { + val save = configService.set_rudder_featureSwitch_directiveScriptEngine(x) + S.notice("directiveScriptEngineMsg", save match { + case Full(_) => + "'directive script engine' property updated. The feature will be loaded as soon as you go to another page or reload this one." + case eb: EmptyBox => + "There was an error when updating the value of the 'directive script engine' property" + } ) + } + + ( "#directiveScriptEngineCheckbox" #> { + SHtml.ajaxCheckbox( + x == Enabled + , (b : Boolean) => { if(b) { x = Enabled } else { x = Disabled }; check} + , ("id","directiveScriptEngineCheckbox") + ) + } & + "#directiveScriptEngineSubmit " #> { + SHtml.ajaxSubmit("Save changes", submit _) + } & + "#directiveScriptEngineSubmit *+" #> { + Script(Run("correctButtons();") & check()) + } + ) + + case eb: EmptyBox => + ( "#directiveScriptEngine" #> { + val fail = eb ?~ "there was an error while fetching value of property: 'directive script engine'" + logger.error(fail.messageChain) +
{fail.messageChain}
+ } ) + } ) apply xml + } } diff --git a/rudder-web/src/main/webapp/secure/administration/policyServerManagement.html b/rudder-web/src/main/webapp/secure/administration/policyServerManagement.html index b8eb52e5d46..5b408d051de 100644 --- a/rudder-web/src/main/webapp/secure/administration/policyServerManagement.html +++ b/rudder-web/src/main/webapp/secure/administration/policyServerManagement.html @@ -63,31 +63,31 @@
-
- - - - - - - - - - - - - - - -
DeleteAllowed network
[error]
+ + + + + + + + + + + + + + + + +
DeleteAllowed network
[error]
- - + + -
+

@@ -165,7 +165,7 @@

Protocol

If enabled, prompt users to enter a message explaining the reason for each configuration change they make.
- These messages will be stored in each Event log and as the commit message for the underlying git repository in

+ These messages will be stored in each Event log and as the commit message for the underlying git repository in

@@ -181,7 +181,7 @@

Protocol


-
+

@@ -226,7 +226,7 @@

Protocol


-
+

@@ -292,7 +292,7 @@

Protocol


-
+
@@ -402,7 +402,33 @@

Protocol

+
+
Enable script evaluation in Directives
+
+
+
+
+ If enabled, all Directive variables can use the ${eval ...} syntax to evaluate a JavaScript expression. + These expressions are evaluated during promise generation, and can therefore provide unique values for each node. + Read the script documentationfor more information. +
+
+ +
+
+ +
+ +
+
+ + [messages] + +
+
+
+
Clear policy caches