diff --git a/awx/ui/static/js/controllers/Credentials.js b/awx/ui/static/js/controllers/Credentials.js index 600633359d..dab1960d79 100644 --- a/awx/ui/static/js/controllers/Credentials.js +++ b/awx/ui/static/js/controllers/Credentials.js @@ -136,7 +136,7 @@ CredentialsList.$inject = ['$scope', '$rootScope', '$location', '$log', '$routeP export function CredentialsAdd($scope, $rootScope, $compile, $location, $log, $routeParams, CredentialForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, ReturnToCaller, ClearScope, GenerateList, SearchInit, PaginateInit, LookUpInit, UserList, TeamList, - GetBasePath, GetChoices, Empty, KindChange, OwnerChange, LoginMethodChange, FormSave) { + GetBasePath, GetChoices, Empty, KindChange, OwnerChange, FormSave) { ClearScope(); @@ -158,6 +158,13 @@ export function CredentialsAdd($scope, $rootScope, $compile, $location, $log, $r variable: 'credential_kind_options' }); + GetChoices({ + scope: $scope, + url: defaultUrl, + field: 'become_method', + variable: 'become_options' + }); + LookUpInit({ scope: $scope, form: form, @@ -209,16 +216,6 @@ export function CredentialsAdd($scope, $rootScope, $compile, $location, $log, $r OwnerChange({ scope: $scope }); } - if (!Empty($routeParams.su_username) || !Empty($routeParams.su_password)) { - $scope.login_method = 'su'; - LoginMethodChange({ scope: $scope }); - } else if (!Empty($routeParams.sudo_username) || !Empty($routeParams.sudo_password)) { - $scope.login_method = 'sudo'; - LoginMethodChange({ scope: $scope }); - } else { - $scope.login_method = ''; - LoginMethodChange({ scope: $scope }); - } // Handle Kind change $scope.kindChange = function () { @@ -239,11 +236,6 @@ export function CredentialsAdd($scope, $rootScope, $compile, $location, $log, $r OwnerChange({ scope: $scope }); }; - // Handle Login Method change - $scope.loginMethodChange = function () { - LoginMethodChange({ scope: $scope }); - }; - // Reset defaults $scope.formReset = function () { //DebugForm({ scope: $scope, form: CredentialForm }); @@ -294,13 +286,13 @@ export function CredentialsAdd($scope, $rootScope, $compile, $location, $log, $r CredentialsAdd.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'CredentialForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'ReturnToCaller', 'ClearScope', 'generateList', 'SearchInit', 'PaginateInit', - 'LookUpInit', 'UserList', 'TeamList', 'GetBasePath', 'GetChoices', 'Empty', 'KindChange', 'OwnerChange', 'LoginMethodChange', 'FormSave' + 'LookUpInit', 'UserList', 'TeamList', 'GetBasePath', 'GetChoices', 'Empty', 'KindChange', 'OwnerChange', 'FormSave' ]; export function CredentialsEdit($scope, $rootScope, $compile, $location, $log, $routeParams, CredentialForm, GenerateForm, Rest, Alert, ProcessErrors, LoadBreadCrumbs, RelatedSearchInit, RelatedPaginateInit, ReturnToCaller, ClearScope, Prompt, GetBasePath, GetChoices, - KindChange, UserList, TeamList, LookUpInit, Empty, OwnerChange, LoginMethodChange, FormSave, Stream, Wait) { + KindChange, UserList, TeamList, LookUpInit, Empty, OwnerChange, FormSave, Stream, Wait) { ClearScope(); @@ -368,7 +360,6 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log, $ reset: false }); OwnerChange({ scope: $scope }); - LoginMethodChange({ scope: $scope }); Wait('stop'); }); @@ -408,14 +399,13 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log, $ } master.owner = $scope.owner; - if (!Empty($scope.su_username) || !Empty($scope.su_password)) { - $scope.login_method = 'su'; - } else if (!Empty($scope.sudo_username) || !Empty($scope.sudo_password)) { - $scope.login_method = 'sudo'; - } else { - $scope.login_method = ''; + for (i = 0; i < $scope.become_options.length; i++) { + if ($scope.become_options[i].value === data.become_method) { + $scope.become_method = $scope.become_options[i]; + break; + } } - master.login_method = $scope.login_method; + master.become_method = $scope.become_method; for (i = 0; i < $scope.credential_kind_options.length; i++) { if ($scope.credential_kind_options[i].value === data.kind) { @@ -467,6 +457,12 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log, $ callback: 'choicesReadyCredential' }); + GetChoices({ + scope: $scope, + url: defaultUrl, + field: 'become_method', + variable: 'become_options' + }); $scope.showActivity = function () { Stream({ scope: $scope }); }; @@ -485,11 +481,6 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log, $ OwnerChange({ scope: $scope }); }; - // Handle Login Method change - $scope.loginMethodChange = function () { - LoginMethodChange({ scope: $scope }); - }; - // Handle Kind change $scope.kindChange = function () { KindChange({ scope: $scope, form: form, reset: true }); @@ -504,7 +495,6 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log, $ setAskCheckboxes(); KindChange({ scope: $scope, form: form, reset: false }); OwnerChange({ scope: $scope }); - LoginMethodChange({ scope: $scope }); }; // Related set: Add button @@ -594,5 +584,5 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log, $ CredentialsEdit.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'CredentialForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'RelatedSearchInit', 'RelatedPaginateInit', 'ReturnToCaller', 'ClearScope', 'Prompt', 'GetBasePath', 'GetChoices', 'KindChange', 'UserList', 'TeamList', 'LookUpInit', - 'Empty', 'OwnerChange', 'LoginMethodChange', 'FormSave', 'Stream', 'Wait' + 'Empty', 'OwnerChange', 'FormSave', 'Stream', 'Wait' ]; diff --git a/awx/ui/static/js/forms/Credentials.js b/awx/ui/static/js/forms/Credentials.js index c918b1ae48..0bd02f1dcf 100644 --- a/awx/ui/static/js/forms/Credentials.js +++ b/awx/ui/static/js/forms/Credentials.js @@ -274,57 +274,31 @@ export default hasShowInputButton: true, askShow: "kind.value == 'ssh'", // Only allow ask for machine credentials }, - "login_method": { - label: "Privilege Escalation Credentials", - hintText: "If your playbooks use privilege escalation (\"sudo: true\", \"su: true\", etc), you can specify the username to become, and the password to use here.", - type: 'radio_group', + "become_method": { + label: "Privilege Escalation", + // hintText: "If your playbooks use privilege escalation (\"sudo: true\", \"su: true\", etc), you can specify the username to become, and the password to use here.", + type: 'select', ngShow: "kind.value == 'ssh'", - ngChange: "loginMethodChange()", - options: [{ - label: 'None', // FIXME: Maybe 'Default' or 'SSH only' instead? - value: '', - selected: true - }, { - label: 'Sudo', - value: 'sudo' - }, { - label: 'Su', - value: 'su' - }], - awPopOver: "

