password field changes and configuration fixes

* create a new sensitive input type to replace password
* added a new input configuration for the sensitive type called hasShowInputButton (this shows a button to the left of the field that toggles the display of plain text)
* updated the credentials form configuration
    * removed confirm password boxes for *entering* sensitve information
    * updated the Keyphrase label
* fixed a bug in the credentials controller's ask function, which did not check to see if a confirmation input was defined or not
This commit is contained in:
John Mitchell 2015-02-25 15:43:10 -05:00
parent a4b4c92a31
commit fb5db213cd
7 changed files with 255 additions and 154 deletions

View File

@ -44,7 +44,6 @@ import {AdminsList} from 'tower/controllers/Admins';
import {UsersList, UsersAdd, UsersEdit} from 'tower/controllers/Users';
import {TeamsList, TeamsAdd, TeamsEdit} from 'tower/controllers/Teams';
import {PermissionsAdd, PermissionsList, PermissionsEdit} from 'tower/controllers/Permissions';
import 'tower/shared/RestServices';
import 'tower/shared/api-loader';
import 'tower/shared/form-generator';

View File

@ -259,14 +259,23 @@ export function CredentialsAdd($scope, $rootScope, $compile, $location, $log, $r
// Respond to 'Ask at runtime?' checkbox
$scope.ask = function (fld, associated) {
if ($scope[fld + '_ask']) {
console.log("got here");
debugger;
if ($scope[fld] === 'ASK') {
$scope[fld + "_ask"] = true;
}
else if ($scope[fld + '_ask']) {
$scope[fld] = 'ASK';
$scope[associated] = '';
$scope[form.name + '_form'][associated].$setValidity('awpassmatch', true);
if (associated !== "undefined") {
$scope[associated] = '';
$scope[form.name + '_form'][associated].$setValidity('awpassmatch', true);
}
} else {
$scope[fld] = '';
$scope[associated] = '';
$scope[form.name + '_form'][associated].$setValidity('awpassmatch', true);
if (associated !== "undefined") {
$scope[associated] = '';
$scope[form.name + '_form'][associated].$setValidity('awpassmatch', true);
}
}
};
@ -306,7 +315,7 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log, $
function setAskCheckboxes() {
var fld, i;
for (fld in form.fields) {
if (form.fields[fld].type === 'password' && $scope[fld] === 'ASK') {
if (form.fields[fld].type === 'sensitive' && $scope[fld] === 'ASK') {
// turn on 'ask' checkbox for password fields with value of 'ASK'
$("#" + fld + "-clear-btn").attr("disabled", "disabled");
$scope[fld + '_ask'] = true;
@ -546,15 +555,17 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log, $
// Respond to 'Ask at runtime?' checkbox
$scope.ask = function (fld, associated) {
if ($scope[fld + '_ask']) {
$("#" + fld + "-clear-btn").attr("disabled", "disabled");
$scope[fld] = 'ASK';
$scope[associated] = '';
$scope[form.name + '_form'][associated].$setValidity('awpassmatch', true);
$scope[fld] = 'ASK'
if (associated !== "undefined") {
$scope[associated] = '';
$scope[form.name + '_form'][associated].$setValidity('awpassmatch', true);
}
} else {
$("#" + fld + "-clear-btn").removeAttr("disabled");
$scope[fld] = '';
$scope[associated] = '';
$scope[form.name + '_form'][associated].$setValidity('awpassmatch', true);
if (associated !== "undefined") {
$scope[associated] = 'ASK';
$scope[form.name + '_form'][associated].$setValidity('awpassmatch', true);
}
}
};

View File

@ -141,7 +141,7 @@ export default
},
secret_key: {
label: 'Secret Key',
type: 'password',
type: 'sensitive',
ngShow: "kind.value == 'aws'",
awRequiredWhen: {
variable: "aws_required",
@ -150,6 +150,7 @@ export default
autocomplete: false,
ask: false,
clear: false,
hasShowInputButton: true,
apiField: 'passwowrd'
},
"host": {
@ -206,7 +207,7 @@ export default
},
"api_key": {
label: 'API Key',
type: 'password',
type: 'sensitive',
ngShow: "kind.value == 'rax'",
awRequiredWhen: {
variable: "rackspace_required",
@ -214,34 +215,19 @@ export default
},
autocomplete: false,
ask: false,
hasShowInputButton: true,
clear: false,
// apiField: 'passwowrd'
},
"password": {
label: 'Password',
type: 'password',
type: 'sensitive',
ngShow: "kind.value == 'scm' || kind.value == 'vmware'",
addRequired: false,
editRequired: false,
ngChange: "clearPWConfirm('password_confirm')",
ask: false,
clear: false,
associated: 'password_confirm',
autocomplete: false,
awRequiredWhen: {
variable: "password_required",
init: false
}
},
"password_confirm": {
label: 'Confirm Password',
type: 'password',
ngShow: "kind.value == 'scm' || kind.value == 'vmware'",
addRequired: false,
editRequired: false,
awPassMatch: true,
associated: 'password',
autocomplete: false,
hasShowInputButton: true,
awRequiredWhen: {
variable: "password_required",
init: false
@ -249,24 +235,12 @@ export default
},
"ssh_password": {
label: 'Password', // formally 'SSH Password'
type: 'password',
type: 'sensitive',
ngShow: "kind.value == 'ssh'",
ngChange: "clearPWConfirm('ssh_password_confirm')",
addRequired: false,
editRequired: false,
ask: true,
clear: true,
associated: 'ssh_password_confirm',
autocomplete: false
},
"ssh_password_confirm": {
label: 'Confirm Password', // formally 'Confirm SSH password'
type: 'password',
ngShow: "kind.value == 'ssh'",
addRequired: false,
editRequired: false,
awPassMatch: true,
associated: 'ssh_password',
hasShowInputButton: true,
autocomplete: false
},
"ssh_key_data": {
@ -291,25 +265,14 @@ export default
dataContainer: "body"
},
"ssh_key_unlock": {
label: 'Key Password',
type: 'password',
label: 'Private Key Passphrase',
type: 'sensitive',
ngShow: "kind.value == 'ssh' || kind.value == 'scm'",
addRequired: false,
editRequired: false,
ngChange: "clearPWConfirm('ssh_key_unlock_confirm')",
associated: 'ssh_key_unlock_confirm',
ask: true,
hasShowInputButton: true,
askShow: "kind.value == 'ssh'", // Only allow ask for machine credentials
clear: true
},
"ssh_key_unlock_confirm": {
label: 'Confirm Key Password',
type: 'password',
ngShow: "kind.value == 'ssh' || kind.value == 'scm'",
addRequired: false,
editRequired: false,
awPassMatch: true,
associated: 'ssh_key_unlock'
},
"login_method": {
label: "Privilege Escalation Credentials",
@ -342,24 +305,12 @@ export default
},
"sudo_password": {
label: 'Sudo Password',
type: 'password',
type: 'sensitive',
ngShow: "kind.value == 'ssh' && login_method == 'sudo'",
addRequired: false,
editRequired: false,
ngChange: "clearPWConfirm('sudo_password_confirm')",
ask: true,
clear: true,
associated: 'sudo_password_confirm',
autocomplete: false
},
"sudo_password_confirm": {
label: 'Confirm Sudo Password',
type: 'password',
ngShow: "kind.value == 'ssh' && login_method == 'sudo'",
addRequired: false,
editRequired: false,
awPassMatch: true,
associated: 'sudo_password',
hasShowInputButton: true,
autocomplete: false
},
"su_username": {
@ -372,24 +323,12 @@ export default
},
"su_password": {
label: 'Su Password',
type: 'password',
type: 'sensitive',
ngShow: "kind.value == 'ssh' && login_method == 'su'",
addRequired: false,
editRequired: false,
ngChange: "clearPWConfirm('su_password_confirm')",
ask: true,
clear: true,
associated: 'su_password_confirm',
autocomplete: false
},
"su_password_confirm": {
label: 'Confirm Su Password',
type: 'password',
ngShow: "kind.value == 'ssh' && login_method == 'su'",
addRequired: false,
editRequired: false,
awPassMatch: true,
associated: 'su_password',
hasShowInputButton: true,
autocomplete: false
},
"project": {
@ -408,27 +347,14 @@ export default
editRequired: false,
autocomplete: false
},
"vault_password": {
label: "Vault Password",
type: 'password',
type: 'sensitive',
ngShow: "kind.value == 'ssh'",
addRequired: false,
editRequired: false,
ngChange: "clearPWConfirm('vault_password_confirm')",
ask: true,
clear: true,
associated: 'vault_password_confirm',
autocomplete: false
},
"vault_password_confirm": {
label: "Confirm Vault Password",
type: 'password',
ngShow: "kind.value == 'ssh'",
addRequired: false,
editRequired: false,
awPassMatch: true,
associated: 'vault_password',
hasShowInputButton: true,
autocomplete: false
}
},

View File

@ -83,7 +83,7 @@ export default
},
password: {
label: 'Password',
type: 'password',
type: 'sensitive',
ngShow: 'ldap_user == false',
addRequired: true,
editRequired: false,
@ -93,7 +93,7 @@ export default
},
password_confirm: {
label: 'Confirm Password',
type: 'password',
type: 'sensitive',
ngShow: 'ldap_user == false',
addRequired: false,
editRequired: false,

View File

@ -35,7 +35,7 @@ angular.module('CredentialsHelper', ['Utilities'])
scope.aws_required = false;
scope.email_required = false;
scope.rackspace_required = false;
scope.sshKeyDataLabel = 'SSH Private Key';
scope.sshKeyDataLabel = 'Private Key';
scope.username_required = false; // JT-- added username_required b/c mutliple 'kinds' need username to be required (GCE)
scope.key_required = false; // JT -- doing the same for key and project
scope.project_required = false;
@ -59,7 +59,7 @@ angular.module('CredentialsHelper', ['Utilities'])
scope.aws_required = false;
scope.email_required = false;
scope.rackspace_required = false;
scope.sshKeyDataLabel = 'SSH Private Key';
scope.sshKeyDataLabel = 'Private Key';
scope.username_required = false; // JT-- added username_required b/c mutliple 'kinds' need username to be required (GCE)
scope.key_required = false; // JT -- doing the same for key and project
scope.project_required = false;

View File

@ -403,46 +403,48 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'Job
* default placement to the left and delay to the config setting.
*/
.directive('awToolTip', ['$sce', function($sce) {
return function(scope, element, attrs) {
var delay = (attrs.delay !== undefined && attrs.delay !== null) ? attrs.delay : ($AnsibleConfig) ? $AnsibleConfig.tooltip_delay : {show: 500, hide: 100},
placement;
if (attrs.awTipPlacement) {
placement = attrs.awTipPlacement;
}
else {
placement = (attrs.placement !== undefined && attrs.placement !== null) ? attrs.placement : 'left';
}
return {
link: function(scope, element, attrs) {
var delay = (attrs.delay !== undefined && attrs.delay !== null) ? attrs.delay : ($AnsibleConfig) ? $AnsibleConfig.tooltip_delay : {show: 500, hide: 100},
placement;
if (attrs.awTipPlacement) {
placement = attrs.awTipPlacement;
}
else {
placement = (attrs.placement !== undefined && attrs.placement !== null) ? attrs.placement : 'left';
}
$(element).on('hidden.bs.tooltip', function( ) {
// TB3RC1 is leaving behind tooltip <div> elements. This will remove them
// after a tooltip fades away. If not, they lay overtop of other elements and
// honk up the page.
$('.tooltip').each(function() {
$(this).remove();
$(element).on('hidden.bs.tooltip', function( ) {
// TB3RC1 is leaving behind tooltip <div> elements. This will remove them
// after a tooltip fades away. If not, they lay overtop of other elements and
// honk up the page.
$('.tooltip').each(function() {
$(this).remove();
});
});
});
attrs.awToolTip = attrs.awToolTip.replace(/</g, "&lt;");
attrs.awToolTip = attrs.awToolTip.replace(/>/g, "&gt;");
attrs.awToolTip = $sce.getTrustedHtml(attrs.awToolTip);
$(element).tooltip({
placement: placement,
delay: delay,
html: true,
title: attrs.awToolTip,
container: 'body',
trigger: 'hover focus'
});
if (attrs.tipWatch) {
// Add dataTipWatch: 'variable_name'
scope.$watch(attrs.tipWatch, function(newVal, oldVal) {
if (newVal !== oldVal) {
// Where did fixTitle come from?:
// http://stackoverflow.com/questions/9501921/change-twitter-bootstrap-tooltip-content-on-click
$(element).tooltip('hide').attr('data-original-title', newVal).tooltip('fixTitle');
}
attrs.awToolTip = attrs.awToolTip.replace(/</g, "&lt;");
attrs.awToolTip = attrs.awToolTip.replace(/>/g, "&gt;");
attrs.awToolTip = $sce.getTrustedHtml(attrs.awToolTip);
$(element).tooltip({
placement: placement,
delay: delay,
html: true,
title: attrs.awToolTip,
container: 'body',
trigger: 'hover focus'
});
if (attrs.tipWatch) {
// Add dataTipWatch: 'variable_name'
scope.$watch(attrs.tipWatch, function(newVal, oldVal) {
if (newVal !== oldVal) {
// Where did fixTitle come from?:
// http://stackoverflow.com/questions/9501921/change-twitter-bootstrap-tooltip-content-on-click
$(element).tooltip('hide').attr('data-original-title', newVal).tooltip('fixTitle');
}
});
}
}
};
}])

View File

@ -94,7 +94,7 @@
* | spinner | true or false. If true, adds aw-spinner directive. Optionally add min and max attributes to control the range of allowed values. |
* | type | String containing one of the following types defined in buildField: alertblock, hidden, text, password, email, textarea, select, number, checkbox, checkbox_group, radio, radio_group, lookup, custom. |
* | trueValue | For radio buttons and checkboxes. Value to set the model to when the checkbox or radio button is selected. |
*
* | hasShowInputButton (sensitive type only) | This creates a button next to the input that toggles the input as text and password types. |
* The form object contains a buttons object for defining any buttons to be included in the generated HTML. Generally all forms will have a Reset and a Submit button. If no buttons should be generated define buttons as an empty object, or set the showButtons option to false.
*
* The icon used for the button is determined by SelectIcon() found in lib/ansible/generator-helpers.js.
@ -197,6 +197,11 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', ListGenerat
this.scope = element.scope();
}
for (fld in form.fields) {
this.scope[fld + '_field'] = form.fields[fld];
this.scope[fld + '_field'].name = fld;
}
$compile(element)(this.scope);
if (!options.html) {
@ -662,16 +667,16 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', ListGenerat
html += "\" ";
}
html += (field.labelNGClass) ? "ng-class=\"" + field.labelNGClass + "\" " : "";
html += "for=\"" + fld + '">';
html += "for=\"" + fld + '">\n';
html += (field.icon) ? Icon(field.icon) : "";
if (field.labelBind) {
html += "<span class=\"label-text\" ng-bind=\"" + field.labelBind + "\"></span>";
html += "\t\t<span class=\"label-text\" ng-bind=\"" + field.labelBind + "\">\n\t\t</span>";
} else {
html += "<span class=\"label-text\">" + field.label + "</span>";
html += "\t\t<span class=\"label-text\">\n\t\t\t" + field.label + "\n\t\t</span>";
}
html += (field.awPopOver && !field.awPopOverRight) ? Attr(field, 'awPopOver', fld) : "";
html += (field.hintText) ? " <span class=\"label-hint-text\"><i class=\"fa fa-info-circle\"></i> Hint: " + field.hintText + "</span>" : "";
html += "</label>\n";
html += (field.hintText) ? "\n\t\t<span class=\"label-hint-text\">\n\t\t\t<i class=\"fa fa-info-circle\">\n\t\t\t</i>\n\t\t\tHint: " + field.hintText + "\n\t\t</span>" : "";
html += "\n\t</label>\n";
}
return html;
}
@ -704,7 +709,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', ListGenerat
}
if ((!field.readonly) || (field.readonly && options.mode === 'edit')) {
html += "<div class=\"form-group\" ";
html += "<div class='form-group' ";
html += (field.ngShow) ? this.attr(field, 'ngShow') : "";
html += (field.ngHide) ? this.attr(field, 'ngHide') : "";
html += ">\n";
@ -832,6 +837,166 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', ListGenerat
html += "</div>\n";
}
//fields with sensitive data that needs to be obfuscated from view
if (field.type === 'sensitive') {
field.showInputInnerHTML = "ABC";
field.inputType = "password";
html += "\t" + label();
if (field.hasShowInputButton) {
field.toggleInput = function(id) {
var buttonId = id + "_show_input_button",
inputId = id + "_input",
buttonInnerHTML = $(buttonId).html();
if (buttonInnerHTML.indexOf("ABC") > -1) {
$(buttonId).html("<i class=\"fa fa-asterisk\"></i><i class=\"fa fa-asterisk\"></i><i class=\"fa fa-asterisk\"></i>");
$(inputId).attr("type", "text");
} else {
$(buttonId).html("ABC");
$(inputId).attr("type", "password");
}
}
html += "\<div class='input-group";
html += (horizontal) ? " " + getFieldWidth() : "";
html += "'>\n";
// TODO: make it so that the button won't show up if the mode is edit, hasShowInputButton !== true, and there are no contents in the field.
html += "<span class='input-group-btn'>\n";
html += "<button class='btn btn-default' ";
html += buildId(field, fld + "_show_input_button", this.form);
html += "aw-tool-tip='Toggle the display of plaintext.' aw-tip-placement='top' ";
html += "ng-click='" + fld + "_field.toggleInput(\"#" + this.form.name + "_" + fld + "\")'";
html += (field.ask) ? " ng-disabled='" + fld + "_ask'" : "";
html += ">\n" + field.showInputInnerHTML;
html += "\n</button>\n";
html += "</span>\n";
} else {
html += "<div";
html += (horizontal) ? " class='" + getFieldWidth() + "'" : "";
html += ">\n";
}
if (field.control === null || field.control === undefined || field.control) {
html += "<input ";
html += buildId(field, fld + "_input", this.form);
html += "type='password' ";
html += "ng-model=\"" + fld + '" ';
html += 'name="' + fld + '" ';
html += (field.ngChange) ? this.attr(field, 'ngChange') : "";
html += (field.chkPass) ? "chk-pass " : "";
html += buildId(field, fld, this.form);
html += (field.controlNGClass) ? "ng-class='" + field.controlNGClass + "' " : "";
html += "class='form-control";
html += (field['class']) ? " " + this.attr(field, 'class') : "";
html += "' ";
html += (field.placeholder) ? this.attr(field, 'placeholder') : "";
html += (options.mode === 'edit' && field.editRequired) ? "required " : "";
html += (options.mode === 'add' && field.addRequired) ? "required " : "";
html += (field.readonly || field.showonly) ? "readonly " : "";
html += (field.awPassMatch) ? "awpassmatch='" + field.associated + "' " : "";
html += (field.capitalize) ? "capitalize " : "";
html += (field.awSurveyQuestion) ? "aw-survey-question" : "";
html += (field.ask) ? "ng-disabled='" + fld + "_ask' " : "";
html += (field.autocomplete !== undefined) ? this.attr(field, 'autocomplete') : "";
html += (field.awRequiredWhen) ? "data-awrequired-init='" + field.awRequiredWhen.init + "' aw-required-when='" +
field.awRequiredWhen.variable + "' " : "";
html += (field.awValidUrl) ? "aw-valid-url " : "";
html += (field.associated && this.form.fields[field.associated].ask) ? "ng-disabled='" + field.associated + "_ask' " : "";
html += (field.awMultiselect) ? "aw-multiselect='" + field.awMultiselect + "' " : "";
html += ">\n";
}
html += "</div>\n";
// if (field.clear) {
// html += "<span class=\"input-group-btn\"><button type=\"button\" ";
// html += "id=\"" + this.form.name + "_" + fld + "_clear_btn\" ";
// html += "class=\"btn btn-default\" ng-click=\"clear('" + fld + "','" + field.associated + "')\" " +
// "aw-tool-tip=\"Clear " + field.label + "\" id=\"" + fld + "-clear-btn\" ";
// html += (field.ask) ? "ng-disabled=\"" + fld + "_ask\" " : "";
// html += " ><i class=\"fa fa-undo\"></i></button>\n";
// html += "</span>\n</div>\n";
//
// }
if (field.ask) {
html += "<label class=\"checkbox-inline ask-checkbox\" ";
html += (field.askShow) ? "ng-show=\"" + field.askShow + "\" " : "";
html += ">";
html += "<input type=\"checkbox\" ng-model=\"" +
fld + "_ask\" ng-change=\"ask('" + fld + "','" + field.associated + "')\" ";
html += "id=\"" + this.form.name + "_" + fld + "_ask_chbox\" ";
html += "> Ask at runtime?</label>";
}
// if (field.genMD5) {
// html += "<span class=\"input-group-btn\"><button type=\"button\" class=\"btn btn-default\" ng-click=\"genMD5('" + fld + "')\" " +
// "aw-tool-tip=\"Generate " + field.label + "\" data-placement=\"top\" id=\"" + this.form.name + "_" + fld + "_gen_btn\">" +
// "<i class=\"fa fa-magic\"></i></button></span>\n</div>\n";
// }
// Add error messages
if ((options.mode === 'add' && field.addRequired) || (options.mode === 'edit' && field.editRequired) ||
field.awRequiredWhen) {
html += "<div class='error' id='" + this.form.name + "-" + fld + "-required-error' ng-show='" + this.form.name + "_form." + fld + ".$dirty && " +
this.form.name + "_form." + fld + ".$error.required'>\nPlease enter a value.\n</div>\n";
}
if (field.type === "email") {
html += "<div class='error' id='" + this.form.name + "-" + fld + "-email-error' ng-show='" + this.form.name + "_form." + fld + ".$dirty && " +
this.form.name + "_form." + fld + ".$error.email'>\nPlease enter a valid email address.\n</div>\n";
}
if (field.awPassMatch) {
html += "<div class='error' id='" + this.form.name + "-" + fld + "-passmatch-error' ng-show='" + this.form.name + "_form." + fld +
".$error.awpassmatch'>\nThis value does not match the password you entered previously. Please confirm that password.\n</div>\n";
}
if (field.awValidUrl) {
html += "<div class='error' id='" + this.form.name + "-" + fld + "-url-error' ng-show='" + this.form.name + "_form." + fld +
".$error.awvalidurl'>\nPlease enter a URL that begins with ssh, http or https. The URL may not contain the '@' character.\n</div>\n";
}
html += "<div class='error api-error' id='" + this.form.name + "-" + fld + "-api-error' ng-bind='" + fld + "_api_error'>\n</div>\n";
if (field.chkPass) {
// complexity error
html += "<div class=\"error\" ng-show=\"" + this.form.name + '_form.' + fld +
".$error.complexity\">Please enter a stronger password (see strength bar below).</div>\n";
// progress bar
html += "<div class=\"pw-progress\">\n";
html += "<div class=\"progress progress-striped\">\n";
html += "<div id=\"progbar\" class=\"progress-bar\" role=\"progress\"></div>\n";
html += "</div>\n";
// help panel
html += HelpCollapse({
hdr: 'Password Complexity',
content: "<p>A password with reasonable strength is required. As you type the password " +
"a progress bar will measure the strength. Sufficient strength is reached when the bar turns green.</p>" +
"<p>Password strength is judged using the following:</p>" +
"<ul class=\"pwddetails\">" +
"<li>Minimum 8 characters in length</li>\n" +
"<li>Contains a sufficient combination of the following items:\n" +
"<ul>\n" +
"<li>UPPERCASE letters</li>\n" +
"<li>Numbers</li>\n" +
"<li>Symbols _!@#$%^&*()</li>\n" +
"</ul>\n" +
"</li>\n" +
"</ul>\n",
idx: this.accordion_count++,
show: null
});
html += "</div><!-- pw-progress -->\n";
}
// Add help panel(s)
html += (field.helpCollapse) ? this.buildHelpCollapse(field.helpCollapse) : '';
}
//textarea fields
if (field.type === 'textarea') {
@ -1524,8 +1689,6 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', ListGenerat
html += "</div>\n"; // accordion body
}
//console.log(html);
return html;
},