Merge pull request #976 from jakemcdermott/multivault-templates-form

multivault select for templates form
This commit is contained in:
Jake McDermott
2018-01-17 23:20:44 -05:00
committed by GitHub
15 changed files with 626 additions and 1221 deletions

View File

@@ -552,8 +552,9 @@ export default ['$compile', 'Attr', 'Icon',
if(list.fields.info) { if(list.fields.info) {
customClass = list.fields.name.modalColumnClass || ''; customClass = list.fields.name.modalColumnClass || '';
const infoHeaderClass = _.get(list.fields.info, 'infoHeaderClass', 'List-tableHeader--info');
html += `<th html += `<th
class="List-tableHeader--info" class="${infoHeaderClass}"
base-path="${list.basePath || list.name}" base-path="${list.basePath || list.name}"
collection="${list.name}" collection="${list.name}"
dataset="${list.iterator}_dataset" dataset="${list.iterator}_dataset"

View File

@@ -48,6 +48,14 @@
CallbackHelpInit({ scope: $scope }); CallbackHelpInit({ scope: $scope });
$scope.surveyTooltip = i18n._('Please save before adding a survey to this job template.'); $scope.surveyTooltip = i18n._('Please save before adding a survey to this job template.');
MultiCredentialService.getCredentialTypes()
.then(({ data }) => {
$scope.multiCredential = {
credentialTypes: data.results,
selectedCredentials: []
};
});
} }
callback = function() { callback = function() {
@@ -280,25 +288,13 @@
data.ask_credential_on_launch = $scope.ask_credential_on_launch ? $scope.ask_credential_on_launch : false; data.ask_credential_on_launch = $scope.ask_credential_on_launch ? $scope.ask_credential_on_launch : false;
data.job_tags = (Array.isArray($scope.job_tags)) ? $scope.job_tags.join() : ""; data.job_tags = (Array.isArray($scope.job_tags)) ? $scope.job_tags.join() : "";
data.skip_tags = (Array.isArray($scope.skip_tags)) ? $scope.skip_tags.join() : ""; data.skip_tags = (Array.isArray($scope.skip_tags)) ? $scope.skip_tags.join() : "";
if ($scope.selectedCredentials && $scope.selectedCredentials
.machine && $scope.selectedCredentials
.machine) {
data.credential = $scope.selectedCredentials
.machine.id;
} else {
data.credential = null;
}
if ($scope.selectedCredentials && $scope.selectedCredentials
.vault && $scope.selectedCredentials
.vault.id) {
data.vault_credential = $scope.selectedCredentials
.vault.id;
} else {
data.vault_credential = null;
}
data.extra_vars = ToJSON($scope.parseType, // drop legacy 'credential' and 'vault_credential' keys from the creation request as they will
$scope.variables, true); // be provided to the related credentials endpoint by the template save success handler.
delete data.credential;
delete data.vault_credential;
data.extra_vars = ToJSON($scope.parseType, $scope.variables, true);
// We only want to set the survey_enabled flag to // We only want to set the survey_enabled flag to
// true for this job template if a survey exists // true for this job template if a survey exists
@@ -340,7 +336,6 @@
Rest.setUrl(defaultUrl); Rest.setUrl(defaultUrl);
Rest.post(data) Rest.post(data)
.then(({data}) => { .then(({data}) => {
if (data.related && data.related.callback) { if (data.related && data.related.callback) {
Alert('Callback URL', Alert('Callback URL',
`Host callbacks are enabled for this template. The callback URL is: `Host callbacks are enabled for this template. The callback URL is:
@@ -359,12 +354,6 @@
null, true); null, true);
} }
MultiCredentialService
.saveExtraCredentials({
creds: $scope.selectedCredentials.extra,
url: data.related.extra_credentials
});
var orgDefer = $q.defer(); var orgDefer = $q.defer();
var associationDefer = $q.defer(); var associationDefer = $q.defer();
Rest.setUrl(data.related.labels); Rest.setUrl(data.related.labels);
@@ -453,7 +442,9 @@
}); });
} }
saveCompleted(data.id); MultiCredentialService
.saveRelated(data, $scope.multiCredential.selectedCredentials)
.then(() => saveCompleted(data.id));
}); });
}); });
}); });

View File