Sudo: Optionally specify a username for sudo operations. This is equivalent to specifying the ansible-playbook --sudo-user parameter.
Su: Optionally specify a username for su operations. This is equivalent to specifying the ansible-playbook --su-user parameter.", + dataTitle: 'Privilege Escalation', + ngOptions: 'become.label for become in become_options track by become.value', + awPopOver: "

Specify a username for 'become' operations. " + + "This is equivalent to specifying the --become-method=BECOME_METHOD parameter, where BECOME_METHOD could be "+ + "sudo | su | pbrun | pfexec | runas
(defaults to sudo)

", dataPlacement: 'right', dataContainer: "body" }, - "sudo_username": { - label: 'Sudo Username', + "become_username": { + label: 'Privilege Escalation Username', type: 'text', - ngShow: "kind.value == 'ssh' && login_method == 'sudo'", + ngShow: "kind.value == 'ssh' && become_method", addRequired: false, editRequired: false, autocomplete: false }, - "sudo_password": { - label: 'Sudo Password', + "become_password": { + label: 'Privilege Escalation Password', type: 'sensitive', - ngShow: "kind.value == 'ssh' && login_method == 'sudo'", - addRequired: false, - editRequired: false, - ask: true, - hasShowInputButton: true, - autocomplete: false - }, - "su_username": { - label: 'Su Username', - type: 'text', - ngShow: "kind.value == 'ssh' && login_method == 'su'", - addRequired: false, - editRequired: false, - autocomplete: false - }, - "su_password": { - label: 'Su Password', - type: 'sensitive', - ngShow: "kind.value == 'ssh' && login_method == 'su'", + ngShow: "kind.value == 'ssh' && become_method", addRequired: false, editRequired: false, ask: true, diff --git a/awx/ui/static/js/forms/JobTemplates.js b/awx/ui/static/js/forms/JobTemplates.js index 856c2527bc..a87c88e5b9 100644 --- a/awx/ui/static/js/forms/JobTemplates.js +++ b/awx/ui/static/js/forms/JobTemplates.js @@ -295,6 +295,17 @@ export default // '
A survey is enabled but it does not exist. Create a survey or disable the survey.
' '
A survey is enabled but it does not exist. Create a survey or uncheck the Enable Survey box to disable the survey.
' }, + become_enabled: { + label: 'Enable Privilege Escalation', + type: 'checkbox', + addRequired: false, + editRequird: false, + column: 2, + awPopOver: "

