'use strict'; require(['jquery', 'underscore', 'splunkjs/mvc', 'splunkjs/mvc/utils', 'splunkjs/mvc/tokenutils', 'splunkjs/mvc/messages', 'splunkjs/mvc/searchmanager', 'splunkjs/mvc/multidropdownview', 'splunkjs/mvc/dropdownview', '/static/app/rest-storage-passwords-manager/Modal.js', 'splunkjs/mvc/simpleform/input/dropdown', 'splunkjs/mvc/simplexml/ready!'], function ($, _, mvc, utils, TokenUtils, Messages, SearchManager, MultiDropdownView, DropdownView, Modal, Dropdown) { function showPassword(row) { var dfd = $.Deferred(); var splunkJsComponent = mvc.Components.getInstance("passwordSearch"); if(splunkJsComponent) { splunkJsComponent.dispose(); } var passwordSearch = new SearchManager({ "id": "passwordSearch", "cancelOnUnload": true, "status_buckets": 0, "earliest_time": "-24h@h", "latest_time": "now", "sample_ratio": 1, "search": "| rest /servicesNS/-/-/storage/passwords \ | search title=" + row.realm + ":" + row.username + ": \ | table clear_password", "app": utils.getCurrentApp(), "auto_cancel": 90, "preview": true, "tokenDependencies": { }, "runWhenTimeIsUndefined": false }, {tokens: true, tokenNamespace: "submitted"}); var mainSearch = mvc.Components.getInstance("passwordSearch"); var myResults = mainSearch.data('results', { output_mode:'json', count:0 }); mainSearch.on('search:done', function(properties) { if(properties.content.resultCount == 0) { return renderModal("password-not-found", "Not Found", "
No password found. Verify list_storage_passwords capability role is enabled.
", "Close") } }); myResults.on("data", function() { var data = myResults.data().results; dfd.resolve(renderModal("show-password", "Password", "

" + data[0].clear_password + "

", "Close")); }); return dfd.promise(); } function anonCallback(callback=function(){}, callbackArgs=null) { if(callbackArgs) { callback.apply(this, callbackArgs); } else { callback(); } } function genericPromise() { var dfd = $.Deferred(); dfd.resolve(); return dfd.promise(); } // Wrapper to execute multiple searches in order and resolve when they've all finished function all(array){ var deferred = $.Deferred(); var fulfilled = 0, length = array.length; var results = []; if (length === 0) { deferred.resolve(results); } else { _.each(array, function(promise, i){ $.when(promise()).then(function(value) { results[i] = value; fulfilled++; if(fulfilled === length){ deferred.resolve(results); } }); }); } return deferred.promise(); }; // Helper to figure out if the create form is open function isFormOpen() { var formOpen = window.sessionStorage.getItem("formOpen"); if(_.isNull(formOpen) || _.isUndefined(formOpen) || formOpen === "false") { return false; } else { return true; } } // Callback to refresh window and hide create-user function refreshWindow() { setTimeout(function () { location.reload() $('#create-user').show(); }, 500); } function renderModal(id, title, body, buttonText, callback=function(){}, callbackArgs=null) { // Create the modal var myModal = new Modal(id, { title: title, backdrop: 'static', keyboard: false, destroyOnHide: true, type: 'wide' }); // Add content myModal.body.append($(body)); // Add cancel button for update/delete action if(id == "user-delete-confirm" || id == "update-user-form") { myModal.footer.append($('').attr({ type: 'button', 'data-dismiss': 'modal' }) .addClass('btn btn-secondary').text("Cancel")).on('click', function(){}); } // Add footer myModal.footer.append($('

\ \
\
\
\
\ \ \
\
\ \ \
\
\ \ \
\
\ \ \

\
\
\ \
\
\
\ \
\
\
\ \
\
\
\ \
\
\
\ \
\
\
\ \
\
\
\
'; var tdHtml = ""; var contextMenu = ''; var header = '
\
\ \
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ '; html += header; _.each(data, function(row, i) { tdHtml += '\ \ \ \ \ \ \ \ \ \ \ \ \ '; }); tdHtml += "

ID

Username

Password

Realm

App

Clear Password

Owner

Read

Write

Sharing

REST URI

' + i + '' + row.username + '\ \
  • \
    \
    ' + row.realm + '' + row.app + '' + row.clear_password + '' + row.owner + '' + row.acl_read + '' + row.acl_write + '' + row.acl_sharing + '' + row.rest_uri + '
    "; html += tdHtml; // Render table $(tableDiv).append(html); // Rendeer context menu div $(contextMenuDiv).append(contextMenu); // Register click listener on create button $('#main-create').on('click', function () { if(!isFormOpen()) { // Change text to close $('#main-create').text("Close"); window.sessionStorage.setItem("formOpen", "true"); // Clear form values $('input[id=createUsername]').val(""); $('input[id=createRealm]').val(""); } else { $('#main-create').text("Create"); window.sessionStorage.setItem("formOpen", "false"); } // Rnder the create user form anonCallback(renderCreateUserForm, ["",""]) }); // Current row index in table var curIndex = null; // Create bootstrap-table $('#rest-password-table').bootstrapTable({ contextMenu: '#example1-context-menu', onContextMenuItem: function(row, $el){ // Actions for right click on row if($el.data("item") == "update"){ $('#rest-password-table').bootstrapTable('expandRow', curIndex); } else if($el.data("item") == "delete"){ deleteMultiCredential([row]); } }, onContextMenuRow: function(row, $el){ // Set the current index when context menu triggered curIndex = $el.data().index; }, onExpandRow: function(index, row, $detail) { // Add a new table row below current element $detail.html('
    ').find('table').append('
    '); // Logic to collapse previous row when new row expanded $('#rest-password-table').find('.detail-view').each(function () { if (!$(this).is($detail.parent())) { $(this).prev().find('.detail-icon').click() } }) // Render the update form renderUpdateUserInTable(row); } }); // Toggle remove button on or off depending whether rows are checked $('#rest-password-table').on('check.bs.table uncheck.bs.table ' + 'check-all.bs.table uncheck-all.bs.table', function () { $('#remove').prop('disabled', !$('#rest-password-table').bootstrapTable('getSelections').length); }); // Wire remove button to delete credentials $('#remove').click(function () { var rows = getIdSelections(); deleteMultiCredential(rows); $('#remove').prop('disabled', true); }); } // Delete credentials function deleteMultiCredential(rows) { // Delete a single credential var deleteCred = function (row) { var dfd = $.Deferred(); var deleteUrl = "/en-US/splunkd/__raw/servicesNS/" + row.owner + "/" + row.app + "/storage/passwords/" + row.realm + ":" + row.username +":"; var message = []; var payload = {"body": undefined, "responseCode": undefined} var aclUrl = row.rest_uri + "/acl"; var aclData = {"perms.read": row.acl_read, "perms.write": row.acl_write, "sharing": row.acl_sharing == "user" ? "app":row.acl_sharing, "owner": row.owner} $.ajax({ type: "POST", url: aclUrl, data: aclData, error: function(xhr, textStatus, error) { payload.body = "
    Failed to delete user " + row.username + " - " + xhr.responseText + "
    "; payload.responseCode = xhr.status; message.push(payload); dfd.resolve(message); } }) .then(function() { $.ajax({ type: "DELETE", url: deleteUrl, success: function(data, textStatus, xhr) { payload.body = "
    Successfully deleted credential - " + row.realm + ":" + row.username + "
    "; payload.responseCode = xhr.status; message.push(payload); dfd.resolve(message); }, error: function(xhr, textStatus, error) { payload.body = "
    Failed to delete user " + row.username + " - " + xhr.responseText + "
    "; payload.responseCode = xhr.status; message.push(payload); dfd.resolve(message); } }) }) return dfd.promise(); } var removeUsers = function () { // promise array var promises = []; _.each(rows, function(row, i) { // Push each row to be deleted onto promises array promises.push(function() { return deleteCred(row); }); }); // Execute deletes and display message $.when(all(promises)).then(function(messages) { var status = ""; var failCount = 0; var deleteCount = messages.length; var prefix = "Credential"; var modalMessage = $.map(messages, function(i) { var payload = i[0]; if(parseInt(payload.responseCode) != 200) { failCount += 1; } return payload.body; }) if(failCount == deleteCount) { status = prefix + " Delete Failed"; } else if (failCount > 0 && failCount < deleteCount) { status = prefix + " Delete Partially Succeeded" } else { status = prefix + " Deleted"; } renderModal("user-delete", status, modalMessage.join("\n"), "Close", refreshWindow) }); } // Get the usernames from all rows var users = $.map(rows, function(row) { return row.username; }) // Render delete confirmation modal and regester delete callback action var deleteUser = renderModal("user-delete-confirm", "Confirm Delete Action", "
    You're about to remove the users " + users.join(', ') + " - Press ok to continue
    ", "Ok", removeUsers, [rows]); } /** * SplunkJS Input object and methods * @param {object} config Config object holding component specific definitions */ function splunkJSInput(config) { var config = this.config = config; // Form to render for updating user var htmlForm = '
    \
    \
    \
    \ \ \
    \
    \ \ \
    \
    \ \ \
    \
    \ \ \

    \
    \
    \ \
    \
    \
    \ \
    \
    \
    \ \
    \
    \
    \ \
    \
    \
    \ \
    \
    \
    \ \
    \
    \
    \
    ' // Save context var that = this; // Remove component and add div back this.remove = function() { var el = "#" + this.config.parentEl; var splunkJsComponent = mvc.Components.get(this.config.id); if(splunkJsComponent) { splunkJsComponent.remove(); $(el).append('
    '); } } // Remove component from inline update form. Don't add the div back since it's dynamic this.updateRemove = function() { var el = "#" + this.config.parentEl; var splunkJsComponent = mvc.Components.get(this.config.id); if(splunkJsComponent) { splunkJsComponent.remove(); } } // Render the component in the form this.renderComponent = function () { // Remove component if it exists that.remove(); var el = "#" + this.config.el; // Get search manager var splunkJsComponentSearch = mvc.Components.get(this.config.id + "-search"); // Check to make sure div is there before rendering if ($(el).length) { var choices = _.has(this.config, "choices") ? this.config.choices:[]; // Create search manager if it doesn't exist if(!splunkJsComponentSearch) { this.config.searchInstance = new SearchManager({ id: this.config.id + "-search", search: this.config.searchString }); } if(this.config.type == "dropdown") { this.config.instance = new DropdownView({ id: this.config.id, managerid: _.isUndefined(this.config.searchString) ? null:this.config.id + "-search", choices: choices, labelField: "label", valueField: "value", default: _.has(this.config, "default") ? this.config.default:null, el: $(el) }).render(); } else { this.config.instance = new MultiDropdownView({ id: this.config.id, choices: choices, managerid: _.isUndefined(this.config.searchString) ? null:this.config.id + "-search", labelField: "label", valueField: "value", width: 350, default: _.has(this.config, "default") ? this.config.default:null, el: $(el) }).render(); } } else { setTimeout(function() { that.renderComponent(); }, 100); } } // Get values from bootstrap table this.getVals = function() { return this.config.instance.val(); } } // Used to render create form function renderCreateUserForm(cUsername = false, cRealm = false) { var createUser = function createUser() { var aclData = {}; var formVals = {}; _.each(arguments[2], function(component, i) { var aclKey = component.config.aclKey; aclData[aclKey] = _.isArray(component.getVals()) ? component.getVals().join():component.getVals(); formVals[aclKey] = _.isArray(component.config.default) ? component.config.default.join():component.config.default; }); var username = $('input[id=createUsername]').val(); var password = $('input[id=createPassword]').val(); var confirmPassword = $('input[id=createConfirmPassword]').val(); var realm = $('input[id=createRealm]').val(); if(username == "") { return renderModal("missing-username", "Missing Username", "
    Please enter a username
    ", "Close") } if(password == "") { return renderModal("missing-password", "Missing Password", "
    Please enter a password
    ", "Close") } if(username && !password) { return renderModal("missing-password", "Missing Password", "
    Please enter a password
    ", "Close"); } // Create object to POST for user creation var createData = {"name": username, "password": password, "realm": realm}; if(password != confirmPassword) { return renderModal("password-mismatch", "Password Mismatch", "
    Passwords do not match
    ", "Close"); } else { var currentUser = Splunk.util.getConfigValue("USERNAME"); var createUrl = "/en-US/splunkd/__raw/servicesNS/" + currentUser + "/" + aclData.app + "/storage/passwords"; var aclUrl = "/en-US/splunkd/__raw/servicesNS/" + currentUser + "/" + aclData.app + "/configs/conf-passwords/credential%3A" + realm + "%3A" + username + "%3A/acl"; // Success message for final modal display var message = []; $.ajax({ type: "POST", url: createUrl, data: createData, success: function(data, textStatus, xhr) { message.push("
    Successfully created user " + realm + ":" + username + "
    "); }, error: function(xhr, textStatus, error) { message.push("
    Failed to create user " + username + ":" + realm + " - " + xhr.responseText + "
    "); } }) .then(function() { // App not a valid key for updating Splunk ACL's, remove it before posting delete aclData.app; // Need to set sharing to app level before applying ACL's otherwise read/write perms don't apply // ACL's will get re-applied back to user sharing context in next chain ACL apply if(aclData.sharing == "user") { var aclDataCopy = _.clone(aclData); aclDataCopy.sharing = "app"; return $.ajax({ type: "POST", url: aclUrl, data: aclDataCopy, error: function(xhr, textStatus, error) { message.push("
    Failed to apply ACL - " + xhr.responseText + "
    "); } }) } }) .then(function () { return $.ajax({ type: "POST", url: aclUrl, data: aclData, success: function(data, textStatus, xhr) { message.push("
    Successfully applied ACL's
    ") }, error: function(xhr, textStatus, error) { message.push("
    Failed to apply ACL - " + xhr.responseText + "
    "); } }) }) .done(function () { renderModal("user-create", "User Created", message.join('\n'), "Close", refreshWindow) }) .fail(function() { renderModal("user-create-fail", "User Create Failed", message.join('\n'), "Close", refreshWindow) }); } } var inputs = [new splunkJSInput({"id": "app-scope-dropdown", "searchString": "| rest /servicesNS/-/-/apps/local | search disabled=0 | rename title as value | table label, value", "el": "app-scope-dropdown", "type": "dropdown", "default": utils.getCurrentApp(), "aclKey": "app", "parentEl": "app-scope"}), new splunkJSInput({"id": "read-user-multi", "searchString": "| rest /servicesNS/-/-/authorization/roles | eval label=title | rename title as value | fields label, value | append [| rest /servicesNS/-/-/authentication/users | eval label=title | rename title as value | fields label, value] | dedup label", "el": "read-user-multi", "type": "multi-dropdown", "default": "*", "choices": [{"label":"*", "value":"*"}], "aclKey": "perms.read", "parentEl": "read-users"}), new splunkJSInput({"id": "write-user-multi", "searchString": "| rest /servicesNS/-/-/authorization/roles | eval label=title | rename title as value | fields label, value | append [| rest /servicesNS/-/-/authentication/users | eval label=title | rename title as value | fields label, value] | dedup label", "el": "write-user-multi", "type": "multi-dropdown", "parentEl": "write-users", "aclKey": "perms.write", "choices": [{"label":"*", "value":"*"}], "default": ["admin","power"]}), new splunkJSInput({"id": "sharing-dropdown", "choices": [{"label":"global", "value": "global"}, {"label":"app", "value": "app"}, {"label":"user", "value": "user"}], "el": "sharing-dropdown", "type": "dropdown", "parentEl": "sharing", "aclKey": "sharing", "default": "app"}), new splunkJSInput({"id": "owner-dropdown", "searchString": "| rest /servicesNS/-/-/authentication/users | eval label=title | rename title as value | table label, value", "el": "owner-dropdown", "type": "dropdown", "aclKey": "owner", "default": Splunk.util.getConfigValue("USERNAME"), "parentEl": "owner"})]; // Render components _.each(inputs, function(input, i) { input.renderComponent(); }); // Register createUser callback for button clearOnClickAndRegister('#create-submit', createUser, [cUsername, cRealm, inputs]); setTimeout(function () { if(cUsername != "" || cRealm != "") { $('input[id=createUsername]').val(cUsername); $('input[id=createRealm]').val(cRealm); } }, 300); } // Render form under row in bootstrap-table function renderUpdateUserInTable(row) { var updateUser = function updateUser () { var formVals = {}; var aclData = {}; // Process SplunkJS form components _.each(arguments[0], function(component, i) { var aclKey = component.config.aclKey; // Save submitted values aclData[aclKey] = _.isArray(component.getVals()) ? component.getVals().join():component.getVals(); // Save initial form values formVals[aclKey] = _.isArray(component.config.default) ? component.config.default.join():component.config.default; }); // Grab input values var username = $('input[id=updateUsername]').val(); var password = $('input[id=updatePassword]').val(); var confirmPassword = $('input[id=updateConfirmPassword]').val(); var realm = $('input[id=updateRealm]').val(); // Save app info since we delete when posting var aclApp = aclData.app; var app = formVals.app; // App is not a valid field when posting to /acl. Remove from object. delete aclData.app; // Initialze to apply ACL's var applyAcl = true; // Make sure both password fields are set if(!password && confirmPassword) { return renderModal("password-mismatch", "Password Error", "
    Please set password and confirm password fields
    ", "Close"); } // No change when form data and submitted values are the same and password is blank if(JSON.stringify(formVals) === JSON.stringify(aclData) && !password) { return renderModal("no-change", "No Change Detected", "
    Nothing to see here
    ", "Close") } // // If ACL's haven't changed, don't apply // if(JSON.stringify(formVals) === JSON.stringify(aclData)) { // applyAcl = false; // } // Add realm to formVals for refrence in REST url's formVals.realm = arguments[1].realm; if(password != confirmPassword) { renderModal("password-mismatch", "Password Mismatch", "
    Passwords do not match
    ", "Close"); } else { var aclUrl = row.rest_uri + "/acl"; // Success message for final modal display var message = []; // Copy acl data to new object var aclDataCopy = _.clone(aclData); // Set sharing to app for consistent behavior through app and password changes // When sharing is set to app, eai:userName is always 'nobody' and this gives a predictable // URI to change password and move configs between apps. // We'll set sharing back to the form value after password change and movement between apps. aclDataCopy.sharing = "app"; // Apply ACL's $.ajax({ type: "POST", url: aclUrl, data: aclDataCopy, error: function(xhr, textStatus, error) { message.push("
    Failed to update password for user " + username + " - " + xhr.responseText + "
    "); } }) .then (function() { // Change the password if(password) { var passwordUrl = "/en-US/splunkd/__raw/servicesNS/nobody/" + formVals.app + "/storage/passwords/" + formVals.realm + ":" + username + ":"; return $.ajax({ type: "POST", url: passwordUrl, data: {"password": password}, success: function(data, textStatus, xhr) { message.push("
    Successfully updated password for credential - " + formVals.realm + ":" + username + "
    "); }, error: function(xhr, textStatus, error) { message.push("
    Failed to update password for user " + username + " - " + xhr.responseText + "
    "); } }) } }) .then(function() { // Move app context if(formVals.app != aclApp) { var moveUrl = "/en-US/splunkd/__raw/servicesNS/nobody/" + formVals.app + "/configs/conf-passwords/credential%3A" + formVals.realm + "%3A" + username + "%3A/move"; return $.ajax({ type: "POST", url: moveUrl, data: {"app": aclApp, "user": "nobody"}, success: function(data, textStatus, xhr) { message.push("
    Successfully moved credential from " + formVals.app + " to " + aclApp + "
    "); }, error: function(xhr, textStatus, error) { message.push("
    Failed to move credential from " + formVals.app + " to " + aclApp + " - " + xhr.responseText + "
    "); } }) } }) .then(function() { // Apply ACL's back to submitted values aclUrl = "/en-US/splunkd/__raw/servicesNS/nobody/" + aclApp + "/configs/conf-passwords/credential%3A" + formVals.realm + "%3A" + username + "%3A/acl"; return $.ajax({ type: "POST", //url: newAclUrl, url: aclUrl, data: aclData, success: function(data, textStatus, xhr) { message.push("
    Successfully applied ACL's
    ") }, error: function(xhr, textStatus, error) { message.push("
    Failed to apply ACL - " + xhr.responseText + "
    "); } }) }) .done(function() { renderModal("user-updated", "User Updated", message.join('\n'), "Close", refreshWindow) }) .fail(function() { renderModal("user-update-failed", "User Update Failed", message.join('\n'), "Close", refreshWindow) }); } } //var divId = "#" + row.username; var divId = "div[id='" + row.username + "']"; var htmlForm = '
    \
    \ \ \
    \
    \ \ \
    \
    \ \ \
    \
    \ \ \

    \
    \
    \ \
    \
    \
    \ \
    \
    \
    \ \
    \
    \
    \ \
    \
    \
    \ \
    \
    \
    \ \
    \
    ' $(divId).append(htmlForm); // Set form username and realm $('input[id=updateUsername]').val(row.username); $('input[id=updateRealm]').val(row.realm); var inputs = [new splunkJSInput({"id": "app-scope-dropdown-inline", "searchString": "| rest /servicesNS/-/-/apps/local | search disabled=0 | rename title as value | table label, value", "el": "app-scope-dropdown-inline", "type": "dropdown", "default": [row.app], "aclKey": "app", "parentEl": "app-scope-inline"}), new splunkJSInput({"id": "read-user-multi-inline", "searchString": "| rest /servicesNS/-/-/authorization/roles | eval label=title | rename title as value | fields label, value | append [| rest /servicesNS/-/-/authentication/users | eval label=title | rename title as value | fields label, value] | dedup label", "el": "read-user-multi-inline", "type": "multi-dropdown", "default": row.acl_read.split(','), "aclKey": "perms.read", "choices": [{"label":"*", "value":"*"}], "parentEl": "read-users-inline"}), new splunkJSInput({"id": "write-user-multi-inline", "searchString": "| rest /servicesNS/-/-/authorization/roles | eval label=title | rename title as value | fields label, value | append [| rest /servicesNS/-/-/authentication/users | eval label=title | rename title as value | fields label, value] | dedup label", "el": "write-user-multi-inline", "type": "multi-dropdown", "parentEl": "write-users-inline", "aclKey": "perms.write", "choices": [{"label":"*", "value":"*"}], "default": row.acl_write.split(',')}), new splunkJSInput({"id": "sharing-dropdown-inline", "choices": [{"label":"global", "value": "global"}, {"label":"app", "value": "app"}, {"label":"user", "value": "user"}], "el": "sharing-dropdown-inline", "type": "dropdown", "parentEl": "sharing-inline", "aclKey": "sharing", "default": [row.acl_sharing]}), new splunkJSInput({"id": "owner-dropdown-inline", "searchString": "| rest /servicesNS/-/-/authentication/users | eval label=title | rename title as value | table label, value", "el": "owner-dropdown-inline", "type": "dropdown", "default": [row.owner], "aclKey": "owner", "parentEl": "owner-inline"})]; // Render component _.each(inputs, function(input, i) { input.renderComponent(); }); // Register updateUser callback for button clearOnClickAndRegister('#update-submit-inline', updateUser, [inputs, row]); } // listener used to show password when clicking the eye icon in the table window.operateEvents = { 'click .show': function (e, value, row, index) { var aclData = {"perms.read": row.acl_read, "perms.write": row.acl_write, "sharing": row.acl_sharing, "owner": row.owner} // Change sharing to app, show password, change back to user if(row.acl_sharing == "user") { //var aclUrl = "/en-US/splunkd/__raw/servicesNS/" + row.owner + "/" + row.app + "/configs/conf-passwords/credential%3A" + row.realm + "%3A" + row.username + "%3A/acl"; var aclUrl = row.rest_uri + "/acl"; var aclDataCopy = _.clone(aclData); aclDataCopy.sharing = "app"; $.ajax({ type: "POST", url: aclUrl, data: aclDataCopy, error: function(xhr, textStatus, error) { return renderModal("fail-password-view", "Cannot View Password", "
    Failed to view password - " + xhr.responseText + "
    ", "Close"); } }) .then(function() { return showPassword(row); }) .done(function() { $.ajax({ type: "POST", url: aclUrl, data: aclData, error: function(xhr, textStatus, error) { return renderModal("fail-password-view", "Cannot View Password", "
    Failed to view password - " + xhr.responseText + "
    ", "Close"); } }) }) } else { showPassword(row); } } }; // Kick it all off populateTable(); });