@@ -362,74 +362,37 @@ export default
$scope.can_edit = jobTemplateData.summary_fields.user_capabilities.edit; $scope.can_edit = jobTemplateData.summary_fields.user_capabilities.edit;
if($scope.job_template_obj.summary_fields.user_capabilities.edit) { const multiCredential = {};
MultiCredentialService.loadCredentials(jobTemplateData) const credentialTypesPromise = MultiCredentialService.getCredentialTypes()
.then(([selectedCredentials, credTypes, credTypeOptions, .then(({ data }) => {
credTags, credentialGetPermissionDenied]) => { multiCredential.credentialTypes = data.results;
$scope.canGetAllRelatedResources = !projectGetPermissionDenied && !inventoryGetPermissionDenied && !credentialGetPermissionDenied ? true : false; });
$scope.selectedCredentials = selectedCredentials; const multiCredentialPromises = [credentialTypesPromise];
$scope.credential_types = credTypes;
$scope.credentialTypeOptions = credTypeOptions;
$scope.credentialsToPost = credTags;
$scope.$emit('jobTemplateLoaded', master);
});
}
else {
if (jobTemplateData.summary_fields.credential) { if ($scope.can_edit) {
$scope.selectedCredentials.machine = jobTemplateData.summary_fields.credential; const selectedCredentialsPromise = MultiCredentialService
} .getRelated(jobTemplateData, { permitted: [403] })
.then(({ data, status }) => {
if (jobTemplateData.summary_fields.vault_credential) { if (status === 403) {
$scope.selectedCredentials.vault = jobTemplateData.summary_fields.vault_credential; $scope.canGetAllRelatedResources = false;
} multiCredential.selectedCredentials = _.get(jobTemplateData, 'summary_fields.credentials');
} else {
if (jobTemplateData.summary_fields.extra_credentials) { $scope.canGetAllRelatedResources = !projectGetPermissionDenied && !inventoryGetPermissionDenied;
$scope.selectedCredentials.extra = jobTemplateData.summary_fields.extra_credentials; multiCredential.selectedCredentials = data.results;
}
MultiCredentialService.getCredentialTypes()
.then(({credential_types, credentialTypeOptions}) => {
let typesArray = Object.keys(credential_types).map(key => credential_types[key]);
let credTypeOptions = credentialTypeOptions;
let machineAndVaultCreds = [],
extraCreds = [];
if($scope.selectedCredentials.machine) {
machineAndVaultCreds.push($scope.selectedCredentials.machine);
} }
if($scope.selectedCredentials.vault) {
machineAndVaultCreds.push($scope.selectedCredentials.vault);
}
machineAndVaultCreds.map(cred => ({
name: cred.name,
id: cred.id,
postType: cred.postType,
kind: typesArray
.filter(type => {
return cred.kind === type.kind || parseInt(cred.credential_type) === type.value;
})[0].name + ":"
}));
if($scope.selectedCredentials.extra && $scope.selectedCredentials.extra.length > 0) {
extraCreds = extraCreds.concat($scope.selectedCredentials.extra).map(cred => ({
name: cred.name,
id: cred.id,
postType: cred.postType,
kind: credTypeOptions
.filter(type => {
return parseInt(cred.credential_type_id) === type.value;
})[0].name + ":"
}));
}
$scope.credentialsToPost = machineAndVaultCreds.concat(extraCreds);
$scope.$emit('jobTemplateLoaded', master);
}); });
multiCredentialPromises.push(selectedCredentialsPromise);
} else {
$scope.canGetAllRelatedResources = false;
multiCredential.selectedCredentials = _.get(jobTemplateData, 'summary_fields.credentials');
} }
$q.all(multiCredentialPromises)
.then(() => {
$scope.multiCredential = multiCredential;
$scope.$emit('jobTemplateLoaded', master);
});
}); });
if ($scope.removeChoicesReady) { if ($scope.removeChoicesReady) {
@@ -522,10 +485,7 @@ export default
} }
MultiCredentialService MultiCredentialService
.findChangedExtraCredentials({ .saveRelated(jobTemplateData, $scope.multiCredential.selectedCredentials);
creds: $scope.selectedCredentials.extra,
url: data.related.extra_credentials
});
InstanceGroupsService.editInstanceGroups(instance_group_url, $scope.instance_groups) InstanceGroupsService.editInstanceGroups(instance_group_url, $scope.instance_groups)
.catch(({data, status}) => { .catch(({data, status}) => {
@@ -724,6 +684,10 @@ export default
data.job_tags = (Array.isArray($scope.job_tags)) ? _.uniq($scope.job_tags).join() : ""; data.job_tags = (Array.isArray($scope.job_tags)) ? _.uniq($scope.job_tags).join() : "";
data.skip_tags = (Array.isArray($scope.skip_tags)) ? _.uniq($scope.skip_tags).join() : ""; data.skip_tags = (Array.isArray($scope.skip_tags)) ? _.uniq($scope.skip_tags).join() : "";
// drop legacy 'credential' and 'vault_credential' keys from the update request as they will
// be provided to the related credentials endpoint by the template save success handler.
delete data.credential;
delete data.vault_credential;
Rest.setUrl(defaultUrl + $state.params.job_template_id); Rest.setUrl(defaultUrl + $state.params.job_template_id);
Rest.put(data) Rest.put(data)

View File

@@ -124,12 +124,11 @@ function(NotificationsList, CompletedJobsList, i18n) {
<multi-credential <multi-credential
credentials="credentials" credentials="credentials"
prompt="ask_credential_on_launch" prompt="ask_credential_on_launch"
selected-credentials="selectedCredentials"
credential-not-present="credentialNotPresent" credential-not-present="credentialNotPresent"
credentials-to-post="credentialsToPost" selected-credentials="multiCredential.selectedCredentials"
credential-types="multiCredential.credentialTypes"
field-is-disabled="!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)"> field-is-disabled="!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)">
</multi-credential>`, </multi-credential>`,
required: true,
awPopOver: i18n._('Select credentials that allow Tower to access the nodes this job will be ran against. You can only select one credential of each type. For machine credentials (SSH), checking "Prompt on launch" without selecting credentials will require you to select a machine credential at run time. If you select credentials and check "Prompt on launch", the selected credential(s) become the defaults that can be updated at run time.'), awPopOver: i18n._('Select credentials that allow Tower to access the nodes this job will be ran against. You can only select one credential of each type. For machine credentials (SSH), checking "Prompt on launch" without selecting credentials will require you to select a machine credential at run time. If you select credentials and check "Prompt on launch", the selected credential(s) become the defaults that can be updated at run time.'),
dataTitle: i18n._('Credentials'), dataTitle: i18n._('Credentials'),
dataPlacement: 'right', dataPlacement: 'right',
@@ -368,7 +367,7 @@ function(NotificationsList, CompletedJobsList, i18n) {
}, },
save: { save: {
ngClick: 'formSave()', //$scope.function to call on click, optional ngClick: 'formSave()', //$scope.function to call on click, optional
ngDisabled: "job_template_form.$invalid || credentialNotPresent",//true //Disable when $pristine or $invalid, optional and when can_edit = false, for permission reasons ngDisabled: "job_template_form.$invalid",//true //Disable when $pristine or $invalid, optional and when can_edit = false, for permission reasons
ngShow: '(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)' ngShow: '(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)'
} }
}, },

View File

@@ -1,311 +1,262 @@
export default ['templateUrl', 'Rest', 'GetBasePath', 'generateList', '$compile', 'CreateSelect2', 'i18n', 'MultiCredentialService', 'credentialTypesLookup', /*************************************************
function(templateUrl, Rest, GetBasePath, GenerateList, $compile, CreateSelect2, i18n, MultiCredentialService, credentialTypesLookup) { * Copyright (c) 2018 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
function MultiCredentialModal(
templateUrl,
generateList,
$compile,
CreateSelect2,
i18n,
CredentialList
) {
const templatePath = 'templates/job_templates/multi-credential/multi-credential-modal';
const emptyListText = i18n._('No Credentials Matching This Type Have Been Created');
const list = _.cloneDeep(CredentialList);
const vaultList = _.cloneDeep(CredentialList);
list.emptyListText = emptyListText;
vaultList.emptyListText = emptyListText;
vaultList.fields.name.modalColumnClass = 'col-md-6';
vaultList.fields.info = {
label: i18n._('Vault ID'),
ngBind: 'credential.inputs.vault_id',
key: false,
nosort: true,
modalColumnClass: 'col-md-6',
infoHeaderClass: '',
dataPlacement: 'top',
};
const listHtml = generateList.build({ mode: 'lookup', input_type: 'radio', list });
const vaultHtml = generateList.build({ mode: 'lookup', input_type: 'checkbox', list: vaultList });
return { return {
templateUrl: templateUrl(templatePath),
restrict: 'E', restrict: 'E',
controllerAs: 'vm',
require: ['multiCredentialModal'],
scope: { scope: {
credentialsToPost: '=', credentialTypes: '=',
credentials: '=', selectedCredentials: '=',
selectedCredentials: '='
}, },
templateUrl: templateUrl('templates/job_templates/multi-credential/multi-credential-modal'), link: (scope, element, attrs, controllers) => {
const compiledList = $compile(listHtml)(scope);
const compiledVaultList = $compile(vaultHtml)(scope);
link: function(scope, element) { const modalBodyElement = $('#multi-credential-modal-body');
credentialTypesLookup() const modalElement = $('#multi-credential-modal');
.then(kinds => {
scope.credentialKinds = kinds;
scope.credentialKind = scope.selectedCredentials.machine && scope.selectedCredentials.machine.readOnly ? (scope.selectedCredentials.vault && scope.selectedCredentials.vault.readOnly ? "" + kinds.Network : "" + kinds.Vault) : "" + kinds.Machine; scope.showModal = () => modalElement.modal('show');
scope.hideModal = () => modalElement.modal('hide');
scope.showModal = function() { scope.createList = () => modalBodyElement.append(compiledList);
$('#multi-credential-modal').modal('show'); scope.createVaultList = () => modalBodyElement.append(compiledVaultList);
}; scope.destroyList = () => modalBodyElement.empty();
scope.destroyModal = function() { modalElement.on('hidden.bs.modal', () => {
scope.credentialKind = kinds.Machine; modalElement.off('hidden.bs.modal');
$('#multi-credential-modal').modal('hide'); $(element).remove();
};
scope.generateCredentialList = function() {
let html = GenerateList.build({
list: scope.list,
input_type: 'radio',
mode: 'lookup'
});
$('#multi-credential-modal-body')
.append($compile(html)(scope));
};
$('#multi-credential-modal').on('hidden.bs.modal', function () {
$('#multi-credential-modal').off('hidden.bs.modal');
$(element).remove();
});
CreateSelect2({
element: `#multi-credential-kind-select`,
multiple: false,
placeholder: i18n._('Select a credential')
});
MultiCredentialService.getCredentialTypes()
.then(({credential_types, credentialTypeOptions}) => {
scope.credential_types = credential_types;
scope.credentialTypeOptions = credentialTypeOptions;
scope.allCredentialTypeOptions = _.cloneDeep(credentialTypeOptions);
// We want to hide machine and vault dropdown options if a credential
// has already been selected for those types and the user interacting
// with the form doesn't have the ability to change them
for(let i=scope.credentialTypeOptions.length - 1; i >=0; i--) {
if((scope.selectedCredentials.machine &&
scope.selectedCredentials.machine.credential_type_id === scope.credentialTypeOptions[i].value &&
scope.selectedCredentials.machine.readOnly) ||
(scope.selectedCredentials.vault &&
scope.selectedCredentials.vault.credential_type_id === scope.credentialTypeOptions[i].value &&
scope.selectedCredentials.vault.readOnly)) {
scope.credentialTypeOptions.splice(i, 1);
}
}
scope.$emit('multiCredentialModalLinked');
});
});
},
controller: ['$scope', 'CredentialList', 'i18n', 'QuerySet',
'GetBasePath', function($scope, CredentialList, i18n, qs,
GetBasePath) {
let updateExtraCredentialsList = function() {
let extraCredIds = $scope.selectedCredentials.extra
.map(cred => cred.id);
$scope.credentials.forEach(cred => {
if (cred.credential_type !== $scope.credentialKinds.Machine) {
cred.checked = (extraCredIds
.indexOf(cred.id) > - 1) ? 1 : 0;
}
});
$scope.credTags = MultiCredentialService
.updateCredentialTags($scope.selectedCredentials,
$scope.allCredentialTypeOptions);
};
let updateMachineCredentialList = function() {
$scope.credentials.forEach(cred => {
if (cred.credential_type === $scope.credentialKinds.Machine) {
cred.checked = ($scope.selectedCredentials
.machine !== null &&
cred.id === $scope.selectedCredentials
.machine.id) ? 1 : 0;
}
});
$scope.credTags = MultiCredentialService
.updateCredentialTags($scope.selectedCredentials,
$scope.allCredentialTypeOptions);
};
let updateVaultCredentialList = function() {
$scope.credentials.forEach(cred => {
if (cred.credential_type === $scope.credentialKinds.Vault) {
cred.checked = ($scope.selectedCredentials
.vault !== null &&
cred.id === $scope.selectedCredentials
.vault.id) ? 1 : 0;
}
});
$scope.credTags = MultiCredentialService
.updateCredentialTags($scope.selectedCredentials,
$scope.allCredentialTypeOptions);
};
let uncheckAllCredentials = function() {
$scope.credentials.forEach(cred => {
cred.checked = 0;
});
$scope.credTags = MultiCredentialService
.updateCredentialTags($scope.selectedCredentials,
$scope.allCredentialTypeOptions);
};
let init = function() {
$scope.originalSelectedCredentials = _.cloneDeep($scope
.selectedCredentials);
$scope.credential_dataset = [];
$scope.credentials = $scope.credentials || [];
$scope.listRendered = false;
let credList = _.cloneDeep(CredentialList);
credList.emptyListText = i18n._('No Credentials Matching This Type Have Been Created');
$scope.list = credList;
$scope.credential_default_params = {
order_by: 'name',
page_size: 5
};
$scope.credential_queryset = {
order_by: 'name',
page_size: 5
};
$scope.$watch('credentialKind', function(){
$scope.credential_queryset.page = 1;
$scope.credential_default_params.credential_type = $scope
.credential_queryset.credential_type = parseInt($scope
.credentialKind);
qs.search(GetBasePath('credentials'), $scope
.credential_default_params)
.then(res => {
$scope.credential_dataset = res.data;
$scope.credentials = $scope.credential_dataset
.results;
if(!$scope.listRendered) {
$scope.generateCredentialList();
$scope.listRendered = true;
$scope.showModal();
}
});
});
$scope.$watchCollection('selectedCredentials.extra', () => {
if($scope.credentials && $scope.credentials.length > 0) {
if($scope.selectedCredentials.extra &&
$scope.selectedCredentials.extra.length > 0 &&
parseInt($scope.credentialKind) !== $scope.credentialKinds.Machine) {
updateExtraCredentialsList();
} else if (parseInt($scope.credentialKind) !== $scope.credentialKinds.Machine) {
uncheckAllCredentials();
}
}
});
$scope.$watch('selectedCredentials.machine', () => {
if($scope.selectedCredentials &&
$scope.selectedCredentials.machine &&
parseInt($scope.credentialKind) === $scope.credentialKinds.Machine) {
updateMachineCredentialList();
} else {
uncheckAllCredentials();
}
});
$scope.$watch('selectedCredentials.vault', () => {
if($scope.selectedCredentials &&
$scope.selectedCredentials.vault &&
parseInt($scope.credentialKind) === $scope.credentialKinds.Vault) {
updateVaultCredentialList();
} else {
uncheckAllCredentials();
}
});
$scope.$watchGroup(['credentials',
'selectedCredentials.machine',
'selectedCredentials.vault'], () => {
if($scope.credentials &&
$scope.credentials.length > 0) {
if($scope.selectedCredentials &&
$scope.selectedCredentials.machine &&
parseInt($scope.credentialKind) === $scope.credentialKinds.Machine) {
updateMachineCredentialList();
} else if($scope.selectedCredentials &&
$scope.selectedCredentials.vault &&
parseInt($scope.credentialKind) === $scope.credentialKinds.Vault) {
updateVaultCredentialList();
} else if($scope.selectedCredentials &&
$scope.selectedCredentials.extra &&
$scope.selectedCredentials.extra.length > 0 &&
parseInt($scope.credentialKind) !== $scope.credentialKinds.Machine) {
updateExtraCredentialsList();
} else {
uncheckAllCredentials();
}
}
});
};
$scope.$on('multiCredentialModalLinked', function() {
init();
}); });
$scope.toggle_row = function(selectedRow) { CreateSelect2({
if(parseInt($scope.credentialKind) === $scope.credentialKinds.Machine) { placeholder: i18n._('Select a credential'),
if($scope.selectedCredentials && element: '#multi-credential-kind-select',
$scope.selectedCredentials.machine && multiple: false,
$scope.selectedCredentials.machine.id === selectedRow.id) { });
$scope.selectedCredentials.machine = null;
} else {
$scope.selectedCredentials.machine = _.cloneDeep(selectedRow);
}
}else if(parseInt($scope.credentialKind) === $scope.credentialKinds.Vault) {
if($scope.selectedCredentials &&
$scope.selectedCredentials.vault &&
$scope.selectedCredentials.vault.id === selectedRow.id) {
$scope.selectedCredentials.vault = null;
} else {
$scope.selectedCredentials.vault = _.cloneDeep(selectedRow);
}
} else {
let rowDeselected = false;
for (let i = $scope.selectedCredentials.extra.length - 1; i >= 0; i--) {
if($scope.selectedCredentials.extra[i].id === selectedRow
.id) {
$scope.selectedCredentials.extra.splice(i, 1);
rowDeselected = true;
} else if(selectedRow.credential_type === $scope
.selectedCredentials.extra[i].credential_type) {
$scope.selectedCredentials.extra.splice(i, 1);
}
}
if(!rowDeselected) {
$scope.selectedCredentials.extra
.push(_.cloneDeep(selectedRow));
}
}
};
$scope.selectedCredentialsDirty = function() { scope.list = list;
if ($scope.originalSelectedCredentials) { controllers[0].init(scope);
return !($scope.originalSelectedCredentials.machine === null && },
$scope.originalSelectedCredentials.vault === null && controller: ['GetBasePath', 'QuerySet', 'MultiCredentialService',
$scope.originalSelectedCredentials.extra.length === 0) && multiCredentialModalController],
!_.isEqual($scope.selectedCredentials,
$scope.originalSelectedCredentials);
} else {
return false;
}
};
$scope.revertToDefaultCredentials = function() {
$scope.selectedCredentials = _.cloneDeep($scope.originalSelectedCredentials);
};
$scope.removeCredential = function(credToRemove) {
[$scope.selectedCredentials,
$scope.credTags] = MultiCredentialService
.removeCredential(credToRemove,
$scope.selectedCredentials, $scope.credTags);
if ($scope.credentials
.filter(cred => cred.id === credToRemove).length) {
uncheckAllCredentials();
}
};
$scope.cancelForm = function() {
$scope.selectedCredentials = $scope.originalSelectedCredentials;
$scope.credTags = $scope.credentialsToPost;
$scope.destroyModal();
};
$scope.saveForm = function() {
$scope.credentialsToPost = $scope.credTags;
$scope.destroyModal();
};
}]
}; };
}]; }
function multiCredentialModalController(GetBasePath, qs, MultiCredentialService) {
const vm = this;
const { createTag, isReadOnly } = MultiCredentialService;
const types = {};
const unwatch = [];
let scope;
vm.init = _scope_ => {
scope = _scope_;
scope.modalSelectedCredentials = _.cloneDeep(scope.selectedCredentials);
scope.credentialTypes.forEach(({ name, id }) => types[name] = id);
scope.toggle_row = vm.toggle_row;
scope.toggle_credential = vm.toggle_credential;
scope.credential_default_params = { order_by: 'name', page_size: 5 };
scope.credential_queryset = { order_by: 'name', page_size: 5 };
scope.credential_dataset = { results: [], count: 0 };
scope.credentials = scope.credential_dataset.results;
scope.credentialType = getInitialCredentialType();
scope.displayedCredentialTypes = scope.credentialTypes;
const watchType = scope.$watch('credentialType', (newValue, oldValue) => {
if (newValue !== oldValue) {
fetchCredentials(parseInt(newValue));
}
});
scope.$watchCollection('modalSelectedCredentials', updateListView);
scope.$watchCollection('modalSelectedCredentials', updateTagView);
scope.$watchCollection('modalSelectedCredentials', updateDisplayedCredentialTypes);
scope.$watchCollection('credentials', updateListView);
unwatch.push(watchType);
fetchCredentials(parseInt(scope.credentialType))
.then(scope.showModal);
};
function updateTagView () {
scope.tags = scope.modalSelectedCredentials
.map(c => createTag(c, scope.credentialTypes));
}
function updateListView () {
scope.credentials.forEach(credential => {
const index = scope.modalSelectedCredentials
.map(({ id }) => id)
.indexOf(credential.id);
if (index > -1) {
credential.checked = 1;
} else {
credential.checked = 0;
}
});
}
function updateDisplayedCredentialTypes() {
const displayedCredentialTypes = _.cloneDeep(scope.credentialTypes);
scope.modalSelectedCredentials.forEach(credential => {
const credentialTypeId = credential.credential_type || credential.credential_type_id;
if(isReadOnly(credential) && credentialTypeId !== types.Vault) {
const index = displayedCredentialTypes
.map(t => t.id).indexOf(credential.credential_type);
if (index > -1) {
displayedCredentialTypes.splice(index, 1);
}
}
});
scope.displayedCredentialTypes = displayedCredentialTypes;
}
function getInitialCredentialType () {
const selectedMachineCredential = scope.modalSelectedCredentials
.find(c => c.id === types.Machine);
if (selectedMachineCredential && isReadOnly(selectedMachineCredential)) {
return `${types.Vault}`;
}
return `${types.Machine}`;
}
function fetchCredentials (credentialType) {
const endpoint = GetBasePath('credentials');
scope.credential_queryset.page = 1;
scope.credential_default_params.credential_type = credentialType;
scope.credential_queryset.credential_type = credentialType;
return qs.search(endpoint, scope.credential_default_params)
.then(({ data }) => {
const results = data.results.filter(c => !isReadOnly(c));
const readOnlyCount = data.results.length - results.length;
data.results = results;
data.count = data.count - readOnlyCount;
scope.credential_dataset = data;
scope.credentials = data.results;
scope.destroyList();
if (credentialType === types.Vault) {
scope.createVaultList();
} else {
scope.createList();
}
});
}
vm.revertSelectedCredentials = () => {
scope.modalSelectedCredentials = _.cloneDeep(scope.selectedCredentials);
};
vm.removeCredential = id => {
const index = scope.modalSelectedCredentials.map(c => c.id).indexOf(id);
const isSelected = index > -1;
if (isSelected) {
scope.modalSelectedCredentials.splice(index, 1);
return;
}
};
vm.toggle_credential = id => {
// This is called only when a checkbox input is clicked directly. Clicks anywhere else on
// the row or direct radio button clicks invoke the toggle_row handler instead with a
// different set of arguments. We normalize those arguments here and pass them through to
// the other function so that the behavior is consistent.
const credential = scope.credentials.find(c => c.id === id);
scope.toggle_row(credential);
};
vm.toggle_row = credential => {
const index = scope.modalSelectedCredentials.map(c => c.id).indexOf(credential.id);
const isSelected = index > -1;
if (isSelected) {
scope.modalSelectedCredentials.splice(index, 1);
return;
}
if (credential.credential_type === types.Vault) {
scope.modalSelectedCredentials = scope.modalSelectedCredentials
.filter(({ inputs }) => inputs.vault_id !== credential.inputs.vault_id)
.concat([credential]);
} else {
scope.modalSelectedCredentials = scope.modalSelectedCredentials
.filter(({ credential_type }) => credential_type !== credential.credential_type)
.concat([credential]);
}
};
vm.cancelForm = () => {
unwatch.forEach(cb => cb());
scope.hideModal();
};
vm.saveForm = () => {
scope.selectedCredentials = _.cloneDeep(scope.modalSelectedCredentials);
unwatch.forEach(cb => cb());
scope.hideModal();
};
}
MultiCredentialModal.$inject = [
'templateUrl',
'generateList',
'$compile',
'CreateSelect2',
'i18n',
'CredentialList',
];
export default MultiCredentialModal;

View File

@@ -2,75 +2,90 @@
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header Form-header"> <div class="modal-header Form-header">
<div class="Form-title Form-title--uppercase" <div class="Form-title Form-title--uppercase" translate>
translate> CREDENTIALS
CREDENTIALS
</div> </div>
<div class="Form-header--fields"></div> <div class="Form-header--fields"></div>
<div class="Form-exitHolder"> <div class="Form-exitHolder">
<button type="button" class="Form-exit" <button type="button" class="Form-exit" ng-click="cancelForm()">
ng-click="cancelForm()"> <i class="fa fa-times-circle"></i>
<i class="fa fa-times-circle"></i>
</button> </button>
</div> </div>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="MultiCredential-selectedBar" <div class="MultiCredential-selectedBar"
ng-show="(credTags && credTags.length) || selectedCredentialsDirty()"> ng-show="(tags && tags.length) || selectedCredentialsDirty()">
<span class="MultiCredential-selectedBarLabel" translate> <span class="MultiCredential-selectedBarLabel" translate>
SELECTED: SELECTED:
</span> </span>
<div class="MultiCredential-tags"> <div class="MultiCredential-tags">
<div class="MultiCredential-tagSection"> <div class="MultiCredential-tagSection">
<div class="MultiCredential-flexContainer"> <div class="MultiCredential-flexContainer">
<div <div class="MultiCredential-tagContainer ng-scope" ng-repeat="tag in tags track by $index">
class="MultiCredential-tagContainer ng-scope" <div class="MultiCredential-deleteContainer"
ng-repeat="tag in credTags track by $index"> ng-click="vm.deselectCredential(tag)"
<div class="MultiCredential-deleteContainer" ng-hide="fieldIsDisabled || tag.readOnly">
ng-click="removeCredential(tag.id)" <i class="fa fa-times MultiCredential-tagDelete"></i>
ng-if="!tag.readOnly"> </div>
<i class="fa fa-times <div class="MultiCredential-iconContainer--disabled" ng-switch="tag.kind" ng-if="fieldIsDisabled || tag.readOnly">
MultiCredential-tagDelete"> <i class="fa fa-cloud MultiCredential-tagIcon" ng-switch-when="cloud"></i>
</i> <i class="fa fa-info MultiCredential-tagIcon" ng-switch-when="insights"></i>
</div> <i class="fa fa-sitemap MultiCredential-tagIcon" ng-switch-when="net"></i>
<div class="MultiCredential-tag" ng-class="tag.readOnly ? 'MultiCredential-tag--disabled' : 'MultiCredential-tag--deletable'"> <i class="fa fa-code-fork MultiCredential-tagIcon" ng-switch-when="scm"></i>
<span <i class="fa fa-key MultiCredential-tagIcon" ng-switch-when="ssh"></i>
class="MultiCredential-name--label <i class="fa fa-archive MultiCredential-tagIcon" ng-switch-when="vault"></i>
ng-binding"> </div>
{{ tag.kind }} <div class="MultiCredential-iconContainer" ng-switch="tag.kind" ng-if="!(fieldIsDisabled || tag.readOnly)">
</span> <i class="fa fa-cloud MultiCredential-tagIcon" ng-switch-when="cloud"></i>
<span class="MultiCredential-name u-wordwrap <i class="fa fa-info MultiCredential-tagIcon" ng-switch-when="insights"></i>
ng-binding"> <i class="fa fa-sitemap MultiCredential-tagIcon" ng-switch-when="net"></i>
{{ tag.name }} <i class="fa fa-code-fork MultiCredential-tagIcon" ng-switch-when="scm"></i>
</span> <i class="fa fa-key MultiCredential-tagIcon" ng-switch-when="ssh"></i>
</div> <i class="fa fa-archive MultiCredential-tagIcon" ng-switch-when="vault"></i>
</div>
<div class="MultiCredential-tag"
ng-class="{'MultiCredential-tag--deletable': !fieldIsDisabled && !tag.readOnly, 'MultiCredential-tag--disabled': tag.readOnly}">
<span ng-if="!tag.info" class="MultiCredential-name--label ng-binding">
{{ tag.name }}
</span>
<span ng-if="tag.info" class="MultiCredential-name--label ng-binding">
{{ tag.name }} | {{ tag.info }}
</span>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="MultiCredential-previewTagRevert"> <div class="MultiCredential-previewTagRevert">
<a class="MultiCredential-revertLink" href="" ng-show="selectedCredentialsDirty()" ng-click="revertToDefaultCredentials()" translate>REVERT</a> <a class="MultiCredential-revertLink"
ng-show="selectedCredentialsDirty()"
ng-click="vm.revertSelectedCredentials()"
href=""
translate>
REVERT
</a>
</div> </div>
</div> </div>
<div class="MultiCredential-credentialSubSection"> <div class="MultiCredential-credentialSubSection">
<span class="MultiCredential-selectLabel" translate>CREDENTIAL TYPE:</span> <span class="MultiCredential-selectLabel" translate>CREDENTIAL TYPE:</span>
<select id="multi-credential-kind-select" ng-model="credentialKind"> <select id="multi-credential-kind-select" ng-model="credentialType">
<option ng-repeat="option in credentialTypeOptions" <option ng-repeat="t in displayedCredentialTypes" value="{{ t.id }}">{{ t.name }}</option>
value="{{option.value}}">{{option.name}}</option>
</select> </select>
</div> </div>
<div id="multi-credential-modal-body"></div> <div id="multi-credential-modal-body"></div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" ng-click="cancelForm()" <button type="button"
ng-click="vm.cancelForm()"
class="Lookup-cancel btn btn-sm Form-cancelButton" class="Lookup-cancel btn btn-sm Form-cancelButton"
translate> translate>
CANCEL CANCEL
</button> </button>
<button type="button" <button type="button"
ng-click="saveForm()" ng-click="vm.saveForm()"
ng-disabled="!credentials || credentials.length === 0" ng-disabled="!credentials || credentials.length === 0"
class="btn btn-sm Form-saveButton" translate> class="btn btn-sm Form-saveButton"
translate>
SELECT SELECT
</button> </button>
</div> </div>

View File

@@ -53,6 +53,9 @@
.MultiCredential-tag--disabled { .MultiCredential-tag--disabled {
background-color: @default-icon; background-color: @default-icon;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
padding-left: 10px;
} }
.MultiCredential-tag--deletable { .MultiCredential-tag--deletable {
@@ -79,7 +82,37 @@
} }
.MultiCredential-tagDelete { .MultiCredential-tagDelete {
font-size: 13px; font-size: 11px;
}
.MultiCredential-iconContainer {
background-color: @default-link!important;
color: @default-bg;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
padding: 0px 5px;
margin: 3px 0px;
margin-left: -3px;
align-items: center;
display: flex;
}
.MultiCredential-iconContainer--disabled {
background-color: @default-icon;
color: @default-bg;
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
padding: 0 5px;
padding-left: 10px;
margin: 3px 0px;
align-items: center;
display: flex;
}
.MultiCredential-tagIcon {
margin: 0px 0px;
font-size: 10px;
} }
.MultiCredential-name { .MultiCredential-name {
@@ -91,7 +124,7 @@
.MultiCredential-name--label { .MultiCredential-name--label {
color: @default-list-header-bg; color: @default-list-header-bg;
font-size: 10px; font-size: 10px;
margin-left: -7px; margin-left: -8px;
margin-right: 5px; margin-right: 5px;
text-transform: uppercase; text-transform: uppercase;
} }

View File

@@ -1,70 +1,70 @@
/************************************************* /*************************************************
* Copyright (c) 2017 Ansible, Inc. * Copyright (c) 2018 Ansible, Inc.
* *
* All Rights Reserved * All Rights Reserved
*************************************************/ *************************************************/
const templatePath = 'templates/job_templates/multi-credential/multi-credential';
export default ['templateUrl', '$compile', function MultiCredential ($compile, templateUrl) {
function(templateUrl, $compile) { return {
return { templateUrl: templateUrl(templatePath),
scope: { require: ['multiCredential'],
credentials: '=', restrict: 'E',
selectedCredentials: '=', controllerAs: 'vm',
prompt: '=', scope: {
credentialNotPresent: '=', selectedCredentials: '=',
credentialsToPost: '=', prompt: '=',
fieldIsDisabled: '=' credentialNotPresent: '=',
}, fieldIsDisabled: '=',
restrict: 'E', credentialTypes: '=',
templateUrl: templateUrl('templates/job_templates/multi-credential/multi-credential'), },
controller: ['$scope', 'MultiCredentialService', link: (scope, element, attrs, controllers) => {
function($scope, MultiCredentialService) { const [controller] = controllers;
if (!$scope.selectedCredentials) {
$scope.selectedCredentials = {
machine: null,
vault: null,
extra: []
};
}
if (!$scope.credentialsToPost) { scope.openModal = () => {
$scope.credentialsToPost = []; const containerElement = $('#content-container');
} const templateFunction = $compile(`
<multi-credential-modal
credential-types="credentialTypes"
selected-credentials="selectedCredentials">
</multi-credential-modal>`);
containerElement.append(templateFunction(scope));
};
$scope.fieldDirty = false; controller.init(scope);
},
controller: multiCredentialController,
};
}
$scope.$watchGroup(['prompt', 'credentialsToPost'], function multiCredentialController (MultiCredentialService) {
function() { const vm = this;
if ($scope.prompt || const { createTag } = MultiCredentialService;
$scope.credentialsToPost.length) {
$scope.fieldDirty = true;
}
$scope.credentialNotPresent = !$scope.prompt && let scope;
$scope.selectedCredentials.machine === null &&
$scope.selectedCredentials.vault === null;
});
$scope.removeCredential = function(credToRemove) { vm.init = _scope_ => {
[$scope.selectedCredentials, scope = _scope_;
$scope.credentialsToPost] = MultiCredentialService scope.$watchCollection('selectedCredentials', onSelectedCredentialsChanged);
.removeCredential(credToRemove, $scope. };
selectedCredentials,
$scope.credentialsToPost); function onSelectedCredentialsChanged (oldValues, newValues) {
}; if (oldValues !== newValues) {
} scope.fieldDirty = (scope.prompt && scope.selectedCredentials.length > 0);
], scope.tags = scope.selectedCredentials.map(c => createTag(c, scope.credentialTypes));
link: function(scope) { }
scope.openMultiCredentialModal = function() {
$('#content-container')
.append($compile(`
<multi-credential-modal
credentials="credentials"
credentials-to-post="credentialsToPost"
selected-credentials="selectedCredentials">
</multi-credential-modal>`)(scope));
};
}
};
} }
];
vm.deselectCredential = ({ id }) => {
const index = scope.selectedCredentials.map(c => c.id).indexOf(id);
if (index > -1) {
scope.selectedCredentials.splice(index, 1);
}
};
}
MultiCredential.$inject = ['$compile', 'templateUrl'];
multiCredentialController.$inject = ['MultiCredentialService'];
export default MultiCredential;

View File

@@ -3,39 +3,49 @@
<button type="button" <button type="button"
class="Form-lookupButton class="Form-lookupButton
Form-lookupButton--variableHeight btn btn-default" Form-lookupButton--variableHeight btn btn-default"
ng-click="openMultiCredentialModal()" ng-click="openModal()"
ng-disabled="fieldIsDisabled"> ng-disabled="fieldIsDisabled">
<i class="fa fa-search"></i> <i class="fa fa-search"></i>
</button> </button>
</span> </span>
<span class="form-control Form-textInput Form-textInput--variableHeight <span class="form-control Form-textInput Form-textInput--variableHeight input-medium lookup"
input-medium lookup" ng-class="{'ng-dirty': fieldDirty}"
ng-class="{ ng-disabled="fieldIsDisabled"
'ng-invalid': credentialNotPresent, style="padding: 4px 6px;">
'ng-dirty': fieldDirty
}"
ng-disabled="fieldIsDisabled"
style="padding: 4px 6px;">
<div class="MultiCredential-tags"> <div class="MultiCredential-tags">
<div class="MultiCredential-tagSection"> <div class="MultiCredential-tagSection">
<div class="MultiCredential-flexContainer"> <div class="MultiCredential-flexContainer">
<div class="MultiCredential-tagContainer ng-scope" <div class="MultiCredential-tagContainer ng-scope"
ng-repeat="tag in credentialsToPost track by $index"> ng-repeat="tag in tags track by $index">
<div class="MultiCredential-deleteContainer" <div class="MultiCredential-deleteContainer"
ng-click="removeCredential(tag.id)" ng-click="vm.deselectCredential(tag)"
ng-hide="fieldIsDisabled || tag.readOnly"> ng-hide="fieldIsDisabled || tag.readOnly">
<i class="fa fa-times MultiCredential-tagDelete"> <i class="fa fa-times MultiCredential-tagDelete"></i>
</i> </div>
<div class="MultiCredential-iconContainer--disabled" ng-switch="tag.kind" ng-if="fieldIsDisabled || tag.readOnly">
<i class="fa fa-cloud MultiCredential-tagIcon" ng-switch-when="cloud"></i>
<i class="fa fa-info MultiCredential-tagIcon" ng-switch-when="insights"></i>
<i class="fa fa-sitemap MultiCredential-tagIcon" ng-switch-when="net"></i>
<i class="fa fa-code-fork MultiCredential-tagIcon" ng-switch-when="scm"></i>
<i class="fa fa-key MultiCredential-tagIcon" ng-switch-when="ssh"></i>
<i class="fa fa-archive MultiCredential-tagIcon" ng-switch-when="vault"></i>
</div>
<div class="MultiCredential-iconContainer" ng-switch="tag.kind" ng-if="!(fieldIsDisabled || tag.readOnly)">
<i class="fa fa-cloud MultiCredential-tagIcon" ng-switch-when="cloud"></i>
<i class="fa fa-info MultiCredential-tagIcon" ng-switch-when="insights"></i>
<i class="fa fa-sitemap MultiCredential-tagIcon" ng-switch-when="net"></i>
<i class="fa fa-code-fork MultiCredential-tagIcon" ng-switch-when="scm"></i>
<i class="fa fa-key MultiCredential-tagIcon" ng-switch-when="ssh"></i>
<i class="fa fa-archive MultiCredential-tagIcon" ng-switch-when="vault"></i>
</div> </div>
<div class="MultiCredential-tag" <div class="MultiCredential-tag"
ng-class="{'MultiCredential-tag--deletable': !fieldIsDisabled && !tag.readOnly, 'MultiCredential-tag--disabled': tag.readOnly}"> ng-class="{'MultiCredential-tag--deletable': !fieldIsDisabled && !tag.readOnly, 'MultiCredential-tag--disabled': tag.readOnly}">
<span class="MultiCredential-name--label <span ng-if="!tag.info" class="MultiCredential-name--label ng-binding">
ng-binding">
{{ tag.kind }}
</span>
<span class="MultiCredential-name u-wordwrap ng-binding">
{{ tag.name }} {{ tag.name }}
</span> </span>
<span ng-if="tag.info" class="MultiCredential-name--label ng-binding">
{{ tag.name }} | {{ tag.info }}
</span>
</div> </div>
</div> </div>
</div> </div>
@@ -43,7 +53,3 @@
</div> </div>
</span> </span>
</div> </div>
<div class="error" ng-cloak ng-show="credentialNotPresent && fieldDirty"
translate>
Please select a machine (SSH) credential or check the "Prompt on launch" option.
</div>

View File

@@ -1,284 +1,102 @@
export default ['Rest', 'ProcessErrors', '$q', 'GetBasePath', function(Rest, ProcessErrors, $q, GetBasePath) { function MultiCredentialService (Rest, ProcessErrors, $q, GetBasePath) {
let val = {};
// given an array of creds, POST them to url and return an array const handleError = (method, resource, permitted = []) => {
// of promises return ({ data, status }) => {
val.saveExtraCredentials = ({creds, url, disassociate = false, if (permitted.indexOf(status) > -1) {
msg = "Failed to add extra credential. POST returned status:"}) => { return { data, status };
if (creds && creds[0] && typeof creds[0] !== 'number') {
creds = creds.map(cred => cred.id);
} }
const hdr = 'Error!';
const msg = `${resource} request failed. ${method} returned status: ${status}`;
return ProcessErrors(null, data, status, null, { hdr, msg });
};
};
return creds.map((cred_id) => { const associate = ({ related }, id) => {
let payload = {'id': cred_id}; Rest.setUrl(related.credentials);
return Rest
.post({ id })
.then(({ data }) => data.results)
.catch(handleError('POST', 'credential association'));
};
if (disassociate) { const disassociate = ({ related }, id) => {
payload.disassociate = true; Rest.setUrl(related.credentials);
} return Rest
.post({ id, disassociate: true })
.catch(handleError('POST', 'credential disassociation'));
};
Rest.setUrl(url); this.saveRelated = ({ related }, credentials) => {
Rest.setUrl(related.credentials);
return Rest
.get()
.then(({ data }) => {
const currentlyAssociated = data.results.map(c => c.id);
const selected = credentials.map(c => c.id);
return Rest.post(payload) const disassociationPromises = currentlyAssociated
.catch(({data, status}) => { .filter(id => selected.indexOf(id) < 0)
ProcessErrors( .map(id => disassociate({ related }, id));
null, data, status, null,
{ const associationPromises = selected
hdr: 'Error!', .filter(id => currentlyAssociated.indexOf(id) < 0)
msg: `${msg} ${status}` .map(id => associate({ related }, id));
});
}); const promises = disassociationPromises
.concat(associationPromises);
return $q.all(promises);
}); });
}; };
// removes credentials no longer a part of the jt, and adds this.getRelated = ({ related }, params = { permitted: [] }) => {
// new ones Rest.setUrl(related.credentials);
val.findChangedExtraCredentials = ({creds, url}) => { return Rest
Rest.setUrl(url); .get()
Rest.get() .catch(handleError('GET', 'related credentials', params.permitted));
.then(({data}) => {
let existingCreds = data.results
.map(cred => cred.id);
let newCreds = creds
.map(cred => cred.id);
let [toAdd, toRemove] = _.partition(_.xor(existingCreds,
newCreds), cred => (newCreds.indexOf(cred) > -1));
let destroyResolve = [];
destroyResolve = val.saveExtraCredentials({
creds: toRemove,
url: url,
disassociate: true,
msg: `Failed to disassociate existing credential.
POST returned status:`
});
$q.all(destroyResolve).then(() => {
val.saveExtraCredentials({
creds: toAdd,
url: url
});
});
})
.catch(({data, status}) => {
ProcessErrors(null, data, status, null, {
hdr: 'Error!',
msg: 'Failed to get existing extra credentials. GET ' +
'returned status: ' + status
});
});
}; };
// calls credential types and returns the data needed to set up the this.getCredentials = () => {
// credential type selector Rest.setUrl(GetBasePath('credentials'));
val.getCredentialTypes = () => { return Rest
.get()
.catch(handleError('GET', 'related credentials'));
};
this.getCredentialTypes = () => {
Rest.setUrl(GetBasePath('credential_types')); Rest.setUrl(GetBasePath('credential_types'));
return Rest.get() return Rest
.then(({data}) => { .get()
let credential_types = {}, credentialTypeOptions = []; .catch(handleError('GET', 'credential types'));
data.results.forEach((credentialType => {
credential_types[credentialType.id] = credentialType;
if(credentialType.kind
.match(/^(machine|cloud|net|ssh|vault)$/)) {
credentialTypeOptions.push({
name: credentialType.name,
value: credentialType.id
});
}
}));
return {
credential_types,
credentialTypeOptions
};
})
.catch(({data, status}) => {
ProcessErrors(null, data, status, null, {
hdr: 'Error!',
msg: 'Failed to get credential types. GET returned ' +
'status: ' + status
});
});
}; };
// converts structured selected credential data into array for tag-based this.isReadOnly = credential => {
// view const canEdit = _.get(credential, 'summary_fields.user_capabilities.edit');
val.updateCredentialTags = (creds, typeOpts) => { const canDelete = _.get(credential, 'summary_fields.user_capabilities.delete');
let machineCred = [];
let extraCreds = [];
let vaultCred = [];
if (creds.machine) { return !(canEdit || canDelete);
let mach = creds.machine;
mach.postType = "machine";
machineCred = [mach];
}
if (creds.vault) {
let vault = creds.vault;
vault.postType = "vault";
vaultCred = [vault];
}
if (creds.extra) {
extraCreds = creds.extra
.map((cred) => {
cred.postType = "extra";
return cred;
});
}
return machineCred.concat(extraCreds).concat(vaultCred).map(cred => ({
name: cred.name,
id: cred.id,
postType: cred.postType,
readOnly: cred.readOnly ? true : false,
kind: typeOpts
.filter(type => {
return parseInt(cred.credential_type) === type.value;
})[0].name + ":"
}));
}; };
// remove credential from structured selected credential data and tag-view this.createTag = (credential, credential_types) => {
// array const credentialTypeId = credential.credential_type || credential.credential_type_id;
val.removeCredential = (credToRemove, structuredObj, tagArr) => { const credentialType = credential_types.find(t => t.id === credentialTypeId);
tagArr.forEach((cred) => {
if (credToRemove === cred.id) {
if (cred.postType === 'machine') {
structuredObj[cred.postType] = null;
} else if (cred.postType === 'vault') {
structuredObj[cred.postType] = null;
} else {
structuredObj[cred.postType] = structuredObj[cred.postType]
.filter(cred => cred
.id !== credToRemove);
}
}
});
tagArr = tagArr return {
.filter(cred => cred.id !== credToRemove); id: credential.id,
name: credential.name,
return [structuredObj, tagArr]; kind: _.get(credentialType, 'kind'),
typeName: _.get(credentialType, 'name'),
info: _.get(credential, 'inputs.vault_id'),
readOnly: this.isReadOnly(credential),
};
}; };
}
// load all relevant credential data to populate job template edit form MultiCredentialService.$inject = [
val.loadCredentials = (data) => { 'Rest',
let selectedCredentials = { 'ProcessErrors',
machine: null, '$q',
vault: null, 'GetBasePath',
extra: [] ];
}, credTypes, credTypeOptions, credTags;
let credDefers = []; export default MultiCredentialService;
let job_template_obj = data;
let credentialGetPermissionDenied = false;
// get machine credential
if (data.related.credential) {
Rest.setUrl(data.related.credential);
credDefers.push(Rest.get()
.then(({data}) => {
selectedCredentials.machine = data;
})
.catch(({data, status}) => {
if (status === 403) {
/* User doesn't have read access to the machine credential, so use summary_fields */
credentialGetPermissionDenied = true;
selectedCredentials.machine = job_template_obj.summary_fields.credential;
selectedCredentials.machine.credential_type = job_template_obj.summary_fields.credential.credential_type_id;
selectedCredentials.machine.readOnly = true;
} else {
ProcessErrors(
null, data, status, null,
{
hdr: 'Error!',
msg: 'Failed to get machine credential. ' +
'Get returned status: ' +
status
});
}
}));
}
if (data.related.vault_credential) {
Rest.setUrl(data.related.vault_credential);
credDefers.push(Rest.get()
.then(({data}) => {
selectedCredentials.vault = data;
})
.catch(({data, status}) => {
if (status === 403) {
/* User doesn't have read access to the vault credential, so use summary_fields */
credentialGetPermissionDenied = true;
selectedCredentials.vault = job_template_obj.summary_fields.vault_credential;
selectedCredentials.vault.credential_type = job_template_obj.summary_fields.vault_credential.credential_type_id;
selectedCredentials.vault.readOnly = true;
} else {
ProcessErrors(
null, data, status, null,
{
hdr: 'Error!',
msg: 'Failed to get machine credential. ' +
'Get returned status: ' +
status
});
}
}));
}
// get extra credentials
if (data.related.extra_credentials) {
Rest.setUrl(data.related.extra_credentials);
credDefers.push(Rest.get()
.then(({data}) => {
selectedCredentials.extra = data.results;
})
.catch(({data, status}) => {
if (status === 403) {
/* User doesn't have read access to the extra credentials, so use summary_fields */
credentialGetPermissionDenied = true;
selectedCredentials.extra = job_template_obj.summary_fields.extra_credentials;
_.map(selectedCredentials.extra, (cred) => {
cred.credential_type = cred.credential_type_id;
cred.readOnly = true;
return cred;
});
} else {
ProcessErrors(null, data, status, null,
{
hdr: 'Error!',
msg: 'Failed to get extra credentials. ' +
'Get returned status: ' +
status
});
}
}));
}
// get credential types
credDefers.push(val.getCredentialTypes()
.then(({credential_types, credentialTypeOptions}) => {
credTypes = credential_types;
credTypeOptions = credentialTypeOptions;
}));
return $q.all(credDefers).then(() => {
// get credential tags
credTags = val
.updateCredentialTags(selectedCredentials, credTypeOptions);
return [selectedCredentials, credTypes, credTypeOptions,
credTags, credentialGetPermissionDenied];
});
};
return val;
}];

