Skip to content

Commit

Permalink
Fixes #8785: Improve Script field behavior in passwords
Browse files Browse the repository at this point in the history
  • Loading branch information
VinceMacBuche committed Jul 29, 2016
1 parent dcedf8e commit 99b5b30
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 29 deletions.
4 changes: 2 additions & 2 deletions rudder-web/src/main/scala/bootstrap/liftweb/AppConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1697,8 +1697,8 @@ object RudderConfig extends Loggable {
case BooleanVType => new CheckboxField(id)
case TextareaVType(r) => new TextareaField(id)
// Same field type for password and MasterPassword, difference is that master will have slave/used derived passwords, and password will not have any slave/used field
case PasswordVType(algos) => new PasswordField(id, algos, input.constraint.mayBeEmpty)
case MasterPasswordVType(algos) => new PasswordField(id, algos, input.constraint.mayBeEmpty)
case PasswordVType(algos) => new PasswordField(id, algos, input.constraint.mayBeEmpty , configService.rudder_featureSwitch_directiveScriptEngine)
case MasterPasswordVType(algos) => new PasswordField(id, algos, input.constraint.mayBeEmpty, configService.rudder_featureSwitch_directiveScriptEngine)
case AixDerivedPasswordVType => new DerivedPasswordField(id, HashAlgoConstraint.DerivedPasswordType.AIX)
case LinuxDerivedPasswordVType => new DerivedPasswordField(id, HashAlgoConstraint.DerivedPasswordType.Linux)
case _ => default(id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ import net.liftweb.util.CssSel
import net.liftweb.util.Helpers
import com.normation.utils.Control
import com.normation.rudder.web.rest.RestUtils
import com.normation.rudder.domain.appconfig.FeatureSwitch
import com.normation.rudder.domain.appconfig.FeatureSwitch.Disabled
import com.normation.rudder.domain.appconfig.FeatureSwitch.Enabled
import com.normation.rudder.services.policies.JsEngine

/**
* This field is a simple input text, without any
Expand Down Expand Up @@ -554,7 +558,12 @@ class CheckboxField(val id: String) extends DirectiveField {
*
*/

class PasswordField(val id: String, algos:Seq[HashAlgoConstraint], canBeDeleted : Boolean) extends DirectiveField {
class PasswordField(
val id: String
, algos:Seq[HashAlgoConstraint]
, canBeDeleted : Boolean
, scriptSwitch : () => Box[FeatureSwitch]
) extends DirectiveField {
self =>
type ValueType = String
def getPossibleValues(filters: (ValueType => Boolean)*): Option[Set[ValueType]] = None // not supported in the general cases
Expand All @@ -580,9 +589,13 @@ class PasswordField(val id: String, algos:Seq[HashAlgoConstraint], canBeDeleted
//the algo to use
private[this] var currentAlgo: Option[HashAlgoConstraint] = algos.headOption
private[this] var currentHash: Option[String] = None
private[this] var currentAction: String = "keep"
//to store the result
private[this] var currentValue: Option[String] = None

private[this] var previousAlgo: Option[HashAlgoConstraint] = None
private[this] var previousHash: Option[String] = None

/*
* find the new internal value of the hash given:
* - past value
Expand All @@ -595,6 +608,7 @@ class PasswordField(val id: String, algos:Seq[HashAlgoConstraint], canBeDeleted
}

currentValue = Some(newInput)

if(keepCurrentPwd) pastValue
else if(blankPwd) ""
else if(newInput == "") ""
Expand Down Expand Up @@ -634,9 +648,14 @@ class PasswordField(val id: String, algos:Seq[HashAlgoConstraint], canBeDeleted
}
algo = HashAlgoConstraint.fromString(hash).getOrElse(currentAlgo.getOrElse(PLAIN))
} yield {
currentAction = action
if (!keep) {
previousAlgo = currentAlgo
previousHash = currentHash
}
newInternalValue(keep,blank,toClient, password, algo)
}) match {
case Full(newValue) => _x = newValue
case Full(newValue) => set(newValue)
case eb: EmptyBox =>
val fail = eb ?~! s"Error while parsing password input, value received is: ${Printer.compact(RestUtils.render(json))}"
logger.error(fail.messageChain)
Expand Down Expand Up @@ -693,7 +712,6 @@ class PasswordField(val id: String, algos:Seq[HashAlgoConstraint], canBeDeleted
def name = a match {
case PLAIN => "Verbatim text"
case PreHashed => "Pre hashed"
case SCRIPT => "Script"
case MD5 => "MD5 (Non salted)"
case SHA1 => "SHA1 (Non salted)"
case SHA256 => "SHA256 (Non salted)"
Expand All @@ -708,15 +726,59 @@ class PasswordField(val id: String, algos:Seq[HashAlgoConstraint], canBeDeleted
def toForm = {
val hashes = JsObj(algos.filterNot { x => x == PLAIN || x == PreHashed }.map(a => (a.prefix, Str(a.name))):_*)
val formId = Helpers.nextFuncName
val valueInput = SHtml.text("", s => parseClient(s), ("ng-model","result"), ("ng-hide", "true") )
val valueInput = SHtml.text("", {s => parseClient(s)}, ("ng-model","result"), ("ng-hide", "true") )
val otherPasswords = if (slavesValues.size == 0) "undefined" else JsObj(slavesValues().mapValues(Str(_)).toSeq:_*).toJsCmd
val (scriptEnabled,isScript, currentValue) = scriptSwitch().getOrElse(Disabled) match {
case Disabled => (false,false, currentHash)
case Enabled =>
val (isAScript,value) = {
if (currentHash.getOrElse("").startsWith(JsEngine.DEFAULT_EVAL)) {
(true,currentHash.map { _.substring(JsEngine.DEFAULT_EVAL.length()) })
} else if(currentHash.getOrElse("").startsWith(JsEngine.EVALJS)) {
(true,currentHash.map { _.substring(JsEngine.EVALJS.length()) })
} else {
(false,currentHash)
}
}
(true,isAScript,value)
}

val (previousScript,prevHash) = {
if (previousHash.getOrElse("").startsWith(JsEngine.DEFAULT_EVAL)) {
(true,previousHash.map { _.substring(JsEngine.DEFAULT_EVAL.length()) })
} else if(previousHash.getOrElse("").startsWith(JsEngine.EVALJS)) {
(true,previousHash.map { _.substring(JsEngine.EVALJS.length()) })
} else {
(false,previousHash)
}
}
val prevAlgo = previousAlgo match {
case None => currentAlgo match {
case None => algos.headOption
case current => current
}
case previous => previous
}

val initScript = {
Script(OnLoad( JsRaw(s"""
angular.bootstrap("#${formId}", ['password']);
var scope = angular.element($$("#${formId}-controller")).scope();
scope.$$apply(function(){
scope.init(${currentHash.map(Str(_).toJsCmd).getOrElse("undefined")}, ${currentAlgo.map(x =>Str(x.prefix).toJsCmd).getOrElse("undefined")}, ${hashes.toJsCmd}, ${otherPasswords}, ${canBeDeleted});
} );""")))
scope.$$apply(function(){
scope.init(
${currentValue.map(Str(_).toJsCmd).getOrElse("undefined")}
, ${currentAlgo.map(x =>Str(x.prefix).toJsCmd).getOrElse("undefined")}
, ${isScript}
, ${Str(currentAction).toJsCmd}
, ${hashes.toJsCmd}
, ${otherPasswords}
, ${canBeDeleted}
, ${scriptEnabled}
, ${prevHash.map(Str(_).toJsCmd).getOrElse("undefined")}
, ${prevAlgo.map(x =>Str(x.prefix).toJsCmd).getOrElse("undefined")}
, ${previousScript}
);
});""")))
}

val form = (".password-section *+" #> valueInput).apply(PasswordField.xml(formId)) ++ initScript
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@
*************************************************************************************
*/


var passwordModule = angular.module("password", [])
passwordModule.controller("passwordController", function($scope) {
// Declare Variables

// Current password, and 'other passwords' defined if there is a 'slave' field, displayedPass is the pass we currently display
$scope.current = {password : undefined, hash : "md5", show : false};
$scope.otherPasswords = undefined;
Expand All @@ -52,38 +53,67 @@ passwordModule.controller("passwordController", function($scope) {
$scope.action = "keep";
$scope.formType = "withHashes";
$scope.canBeDeleted = false;
$scope.scriptEnabled = false;

// Result (the value that will be sent back to Lift form), initialized as undefined, but will be update directly and on every change of action and the new password (so will be changed on init)
$scope.result = undefined;
updateResult()
updateResult();

$scope.$watch('action',updateResult);
$scope.$watch('newPassword.password',updateResult);
$scope.$watch('newPassword.hash',updateResult);

function updateResult () {
// Keep and delete, use current password as base
var result = $scope.current
var result = angular.copy($scope.current)
if ($scope.action === "change") {
result = $scope.newPassword;
result = angular.copy($scope.newPassword);
}

if (result.hash === "plain" && result.isScript) {
result.password = "evaljs:"+result.password
}

// Action will allow to differentiate between 'delete' and 'keep' and is used for 'change' too
result.action = $scope.action
$scope.result = JSON.stringify(result);
}

// init function, That will be called from 'outside' angular scope to set with values sent from the webapp
$scope.init = function(current, currentHash, hashes, otherPasswords, canBeDeleted) {
$scope.current.password=current;
$scope.init = function(current, currentHash, isScript, currentAction, hashes, otherPasswords, canBeDeleted, scriptEnabled, previousPass, previousHash, previousIsScript) {
if (currentAction === "keep") {
$scope.current.password=current;
$scope.current.hash = currentHash;
$scope.current.isScript = isScript;
}
if (currentAction === "change") {
$scope.newPassword.password=current;
$scope.newPassword.hash = currentHash;
$scope.newPassword.isScript = isScript;

if ($scope.newPassword.isScript) {
$scope.formType="script";
} else if ($scope.newPassword.hash === "pre-hashed") {
$scope.formType="preHashed";
} else if ($scope.newPassword.hash === "plain") {
$scope.formType="clearText";
} else {
$scope.formType="withHashes";
}

$scope.current.password=previousPass;
$scope.current.hash = previousHash;
$scope.current.isScript = previousIsScript;
}
$scope.hashes = hashes;
$scope.current.hash = currentHash;
$scope.displayedPass = $scope.current.password;
$scope.newPassword.hash=Object.keys(hashes)[0];
$scope.action = currentAction
if (current === undefined) {
$scope.action = "change";
}
$scope.otherPasswords = otherPasswords;
$scope.canBeDeleted = canBeDeleted;
$scope.scriptEnabled = scriptEnabled;
}

$scope.displayCurrentHash = function() {
Expand All @@ -101,24 +131,27 @@ passwordModule.controller("passwordController", function($scope) {
}

$scope.passwordForm = function(formType) {

$scope.newPassword.isScript=false;
if(formType === "withHashes") {
$scope.newPassword.hash=Object.keys($scope.hashes)[0];
} else if (formType === "clearText") {
$scope.newPassword.hash="plain";
} else if (formType === "preHashed") {
$scope.newPassword.hash="pre-hashed";
} else if (formType === "script") {
$scope.newPassword.hash="script";
$scope.newPassword.hash="plain";
$scope.newPassword.isScript=true;
}
$scope.formType=formType;
}

$scope.changeAction = function(action) {
$scope.action = action;
if (action === "change") {
if ($scope.current.hash === "script") {
if ($scope.current.isScript) {
$scope.formType="script";
$scope.newPassword.password=$scope.current.password;
$scope.newPassword=$scope.current;
} else if ($scope.current.hash === "pre-hashed") {
$scope.formType="preHashed";
} else if ($scope.current.hash === "plain") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -427,21 +427,23 @@ <h3>Protocol</h3>
<div class="lift:administration.PropertiesManagement.directiveScriptEngineConfiguration" id="directiveScriptEngine">
<div class="intro">
<div>
If enabled, all Directive variables can use the <span class="tw-bs"><code>${eval ...}</code></span> syntax to evaluate a JavaScript expression.
If enabled, all password fields can contain a JavaScript expression.
These expressions are evaluated during promise generation, and can therefore provide unique values for each node.
Read the <a href="/rudder-doc/">script documentation</a>for more information.
Read the <a href="/rudder-doc/#_javascript_evaluation_in_directives">script documentation</a>for more information.
</div>
</div>

<hr class="spacer" />
<div class="deca">
<form class="lift:form.ajax">
<div class="wbBaseField">
<label class="threeCol textright" for="directiveScriptEngineCheckbox" style="font-weight:bold;width: 30%;">Enable script evaluation in Directives with <span class="tw-bs"><code>${eval ...}</code></span>:</label><input id="directiveScriptEngineCheckbox" type="checkbox"/>
<label class="threeCol textright" for="directiveScriptEngineCheckbox" style="font-weight:bold;width: 30%;">Enable script evaluation in Directives:</label><input id="directiveScriptEngineCheckbox" type="checkbox"/>
</div>
<hr class="spacer"/>
<input type="submit" value="Save Changes" id="directiveScriptEngineSubmit"/>
<lift:Msg id="directiveScriptEngineMsg">[messages]</lift:Msg>
<lift:authz role="administration_write">
<input type="submit" value="Save Changes" id="directiveScriptEngineSubmit"/>
<lift:Msg id="directiveScriptEngineMsg">[messages]</lift:Msg>
</lift:authz>
</form>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<div class="tw-bs">
<div class="password-section" ng-controller="passwordController">
<div class="current-password padding-form" ng-if="displayCurrent()">
<div ng-if="current.hash !== 'script'" class="form-group is-passwd">
<div ng-if="! current.isScript" class="form-group is-passwd">
<label class="control-label" for="currentPassword-{{current.show ? 'clear': 'hidden'}}">Current password <small style="color: #999">- {{displayCurrentHash()}}</small></label>
<div class="input-group col-xs-12">
<input type="text" ng-if="current.show" readonly="" placeholder="" id="currentPassword-clear" class="form-control toggle-type" ng-value="displayedPass"></input>
Expand All @@ -26,7 +26,7 @@
</div>
</div>
<div>
<div class="form-group is-script" ng-if="current.hash === 'script'">
<div class="form-group is-script" ng-if="current.isScript">
<label for="currentScript" class=" control-label">Script to compute passwords</label> <span class="btn-group"> <button type="button" class="btn btn-info" ng-if="action !== 'change'" ng-click="changeAction('change')">Change</button>
<button type="button" class="btn btn-success" ng-if="action === 'change'" ng-click="changeAction('keep')">Revert</button>
<button type="button" class="btn btn-danger" ng-click="changeAction('delete')">Delete</button>
Expand All @@ -47,7 +47,7 @@ <h4 class="">Password will not be managed by Rudder anymore
<a ng-class="formType === 'withHashes' ? 'active' : '' " ng-click="passwordForm('withHashes')" class="btn btn-default">Enter password + hash</a>
<a ng-class="formType === 'preHashed' ? 'active' : '' " ng-click="passwordForm('preHashed')" class="btn btn-default">Enter pre-hashed value</a>
<a ng-class="formType === 'clearText' ? 'active' : '' " ng-click="passwordForm('clearText')" class="btn btn-default">Use clear text password</a>
<a ng-class="formType === 'script' ? 'active' : '' " ng-click="passwordForm('script')" class="btn btn-default">Enter script to set passwords</a>
<a ng-class="formType === 'script' ? 'active' : '' " ng-click="passwordForm('script')" ng-if="scriptEnabled" class="btn btn-default">Enter script to set passwords</a>
</div>

<div class=" bloc-action" ng-if="formType === 'withHashes'">
Expand Down Expand Up @@ -107,7 +107,7 @@ <h4 class="">Password will not be managed by Rudder anymore
<div class="input-group col-xs-12 ">
<textarea class="form-control" ng-model="newPassword.password" rows="4" id="script"></textarea>
<span id="passwordHashHelp" class="help-block">
You can enter a JavaScript expression here in a <code>${eval ...}</code> block. See <a href="/rudder-doc">documentation</a> here for details.<br/>
You can enter a JavaScript expression here. See <a href="/rudder-doc/#_javascript_evaluation_in_directives">documentation</a> here for details.<br/>
Passwords will be computed for each node during policy generation. To use hashed passwords, make sure to call the rudder.password function in the script above.
</span>
</div>
Expand Down

0 comments on commit 99b5b30

Please sign in to comment.