If enabled, run this playbook as an administrator. This is the equivalent of passing the --become option to the ansible-playbook command.

", + dataPlacement: 'right', + dataTitle: 'Become Privilege Escalation', + dataContainer: "body" + }, allow_callbacks: { label: 'Allow Provisioning Callbacks', type: 'checkbox', diff --git a/awx/ui/static/js/helpers/Credentials.js b/awx/ui/static/js/helpers/Credentials.js index 36abc98ba8..1df3258801 100644 --- a/awx/ui/static/js/helpers/Credentials.js +++ b/awx/ui/static/js/helpers/Credentials.js @@ -122,12 +122,8 @@ angular.module('CredentialsHelper', ['Utilities']) scope.ssh_key_data = null; scope.ssh_key_unlock = null; scope.ssh_key_unlock_confirm = null; - scope.sudo_username = null; - scope.sudo_password = null; - scope.sudo_password_confirm = null; - scope.su_username = null; - scope.su_password = null; - scope.su_password_confirm = null; + scope.become_username = null; + scope.become_password = null; } // Collapse or open help widget based on whether scm value is selected @@ -168,25 +164,6 @@ angular.module('CredentialsHelper', ['Utilities']) } ]) - -.factory('LoginMethodChange', [ - function () { - return function (params) { - var scope = params.scope, - login_method = scope.login_method; - if (login_method !== 'sudo') { - scope.sudo_username = null; - scope.sudo_password = null; - } - if (login_method !== 'su') { - scope.su_username = null; - scope.su_password = null; - } - }; -} -]) - - .factory('FormSave', ['$location', 'Alert', 'Rest', 'ProcessErrors', 'Empty', 'GetBasePath', 'CredentialForm', 'ReturnToCaller', 'Wait', function ($location, Alert, Rest, ProcessErrors, Empty, GetBasePath, CredentialForm, ReturnToCaller, Wait) { return function (params) { @@ -215,7 +192,7 @@ angular.module('CredentialsHelper', ['Utilities']) } data.kind = scope.kind.value; - + data.become_method = (scope.become_method.value) ? scope.become_method.value : ""; switch (data.kind) { case 'ssh': data.password = scope.ssh_password; diff --git a/awx/ui/static/js/helpers/JobSubmission.js b/awx/ui/static/js/helpers/JobSubmission.js index 373814bbf8..b7db81955b 100644 --- a/awx/ui/static/js/helpers/JobSubmission.js +++ b/awx/ui/static/js/helpers/JobSubmission.js @@ -8,7 +8,81 @@ * @ngdoc function * @name helpers.function:JobSubmission * @description +* The JobSubmission.js file handles launching a job via a playbook run. There is a workflow that is involved in gathering all the +* variables needed to launch a job, including credentials, passwords, extra variables, and survey data. Depending on what information +* is needed to launch the job, a modal is built that prompts the user for any required information. This modal is built by creating +* an html string with all the fields necessary to launch the job. This html string then gets compiled and opened in a dialog modal. +* +* #Workflow when user hits launch button +* +* A 'get' call is made to the API's 'job_templates/:job_template_id/launch' endpoint for that job template. The response from the API will specify +* +*``` +* "credential_needed_to_start": true, +* "can_start_without_user_input": false, +* "ask_variables_on_launch": false, +* "passwords_needed_to_start": [], +* "variables_needed_to_start": [], +* "survey_enabled": false +*``` +* #Step 1a - Check if there is a credential included in the job template: PromptForCredential +* +* The first step is to check if a credential was specified in the job template, by looking at the value of `credential_needed_to_start` . +* If this boolean is true, then that means that the user did NOT specify a credential in the job template and we must prompt them to select a credential. +* This emits a call to `PromptForCredential` which will do a lookup on the credentials endpoint and show a modal window with the list +* of credentials for the user to choose from. +* +* #Step 1b - Check if the credential requires a password: CheckPasswords +* +* The second part of this process is to check if the credential the user picks requires a prompt for a password. A call is made (in the `CheckPasswords` factory) +* to the chosen credential +* and checks if ``password: ASK`` , ``become_password:ASK`` , or ``vault_password: ASK``. If any of these are ASK, then we begin building the html string for +* each required password (see step 2). If none of these require a password, then we contine on to prompting for vars (see step 3) +* +* #Step 2 - Build password html string: PromptForPasswords +* +* We may detect from the inital 'get' call that we may need to prompt the user for passwords. The ``passwords_needed_to_start`` array from the 'get' call +* will explictly tell us which passwords we must prompt for. Alternatively, we may have found that in steps 1a and 1b that +* we have must prompt for passwords. Either way, we arrive in `PromptForPasswords` factory which builds the html string depending on how the particular credential is setup. +* +* #Step 3 - extra vars text editor: PromptForVars +* +* We may arrive at step three if the credential selected does not require a password, or if the password html string is already done being built. +* if ``ask_variables_on_launch`` was true in the inital 'get' call, then we build the extra_vars text editor in the `PromptForVars` factory. +* This factory makes a REST call to the job template and finds if any 'extra_vars' were specified in the job template. It takes any specified +* extra vars and includes them in the extra_vars text editor that is built in the same factory. This code is added to the html string and passed along +* to the next step. +* +* #Step 4 - Survey Taker: PromptForSurvey +* +* The last step in building the job submission modal is building the survey taker. If ``survey_enabled`` is true from the initial 'get' call, +* we make a REST call to the survey endpoint for the specified job and gather the survey data. The `PromptForSurvey` factory takes the survey +* data and adds to the html string any various survey question. +* +* #Step 5 - build the modal: CreateLaunchDialog +* +* At this point, we need to compile our giant html string onto the modal and open the job submission modal. This happens in the `CreateLaunch` +* factory. In this factory the 'Launch' button for the job is tied to the validity of the form, which handles the validation of these fields. +* +* #Step 6 - Launch the job: LaunchJob +* +* This is maybe the most crucial step. We have setup everything we need in order to gather information from the user and now we want to be sure +* we handle it correctly. And there are many scenarios to take into account. The first scenario we check for is is ``survey_enabled=true`` and +* ``prompt_for_vars=false``, in which case we want to make sure to include the extra_vars from the job template in the data being +* sent to the API (it is important to note that anything specified in the extra vars on job submission will override vars specified in the job template. +* Likewise, any variables specified in the extra vars that are duplicated by the survey vars, will get overridden by the survey vars). +* If the previous scenario is NOT the case, then we continue to gather the modal's answers regularly: gather the passwords, then the extra_vars, then +* any survey results. Also note that we must gather any required survey answers, as well as any optional survey answers that happened to be provided +* by the user. We also include the credential that was chosen if the user was prompted to select a credential. +* At this point we have all the info we need and we are almost ready to perform a POST to the '/launch' endpoint. We must lastly check +* if the user was not prompted for anything and therefore we don't want to pass any extra_vars to the POST. Once this is done we +* make the REST POST call and provide all the data to hte API. The response from the API will be the job ID, which is used to redirect the user +* to the job detail page for that job run. +* +* @Usage +* This is usage information. */ + 'use strict'; export default @@ -127,11 +201,11 @@ function(Rest, Wait, ProcessErrors, ToJSON, Empty, GetBasePath) { }]) .factory('PromptForCredential', ['$location', 'Wait', 'GetBasePath', 'LookUpInit', 'JobTemplateForm', 'CredentialList', 'Rest', 'Prompt', 'ProcessErrors', -function($location, Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialList, Rest, Prompt, ProcessErrors) { + 'CheckPasswords', +function($location, Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialList, Rest, Prompt, ProcessErrors, CheckPasswords) { return function(params) { var scope = params.scope, - callback = params.callback || 'CredentialReady', selectionMade; Wait('stop'); @@ -142,7 +216,12 @@ function($location, Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialLi } scope.removeShowLookupDialog = scope.$on('ShowLookupDialog', function() { selectionMade = function () { - scope.$emit(callback, scope.credential); + // scope.$emit(callback, scope.credential); + CheckPasswords({ + scope: scope, + credential: scope.credential, + callback: 'ContinueCred' + }); }; LookUpInit({ @@ -345,72 +424,8 @@ function($compile, Rest, GetBasePath, TextareaResize,CreateDialog, GenerateForm, html += "\n"; } }); - // html += "\n"; - - // $('#password-modal').empty().html(buildHtml); - // e = angular.element(document.getElementById('password-modal')); - // $compile(e)(scope); scope.$emit(callback, html, url); - // CreateLaunchDialog({scope: scope}) - // buttons = [{ - // label: "Cancel", - // onClick: function() { - // scope.passwordCancel(); - // }, - // icon: "fa-times", - // "class": "btn btn-default", - // "id": "password-cancel-button" - // },{ - // label: "Continue", - // onClick: function() { - // scope.passwordAccept(); - // }, - // icon: "fa-check", - // "class": "btn btn-primary", - // "id": "password-accept-button" - // }]; - - - // CreateDialog({ - // id: 'password-modal', - // scope: scope, - // buttons: buttons, - // width: 600, - // height: (parent_scope.passwords.length > 1) ? 700 : 500, - // minWidth: 500, - // title: 'parent_scope.passwords Required', - // callback: 'DialogReady' - // }); - - // if (scope.removeDialogReady) { - // scope.removeDialogReady(); - // } - // scope.removeDialogReady = scope.$on('DialogReady', function() { - // $('#password-modal').dialog('open'); - // $('#password-accept-button').attr({ "disabled": "disabled" }); - // }); - // scope.keydown = function(e){ - // if(e.keyCode===13){ - // scope.passwordAccept(); - // } - // }; - - // scope.passwordAccept = function() { - // if (!scope.password_form.$invalid) { - // scope.passwords.forEach(function(password) { - // acceptedPasswords[password] = scope[password]; - // }); - // $('#password-modal').dialog('close'); - // scope.$emit(callback, acceptedPasswords); - // } - // }; - - // scope.passwordCancel = function() { - // $('#password-modal').dialog('close'); - // scope.$emit('CancelJob'); - // scope.$destroy(); - // }; // Password change scope.clearPWConfirm = function (fld) { @@ -647,9 +662,6 @@ function($compile, Rest, GetBasePath, TextareaResize,CreateDialog, GenerateForm, } } - - - Rest.setUrl(survey_url); Rest.get() .success(function (data) { @@ -671,8 +683,45 @@ function($compile, Rest, GetBasePath, TextareaResize,CreateDialog, GenerateForm, }; }]) + .factory('CheckPasswords', ['$compile', 'Rest', 'GetBasePath', 'TextareaResize', 'CreateLaunchDialog', 'GenerateForm', 'JobVarsPromptForm', 'Wait', + 'ParseVariableString', 'ToJSON', 'ProcessErrors', '$routeParams', 'Empty', + function($compile, Rest, GetBasePath, TextareaResize,CreateLaunchDialog, GenerateForm, JobVarsPromptForm, Wait, + ParseVariableString, ToJSON, ProcessErrors, $routeParams, Empty) { + return function(params) { + var scope = params.scope, + callback = params.callback, + credential = params.credential; + var passwords = []; + if (!Empty(credential)) { + Rest.setUrl(GetBasePath('credentials')+credential); + Rest.get() + .success(function (data) { + if(data.kind === "ssh"){ + if(data.password === "ASK" ){ + passwords.push("ssh_password"); + } + if(data.ssh_key_unlock === "ASK"){ + passwords.push("ssh_key_unlock"); + } + if(data.become_password === "ASK"){ + passwords.push("become_password"); + } + if(data.vault_password === "ASK"){ + passwords.push("vault_password"); + } + scope.$emit(callback, passwords); + } + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to get job template details. GET returned status: ' + status }); + }); + } + + }; + }]) /** @@ -828,53 +877,23 @@ function($compile, Rest, GetBasePath, TextareaResize,CreateDialog, GenerateForm, }); - if (scope.removeCredentialReady) { - scope.removeCredentialReady(); + if (scope.removeContinueCred) { + scope.removeContinueCred(); } - scope.removeCredentialReady = scope.$on('CredentialReady', function(e, credential) { - var passwords = []; - if (!Empty(credential)) { - Rest.setUrl(GetBasePath('credentials')+credential); - Rest.get() - .success(function (data) { - if(data.kind === "ssh"){ - if(data.password === "ASK" ){ - passwords.push("ssh_password"); - } - if(data.ssh_key_unlock === "ASK"){ - passwords.push("ssh_key_unlock"); - } - if(data.sudo_password === "ASK"){ - passwords.push("sudo_password"); - } - if(data.su_password === "ASK"){ - passwords.push("su_password"); - } - if(data.vault_password === "ASK"){ - passwords.push("vault_password"); - } - if(passwords.length>0){ - scope.passwords_needed_to_start = passwords; - scope.$emit('PromptForPasswords', passwords, html, url); - } - else if (scope.ask_variables_on_launch){ - scope.$emit('PromptForVars', html, url); - } - else if (!Empty(scope.survey_enabled) && scope.survey_enabled===true) { - scope.$emit('PromptForSurvey', html, url); - } - else { - scope.$emit('StartPlaybookRun', url); - } - } - - }) - .error(function (data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to get job template details. GET returned status: ' + status }); - }); - } - + scope.removeContinueCred = scope.$on('ContinueCred', function(e, passwords) { + if(passwords.length>0){ + scope.passwords_needed_to_start = passwords; + scope.$emit('PromptForPasswords', passwords, html, url); + } + else if (scope.ask_variables_on_launch){ + scope.$emit('PromptForVars', html, url); + } + else if (!Empty(scope.survey_enabled) && scope.survey_enabled===true) { + scope.$emit('PromptForSurvey', html, url); + } + else { + scope.$emit('StartPlaybookRun', url); + } }); // Get the job or job_template record