View File

@@ -39,6 +39,7 @@ const vault = createFormSection({
selector: '.at-InputGroup-inset', selector: '.at-InputGroup-inset',
labels: { labels: {
vaultPassword: 'Vault Password', vaultPassword: 'Vault Password',
vaultIdentifier: 'Vault Identifier'
} }
}); });

View File

@@ -226,11 +226,10 @@ module.exports = {
.waitForElementVisible('div.spinny') .waitForElementVisible('div.spinny')
.waitForElementNotVisible('div.spinny') .waitForElementNotVisible('div.spinny')
.waitForElementNotPresent('multi-credential-modal tbody tr:nth-child(2)') .waitForElementNotPresent('multi-credential-modal tbody tr:nth-child(2)')
.waitForElementVisible('multi-credential-modal tbody tr:nth-child(1) input[type="radio"]') .waitForElementVisible('multi-credential-modal tbody tr:nth-child(1) input[type="checkbox"]')
.click('multi-credential-modal tbody tr:nth-child(1) input[type="radio"]') .click('multi-credential-modal tbody tr:nth-child(1) input[type="checkbox"]')
.click('multi-credential-modal button[class*="save"]') .click('multi-credential-modal button[class*="save"]')
.waitForElementVisible('div.spinny') .pause(1000);
.waitForElementNotVisible('div.spinny');
return this; return this;
}, },
@@ -256,8 +255,7 @@ module.exports = {
.waitForElementVisible('multi-credential-modal tbody tr:nth-child(1) input[type="radio"]') .waitForElementVisible('multi-credential-modal tbody tr:nth-child(1) input[type="radio"]')
.click('multi-credential-modal tbody tr:nth-child(1) input[type="radio"]') .click('multi-credential-modal tbody tr:nth-child(1) input[type="radio"]')
.click('multi-credential-modal button[class*="save"]') .click('multi-credential-modal button[class*="save"]')
.waitForElementVisible('div.spinny') .pause(1000);
.waitForElementNotVisible('div.spinny');
return this; return this;
} }

View File

@@ -6,12 +6,12 @@ const INVENTORY_NAME = `inventory-${id}`;
const MACHINE_CREDENTIAL_NAME = `credential-machine-${id}`; const MACHINE_CREDENTIAL_NAME = `credential-machine-${id}`;
const ORGANIZATION_NAME = `organization-${id}`; const ORGANIZATION_NAME = `organization-${id}`;
const PROJECT_NAME = `project-${id}`; const PROJECT_NAME = `project-${id}`;
const PROJECT_URL = 'https://github.com/ansible/awx'; const PROJECT_URL = 'https://github.com/jlaska/ansible-playbooks';
const PROJECT_BRANCH = 'devel'; const PROJECT_BRANCH = 'master';
const PLAYBOOK_NAME = 'awx/ui/test/e2e/tests/smoke.yml'; const PLAYBOOK_NAME = 'multivault.yml';
const TEMPLATE_NAME = `template-${id}`; const TEMPLATE_NAME = `template-${id}`;
const VAULT_CREDENTIAL_NAME = `credential-vault-${id}`; const VAULT_CREDENTIAL_NAME_1 = `credential-vault-${id}-1`;
const VAULT_CREDENTIAL_PASSWORD = 'VAULT_CREDENTIAL_PASSWORD'; const VAULT_CREDENTIAL_NAME_2 = `credential-vault-${id}-2`;
module.exports = { module.exports = {
'login to awx': client => { 'login to awx': client => {
@@ -153,7 +153,7 @@ module.exports = {
client.waitForElementVisible('div.spinny'); client.waitForElementVisible('div.spinny');
client.waitForElementNotVisible('div.spinny'); client.waitForElementNotVisible('div.spinny');
}, },
'create vault credential': client => { 'create vault credentials': client => {
const credentials = client.page.credentials(); const credentials = client.page.credentials();
const { details } = credentials.section.add.section; const { details } = credentials.section.add.section;
@@ -171,9 +171,35 @@ module.exports = {
details.waitForElementVisible('@save'); details.waitForElementVisible('@save');
details.clearAndSelectType('Vault'); details.clearAndSelectType('Vault');
details.setValue('@organization', ORGANIZATION_NAME); details.setValue('@organization', ORGANIZATION_NAME);
details.setValue('@name', VAULT_CREDENTIAL_NAME); details.setValue('@name', VAULT_CREDENTIAL_NAME_1);
details.section.vault.setValue('@vaultPassword', VAULT_CREDENTIAL_PASSWORD); details.section.vault.setValue('@vaultPassword', 'secret1');
details.section.vault.setValue('@vaultIdentifier', 'first');
details.expect.element('@save').enabled;
details.click('@save');
credentials.waitForElementVisible('div.spinny');
credentials.waitForElementNotVisible('div.spinny');
credentials.section.navigation.waitForElementVisible('@credentials');
credentials.section.navigation.expect.element('@credentials').enabled;
credentials.section.navigation.click('@credentials');
credentials.waitForElementVisible('div.spinny');
credentials.waitForElementNotVisible('div.spinny');
credentials.section.list.waitForElementVisible('@add');
credentials.section.list.expect.element('@add').enabled;
credentials.section.list.click('@add');
details.waitForElementVisible('@save');
details.clearAndSelectType('Vault');
details.setValue('@organization', ORGANIZATION_NAME);
details.setValue('@name', VAULT_CREDENTIAL_NAME_2);
details.section.vault.setValue('@vaultPassword', 'secret2');
details.section.vault.setValue('@vaultIdentifier', 'second');
details.expect.element('@save').enabled; details.expect.element('@save').enabled;
details.click('@save'); details.click('@save');
@@ -217,7 +243,8 @@ module.exports = {
templates.selectAdd('Job Template'); templates.selectAdd('Job Template');
templates.selectInventory(INVENTORY_NAME); templates.selectInventory(INVENTORY_NAME);
templates.selectProject(PROJECT_NAME); templates.selectProject(PROJECT_NAME);
templates.selectVaultCredential(VAULT_CREDENTIAL_NAME); templates.selectVaultCredential(VAULT_CREDENTIAL_NAME_1);
templates.selectVaultCredential(VAULT_CREDENTIAL_NAME_2);
templates.selectMachineCredential(MACHINE_CREDENTIAL_NAME); templates.selectMachineCredential(MACHINE_CREDENTIAL_NAME);
templates.selectPlaybook(PLAYBOOK_NAME); templates.selectPlaybook(PLAYBOOK_NAME);
templates.sendKeys('label[for="name"] + div input', TEMPLATE_NAME); templates.sendKeys('label[for="name"] + div input', TEMPLATE_NAME);
@@ -253,7 +280,8 @@ module.exports = {
templates.click('i[class$="launch"]'); templates.click('i[class$="launch"]');
}, },
'verify expected job results': client => { 'verify expected job results': client => {
const output = './/span[normalize-space(text())=\'"msg": "Hello World!"\']'; const output1 = './/span[normalize-space(text())=\'"first": "First!"\']';
const output2 = './/span[normalize-space(text())=\'"second": "Second!"\']';
const running = 'i[class$="icon-job-running"]'; const running = 'i[class$="icon-job-running"]';
const success = 'i[class$="icon-job-successful"]'; const success = 'i[class$="icon-job-successful"]';
@@ -265,7 +293,8 @@ module.exports = {
client.waitForElementVisible(success, 60000); client.waitForElementVisible(success, 60000);
client.useXpath(); client.useXpath();
client.waitForElementVisible(output, 60000); client.waitForElementVisible(output1, 60000);
client.waitForElementVisible(output2, 60000);
client.useCss(); client.useCss();
client.end(); client.end();

View File

@@ -1,10 +1,25 @@
--- ---
- hosts: all # ansible-playbook multivault.yml --vault-id var1@prompt --vault-id var2@prompt
tasks: # Vault password (var1): secret1
- name: Import Vault Variables # Vault password (var2): secret2
include_vars: smoke-vars.yml
no_log: true
- name: Display Vault Message - hosts: all
debug: gather_facts: false
msg: '{{ vault_message }}' vars:
first: !vault |
$ANSIBLE_VAULT;1.2;AES256;first
30326539376633656433636231653132623266336338316462356132366361653566303364353335
6665626463633737666336643334353262373836613332650a353531666262636531383430363935
33633465306165393538323336323135393730383563653738666163633835383262396135353765
6238333837306332630a336538623333313636353363326666613564353666623635373432386162
3562
second: !vault |
$ANSIBLE_VAULT;1.2;AES256;second
34653738643565633930336534363230343562343362643432616165373034376565353833366361
6264346330376564643262643166623164323433336631360a396336353866323663613935383534
33643034373439326435373539323433313832366437303764353562653834623966663533613464
3961663934613264360a613763346638636566386461333235366335336564353935356232316265
3164
tasks:
- debug: var=first
- debug: var=second

View File

@@ -1,416 +0,0 @@
'use strict';
describe('MultiCredentialService', () => {
let MultiCredentialService;
beforeEach(angular.mock.module('multiCredential',
($provide) => {
['Rest', 'ProcessErrors', '$q', 'GetBasePath']
.forEach(item => $provide.value(item, {}));
}));
beforeEach(angular.mock.inject((_MultiCredentialService_) => {
MultiCredentialService = _MultiCredentialService_;
}));
describe('saveExtraCredentials', () => {
xit('should handle creds as array of objects and array of ids', () => {
expect(false).toBe(true);
});
xit('should post creds with add payload', () => {
expect(false).toBe(true);
});
xit('should post creds with disassociate payload', () => {
expect(false).toBe(true);
});
xit('should call ProcessErrors when post fails', () => {
expect(false).toBe(true);
});
});
describe('findChangedExtraCredentials', () => {
xit('should find which creds to add and post them', () => {
expect(false).toBe(true);
});
xit('should find which creds to remove and disassociate them', () => {
expect(false).toBe(true);
});
xit('should not post/disassociate non-changed creds', () => {
expect(false).toBe(true);
});
xit('should call ProcessErrors when any get/post fails', () => {
expect(false).toBe(true);
});
});
describe('getCredentialTypes', () => {
xit('should get cred types and return them directly, as well ' +
'as options for building credential type select box', () => {
expect(false).toBe(true);
});
xit('should call ProcessErrors when getting cred types fails', () => {
expect(false).toBe(true);
});
});
describe('updateCredentialTags', () => {
it('should return array of selected credentials (empty)', () => {
let creds = {
machine: null,
extra: []
};
let typeOpts = [];
let expected = [];
let actual = MultiCredentialService
.updateCredentialTags(creds, typeOpts);
let equal = _.isEqual(expected.sort(), actual.sort());
expect(equal).toBe(true);
});
it('should return array of selected credentials (populated, not read only)', () => {
let creds = {
machine: {
credential_type: 1,
id: 3,
name: 'ssh'
},
extra: [
{
credential_type: 2,
id: 4,
name: 'aws'
},
{
credential_type: 3,
id: 5,
name: 'gce'
}
]
};
let typeOpts = [
{
name: 'SSH',
value: 1
},
{
name: 'Amazon Web Services',
value: 2
},
{
name: 'Google Compute Engine',
value: 3
}
];
let expected = [
{
name: 'ssh',
id: 3,
postType: 'machine',
readOnly: false,
kind: 'SSH:'
},
{
name: 'aws',
id: 4,
postType: 'extra',
readOnly: false,
kind: 'Amazon Web Services:'
},
{
name: 'gce',
id: 5,
postType: 'extra',
readOnly: false,
kind: 'Google Compute Engine:'
}
];
let actual = MultiCredentialService
.updateCredentialTags(creds, typeOpts);
let equal = _.isEqual(expected.sort(), actual.sort());
expect(equal).toBe(true);
});
it('should return array of selected credentials (populated, read only)', () => {
let creds = {
machine: {
credential_type: 1,
id: 3,
name: 'ssh',
readOnly: true
},
extra: [
{
credential_type: 2,
id: 4,
name: 'aws',
readOnly: true
},
{
credential_type: 3,
id: 5,
name: 'gce',
readOnly: true
}
]
};
let typeOpts = [
{
name: 'SSH',
value: 1
},
{
name: 'Amazon Web Services',
value: 2
},
{
name: 'Google Compute Engine',
value: 3
}
];
let expected = [
{
name: 'ssh',
id: 3,
postType: 'machine',
readOnly: true,
kind: 'SSH:'
},
{
name: 'aws',
id: 4,
postType: 'extra',
readOnly: true,
kind: 'Amazon Web Services:'
},
{
name: 'gce',
id: 5,
postType: 'extra',
readOnly: true,
kind: 'Google Compute Engine:'
}
];
let actual = MultiCredentialService
.updateCredentialTags(creds, typeOpts);
let equal = _.isEqual(expected.sort(), actual.sort());
expect(equal).toBe(true);
});
});
describe('removeCredential', () => {
it('should remove machine cred from structured obj and tag arr', () => {
let credToRemove = 3;
let structuredObj = {
machine: {
credential_type: 1,
id: 3,
name: 'ssh'
},
extra: [
{
credential_type: 2,
id: 4,
name: 'aws'
},
{
credential_type: 3,
id: 5,
name: 'gce'
}
]
};
let tagArr = [
{
name: 'ssh',
id: 3,
postType: 'machine',
kind: 'SSH:'
},
{
name: 'aws',
id: 4,
postType: 'extra',
kind: 'Amazon Web Services:'
},
{
name: 'gce',
id: 5,
postType: 'extra',
kind: 'Google Compute Engine:'
}
];
let expected = [
{
machine: null,
extra: [
{
credential_type: 2,
id: 4,
name: 'aws'
},
{
credential_type: 3,
id: 5,
name: 'gce'
}
]
},
[
{
name: 'aws',
id: 4,
postType: 'extra',
kind: 'Amazon Web Services:'
},
{
name: 'gce',
id: 5,
postType: 'extra',
kind: 'Google Compute Engine:'
}
]
];
let actual = MultiCredentialService
.removeCredential(credToRemove, structuredObj, tagArr);
let equal = _.isEqual(expected.sort(), actual.sort());
expect(equal).toBe(true);
});
it('should remove extra cred from structured obj and tag arr', () => {
let credToRemove = 4;
let structuredObj = {
machine: {
credential_type: 1,
id: 3,
name: 'ssh'
},
extra: [
{
credential_type: 2,
id: 4,
name: 'aws'
},
{
credential_type: 3,
id: 5,
name: 'gce'
}
]
};
let tagArr = [
{
name: 'ssh',
id: 3,
postType: 'machine',
kind: 'SSH:'
},
{
name: 'aws',
id: 4,
postType: 'extra',
kind: 'Amazon Web Services:'
},
{
name: 'gce',
id: 5,
postType: 'extra',
kind: 'Google Compute Engine:'
}
];
let expected = [
{
machine: {
credential_type: 1,
id: 3,
name: 'ssh'
},
extra: [
{
credential_type: 3,
id: 5,
name: 'gce'
}
]
},
[
{
name: 'ssh',
id: 3,
postType: 'machine',
kind: 'SSH:'
},
{
name: 'gce',
id: 5,
postType: 'extra',
kind: 'Google Compute Engine:'
}
]
];
let actual = MultiCredentialService
.removeCredential(credToRemove, structuredObj, tagArr);
let equal = _.isEqual(expected.sort(), actual.sort());
expect(equal).toBe(true);
});
});
describe('loadCredentials', () => {
xit('should call to get machine credential data', () => {
expect(false).toBe(true);
});
xit('should call ProcessErrors if machine cred get fails', () => {
expect(false).toBe(true);
});
xit('should call to get extra credentials data', () => {
expect(false).toBe(true);
});
xit('should call ProcessErrors if extra creds get fails', () => {
expect(false).toBe(true);
});
xit('should call to get credential types', () => {
expect(false).toBe(true);
});
xit('should call to update cred tags once GETs have completed', () => {
expect(false).toBe(true);
});
});
});