diff --git a/deployment-apps/rest-storage-passwords-manager/README.md b/deployment-apps/rest-storage-passwords-manager/README.md new file mode 100644 index 00000000..fe987ecb --- /dev/null +++ b/deployment-apps/rest-storage-passwords-manager/README.md @@ -0,0 +1,152 @@ +# Splunk REST storage/passwords Manager for Splunk + +## About + +An intuitive, full-featured, Javascript CRUD interface to the [Splunk storage/passwords REST endpoint](http://docs.splunk.com/Documentation/Splunk/7.0.3/RESTREF/RESTaccess#storage.2Fpasswordsstorage/passwords). If you're an app developer looking to securely store passwords for API's, Custom Alert Actions, Modular Inputs or to access any resource that requires a password, this is your tool! Create, update, delete, view passwords, change permissions, sharing and app scope. Ditch your curl commands and upgrade to this simple to use interface. + +## Dependencies +To store passwords the user must have the `admin_all_objects` capability enabled within an assigned role. To read passwords a user must have `list_storage_passwords` capabilty enabled within an assigned role. + +## Usage +The Credential Management dashboard provides a CRUD interface to create, update and delete credentials to be used within Splunk apps. + +#### Create Credential +Simply click the create button to reveal the credential creation form. + +![Alt text](appserver/static/img/credential_management-tour:enterprise/credential-create.png?raw=true) + +Fill out the form specifying a username, password and optionally a realm. The realm can be used as a descriptor for the credential or left blank; e.g., prod or dev. The form will populate with sane defaults for owner, read users, write users, app scope and sharing. You can update them to whatever you like, including the target app context, before you hit create. + +![Alt text](appserver/static/img/credential_management-tour:enterprise/create-form.png?raw=true) + +![Alt text](appserver/static/img/credential_management-tour:enterprise/create-success-modal.png?raw=true) + +Once created, the dashboard will be refreshed automatically. + +![Alt text](appserver/static/img/credential_management-tour:enterprise/table.png?raw=true) + +#### Update Credential + +Right click on a table entry to reveal a context menu that allows you to update the credential. + +![Alt text](appserver/static/img/credential_management-tour:enterprise/context-update.png?raw=true) + +Alternatively, you can click the detail view (plus icon) in the table to update the credential. + +![Alt text](appserver/static/img/credential_management-tour:enterprise/detail-view.png?raw=true) + +The update form will be rendered under the selected row in the table. You can change the password, any of the permissions or the app context when updating. The realm is the only field that cannot be changed. This is a limitation of the storage/passwords REST endpoint, not the app. You don't have to set the password to update the ACL's on the credential or move between apps. Simply choose new permissions or app scope and hit update. + +![Alt text](appserver/static/img/credential_management-tour:enterprise/inline-update.png?raw=true) + +#### Delete Credential + +Right click on a table entry to reveal a context menu that allows you to delete the credential. + +![Alt text](appserver/static/img/credential_management-tour:enterprise/context-delete.png?raw=true) + +Alternatively, select any individual credential or select all using the checkbox in the header column and press the delte button. + +![Alt text](appserver/static/img/credential_management-tour:enterprise/multi-delete.png?raw=true) + +![Alt text](appserver/static/img/credential_management-tour:enterprise/multi-delete-confirm.png?raw=true) + +#### Reveal Clear Password + +Click the eye icon to view the plain text password. + +![Alt text](appserver/static/img/credential_management-tour:enterprise/show-password.png?raw=true) + +![Alt text](appserver/static/img/credential_management-tour:enterprise/clear-password-modal.png?raw=true) + +## Using Stored Passwords +Please see this [awesome blog post](http://www.georgestarcher.com/splunk-stored-encrypted-credentials/) on using your newly stored credentials. When all else fails, dig into [dev.splunk.com](http://dev.splunk.com/search/?q=storage%2Fpasswords&l=en&submit=Search) for more details. + +## Credits + +Some of the components included in REST storage/passwords Manager App for Splunk are licensed under free or open source licenses. We wish to thank the contributors to those projects. + +Twitter Bootstrap dropdown.js v3.3.7 + +Copyright (c) 2011-2018 Twitter, Inc. +Copyright (c) 2011-2018 The Bootstrap Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +bootstrap-table.js v1.11.1 + +(The MIT License) + +Copyright (c) 2012-2017 Zhixin Wen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +bootstrap-table-contextmenu.js v1.1.4 + + +The MIT License (MIT) + +Copyright (c) 2015 David Graham + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +## Support + +### Feature Requests +Please [submit feature requests through Github](https://github.com/sghaskell/rest-storage-passwords-manager/labels/enhancement) using the ``enhancement`` label so they can be tracked and discussed. + +### Bugs +Please [submit bugs through Github](https://github.com/sghaskell/rest-storage-passwords-manager/labels/bug) using the ``bug`` label so they can be tracked and discussed. + +###### For all other inquiries +Scott Haskell ([shaskell@splunk.com](mailto:shaskell@splunk.com)) +###### [Code hosted at Github](https://github.com/sghaskell/rest-storage-passwords-manager) \ No newline at end of file diff --git a/deployment-apps/rest-storage-passwords-manager/appserver/static/Modal.js b/deployment-apps/rest-storage-passwords-manager/appserver/static/Modal.js new file mode 100644 index 00000000..47ffcfe3 --- /dev/null +++ b/deployment-apps/rest-storage-passwords-manager/appserver/static/Modal.js @@ -0,0 +1,133 @@ +'use strict'; + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _setModalMaxHeight(element) { + this.$element = $(element); + this.$content = this.$element.find('.modal-content'); + var borderWidth = this.$content.outerHeight() - this.$content.innerHeight(); + var dialogMargin = $(window).width() < 768 ? 20 : 60; + var contentHeight = $(window).height() - (dialogMargin + borderWidth); + var headerHeight = this.$element.find('.modal-header').outerHeight() || 0; + var footerHeight = this.$element.find('.modal-footer').outerHeight() || 0; + var maxHeight = contentHeight - (headerHeight + footerHeight); + + this.$content.css({ + 'overflow': 'hidden' + }); + + this.$element + .find('.modal-body').css({ + 'max-height': maxHeight, + 'overflow-y': 'auto' + }); + } + +define(['underscore'], function (_) { + return function () { + /** + * A utility wrapper around Bootstrap's modal. + * @param {string|object} id Either an id or a jQuery element that contains the id in its "data-target" attribute + * @param {object} [options] Bootstrap modal options + * @param {boolean|string} [options.backdrop] Whether or not to show a backdrop, or the string "static" to show a backdrop that doesn't close the modal when clicked + * @param {boolean} [options.keyboard] Whether or not the escape key clsoes the modal + * @param {boolean} [options.show=false] Whether or not to show the modal when it's created + * @param {string} [options.type='normal'] Can be 'normal', 'wide', or 'noPadding' + * @param {string} [options.title] The modal's title + * @param {boolean} [options.destroyOnHide=true] Destroy the modal when it's hidden + * @returns {element} + */ + function Modal(id, options) { + var _this = this; + + _classCallCheck(this, Modal); + + var modalOptions = _.extend({ show: false}, options); + + // if "id" is the element that triggers the modal display, extract the actual id from it; otherwise use it as-is + var modalId = id != null && (typeof id === 'undefined' ? 'undefined' : _typeof(id)) === 'object' && id.jquery != null ? id.attr('data-target').slice(1) : id; + + var header = $('
').addClass('modal-header'); + + var headerCloseButton = $(''); + } + + if (this.options.showRefresh) { + html.push(sprintf(''); + } + + if (this.options.showToggle) { + html.push(sprintf(''); + } + + if (this.options.showColumns) { + html.push(sprintf('
', + this.options.formatColumns()), + '', + '', + '
'); + } + + html.push('
'); + + // Fix #188: this.showToolbar is for extensions + if (this.showToolbar || html.length > 2) { + this.$toolbar.append(html.join('')); + } + + if (this.options.showPaginationSwitch) { + this.$toolbar.find('button[name="paginationSwitch"]') + .off('click').on('click', $.proxy(this.togglePagination, this)); + } + + if (this.options.showRefresh) { + this.$toolbar.find('button[name="refresh"]') + .off('click').on('click', $.proxy(this.refresh, this)); + } + + if (this.options.showToggle) { + this.$toolbar.find('button[name="toggle"]') + .off('click').on('click', function () { + that.toggleView(); + }); + } + + if (this.options.showColumns) { + $keepOpen = this.$toolbar.find('.keep-open'); + + if (switchableCount <= this.options.minimumCountColumns) { + $keepOpen.find('input').prop('disabled', true); + } + + $keepOpen.find('li').off('click').on('click', function (event) { + event.stopImmediatePropagation(); + }); + $keepOpen.find('input').off('click').on('click', function () { + var $this = $(this); + + that.toggleColumn($(this).val(), $this.prop('checked'), false); + that.trigger('column-switch', $(this).data('field'), $this.prop('checked')); + }); + } + + if (this.options.search) { + html = []; + html.push( + ''); + + this.$toolbar.append(html.join('')); + $search = this.$toolbar.find('.search input'); + $search.off('keyup drop blur').on('keyup drop blur', function (event) { + if (that.options.searchOnEnterKey && event.keyCode !== 13) { + return; + } + + if ($.inArray(event.keyCode, [37, 38, 39, 40]) > -1) { + return; + } + + clearTimeout(timeoutId); // doesn't matter if it's 0 + timeoutId = setTimeout(function () { + that.onSearch(event); + }, that.options.searchTimeOut); + }); + + if (isIEBrowser()) { + $search.off('mouseup').on('mouseup', function (event) { + clearTimeout(timeoutId); // doesn't matter if it's 0 + timeoutId = setTimeout(function () { + that.onSearch(event); + }, that.options.searchTimeOut); + }); + } + } + }; + + BootstrapTable.prototype.onSearch = function (event) { + var text = $.trim($(event.currentTarget).val()); + + // trim search input + if (this.options.trimOnSearch && $(event.currentTarget).val() !== text) { + $(event.currentTarget).val(text); + } + + if (text === this.searchText) { + return; + } + this.searchText = text; + this.options.searchText = text; + + this.options.pageNumber = 1; + this.initSearch(); + this.updatePagination(); + this.trigger('search', text); + }; + + BootstrapTable.prototype.initSearch = function () { + var that = this; + + if (this.options.sidePagination !== 'server') { + if (this.options.customSearch !== $.noop) { + this.options.customSearch.apply(this, [this.searchText]); + return; + } + + var s = this.searchText && (this.options.escape ? + escapeHTML(this.searchText) : this.searchText).toLowerCase(); + var f = $.isEmptyObject(this.filterColumns) ? null : this.filterColumns; + + // Check filter + this.data = f ? $.grep(this.options.data, function (item, i) { + for (var key in f) { + if ($.isArray(f[key]) && $.inArray(item[key], f[key]) === -1 || + !$.isArray(f[key]) && item[key] !== f[key]) { + return false; + } + } + return true; + }) : this.options.data; + + this.data = s ? $.grep(this.data, function (item, i) { + for (var j = 0; j < that.header.fields.length; j++) { + + if (!that.header.searchables[j]) { + continue; + } + + var key = $.isNumeric(that.header.fields[j]) ? parseInt(that.header.fields[j], 10) : that.header.fields[j]; + var column = that.columns[getFieldIndex(that.columns, key)]; + var value; + + if (typeof key === 'string') { + value = item; + var props = key.split('.'); + for (var prop_index = 0; prop_index < props.length; prop_index++) { + value = value[props[prop_index]]; + } + + // Fix #142: respect searchForamtter boolean + if (column && column.searchFormatter) { + value = calculateObjectValue(column, + that.header.formatters[j], [value, item, i], value); + } + } else { + value = item[key]; + } + + if (typeof value === 'string' || typeof value === 'number') { + if (that.options.strictSearch) { + if ((value + '').toLowerCase() === s) { + return true; + } + } else { + if ((value + '').toLowerCase().indexOf(s) !== -1) { + return true; + } + } + } + } + return false; + }) : this.data; + } + }; + + BootstrapTable.prototype.initPagination = function () { + if (!this.options.pagination) { + this.$pagination.hide(); + return; + } else { + this.$pagination.show(); + } + + var that = this, + html = [], + $allSelected = false, + i, from, to, + $pageList, + $first, $pre, + $next, $last, + $number, + data = this.getData(), + pageList = this.options.pageList; + + if (this.options.sidePagination !== 'server') { + this.options.totalRows = data.length; + } + + this.totalPages = 0; + if (this.options.totalRows) { + if (this.options.pageSize === this.options.formatAllRows()) { + this.options.pageSize = this.options.totalRows; + $allSelected = true; + } else if (this.options.pageSize === this.options.totalRows) { + // Fix #667 Table with pagination, + // multiple pages and a search that matches to one page throws exception + var pageLst = typeof this.options.pageList === 'string' ? + this.options.pageList.replace('[', '').replace(']', '') + .replace(/ /g, '').toLowerCase().split(',') : this.options.pageList; + if ($.inArray(this.options.formatAllRows().toLowerCase(), pageLst) > -1) { + $allSelected = true; + } + } + + this.totalPages = ~~((this.options.totalRows - 1) / this.options.pageSize) + 1; + + this.options.totalPages = this.totalPages; + } + if (this.totalPages > 0 && this.options.pageNumber > this.totalPages) { + this.options.pageNumber = this.totalPages; + } + + this.pageFrom = (this.options.pageNumber - 1) * this.options.pageSize + 1; + this.pageTo = this.options.pageNumber * this.options.pageSize; + if (this.pageTo > this.options.totalRows) { + this.pageTo = this.options.totalRows; + } + + html.push( + '
', + '', + this.options.onlyInfoPagination ? this.options.formatDetailPagination(this.options.totalRows) : + this.options.formatShowingRows(this.pageFrom, this.pageTo, this.options.totalRows), + ''); + + if (!this.options.onlyInfoPagination) { + html.push(''); + + var pageNumber = [ + sprintf('', + this.options.paginationVAlign === 'top' || this.options.paginationVAlign === 'both' ? + 'dropdown' : 'dropup'), + '', + ''); + + html.push(this.options.formatRecordsPerPage(pageNumber.join(''))); + html.push(''); + + html.push('
', + ''); + } + this.$pagination.html(html.join('')); + + if (!this.options.onlyInfoPagination) { + $pageList = this.$pagination.find('.page-list a'); + $first = this.$pagination.find('.page-first'); + $pre = this.$pagination.find('.page-pre'); + $next = this.$pagination.find('.page-next'); + $last = this.$pagination.find('.page-last'); + $number = this.$pagination.find('.page-number'); + + if (this.options.smartDisplay) { + if (this.totalPages <= 1) { + this.$pagination.find('div.pagination').hide(); + } + if (pageList.length < 2 || this.options.totalRows <= pageList[0]) { + this.$pagination.find('span.page-list').hide(); + } + + // when data is empty, hide the pagination + this.$pagination[this.getData().length ? 'show' : 'hide'](); + } + + if (!this.options.paginationLoop) { + if (this.options.pageNumber === 1) { + $pre.addClass('disabled'); + } + if (this.options.pageNumber === this.totalPages) { + $next.addClass('disabled'); + } + } + + if ($allSelected) { + this.options.pageSize = this.options.formatAllRows(); + } + $pageList.off('click').on('click', $.proxy(this.onPageListChange, this)); + $first.off('click').on('click', $.proxy(this.onPageFirst, this)); + $pre.off('click').on('click', $.proxy(this.onPagePre, this)); + $next.off('click').on('click', $.proxy(this.onPageNext, this)); + $last.off('click').on('click', $.proxy(this.onPageLast, this)); + $number.off('click').on('click', $.proxy(this.onPageNumber, this)); + } + }; + + BootstrapTable.prototype.updatePagination = function (event) { + // Fix #171: IE disabled button can be clicked bug. + if (event && $(event.currentTarget).hasClass('disabled')) { + return; + } + + if (!this.options.maintainSelected) { + this.resetRows(); + } + + this.initPagination(); + if (this.options.sidePagination === 'server') { + this.initServer(); + } else { + this.initBody(); + } + + this.trigger('page-change', this.options.pageNumber, this.options.pageSize); + }; + + BootstrapTable.prototype.onPageListChange = function (event) { + var $this = $(event.currentTarget); + + $this.parent().addClass('active').siblings().removeClass('active'); + this.options.pageSize = $this.text().toUpperCase() === this.options.formatAllRows().toUpperCase() ? + this.options.formatAllRows() : +$this.text(); + this.$toolbar.find('.page-size').text(this.options.pageSize); + + this.updatePagination(event); + return false; + }; + + BootstrapTable.prototype.onPageFirst = function (event) { + this.options.pageNumber = 1; + this.updatePagination(event); + return false; + }; + + BootstrapTable.prototype.onPagePre = function (event) { + if ((this.options.pageNumber - 1) === 0) { + this.options.pageNumber = this.options.totalPages; + } else { + this.options.pageNumber--; + } + this.updatePagination(event); + return false; + }; + + BootstrapTable.prototype.onPageNext = function (event) { + if ((this.options.pageNumber + 1) > this.options.totalPages) { + this.options.pageNumber = 1; + } else { + this.options.pageNumber++; + } + this.updatePagination(event); + return false; + }; + + BootstrapTable.prototype.onPageLast = function (event) { + this.options.pageNumber = this.totalPages; + this.updatePagination(event); + return false; + }; + + BootstrapTable.prototype.onPageNumber = function (event) { + if (this.options.pageNumber === +$(event.currentTarget).text()) { + return; + } + this.options.pageNumber = +$(event.currentTarget).text(); + this.updatePagination(event); + return false; + }; + + BootstrapTable.prototype.initRow = function(item, i, data, parentDom) { + var that=this, + key, + html = [], + style = {}, + csses = [], + data_ = '', + attributes = {}, + htmlAttributes = []; + + if ($.inArray(item, this.hiddenRows) > -1) { + return; + } + + style = calculateObjectValue(this.options, this.options.rowStyle, [item, i], style); + + if (style && style.css) { + for (key in style.css) { + csses.push(key + ': ' + style.css[key]); + } + } + + attributes = calculateObjectValue(this.options, + this.options.rowAttributes, [item, i], attributes); + + if (attributes) { + for (key in attributes) { + htmlAttributes.push(sprintf('%s="%s"', key, escapeHTML(attributes[key]))); + } + } + + if (item._data && !$.isEmptyObject(item._data)) { + $.each(item._data, function(k, v) { + // ignore data-index + if (k === 'index') { + return; + } + data_ += sprintf(' data-%s="%s"', k, v); + }); + } + + html.push('' + ); + + if (this.options.cardView) { + html.push(sprintf('
', this.header.fields.length)); + } + + if (!this.options.cardView && this.options.detailView) { + html.push('', + '', + sprintf('', this.options.iconsPrefix, this.options.icons.detailOpen), + '', + ''); + } + + $.each(this.header.fields, function(j, field) { + var text = '', + value_ = getItemField(item, field, that.options.escape), + value = '', + type = '', + cellStyle = {}, + id_ = '', + class_ = that.header.classes[j], + data_ = '', + rowspan_ = '', + colspan_ = '', + title_ = '', + column = that.columns[j]; + + if (that.fromHtml && typeof value_ === 'undefined') { + return; + } + + if (!column.visible) { + return; + } + + if (that.options.cardView && (!column.cardVisible)) { + return; + } + + if (column.escape) { + value_ = escapeHTML(value_); + } + + style = sprintf('style="%s"', csses.concat(that.header.styles[j]).join('; ')); + + // handle td's id and class + if (item['_' + field + '_id']) { + id_ = sprintf(' id="%s"', item['_' + field + '_id']); + } + if (item['_' + field + '_class']) { + class_ = sprintf(' class="%s"', item['_' + field + '_class']); + } + if (item['_' + field + '_rowspan']) { + rowspan_ = sprintf(' rowspan="%s"', item['_' + field + '_rowspan']); + } + if (item['_' + field + '_colspan']) { + colspan_ = sprintf(' colspan="%s"', item['_' + field + '_colspan']); + } + if (item['_' + field + '_title']) { + title_ = sprintf(' title="%s"', item['_' + field + '_title']); + } + cellStyle = calculateObjectValue(that.header, + that.header.cellStyles[j], [value_, item, i, field], cellStyle); + if (cellStyle.classes) { + class_ = sprintf(' class="%s"', cellStyle.classes); + } + if (cellStyle.css) { + var csses_ = []; + for (var key in cellStyle.css) { + csses_.push(key + ': ' + cellStyle.css[key]); + } + style = sprintf('style="%s"', csses_.concat(that.header.styles[j]).join('; ')); + } + + value = calculateObjectValue(column, + that.header.formatters[j], [value_, item, i], value_); + + if (item['_' + field + '_data'] && !$.isEmptyObject(item['_' + field + '_data'])) { + $.each(item['_' + field + '_data'], function(k, v) { + // ignore data-index + if (k === 'index') { + return; + } + data_ += sprintf(' data-%s="%s"', k, v); + }); + } + + if (column.checkbox || column.radio) { + type = column.checkbox ? 'checkbox' : type; + type = column.radio ? 'radio' : type; + + text = [sprintf(that.options.cardView ? + '
' : '', column['class'] || ''), + '', + that.header.formatters[j] && typeof value === 'string' ? value : '', + that.options.cardView ? '
' : '' + ].join(''); + + item[that.header.stateField] = value === true || (value && value.checked); + } else { + value = typeof value === 'undefined' || value === null ? + that.options.undefinedText : value; + + text = that.options.cardView ? ['
', + that.options.showHeader ? sprintf('%s', style, + getPropertyFromOther(that.columns, 'field', 'title', field)) : '', + sprintf('%s', value), + '
' + ].join('') : [sprintf('', + id_, class_, style, data_, rowspan_, colspan_, title_), + value, + '' + ].join(''); + + // Hide empty data on Card view when smartDisplay is set to true. + if (that.options.cardView && that.options.smartDisplay && value === '') { + // Should set a placeholder for event binding correct fieldIndex + text = '
'; + } + } + + html.push(text); + }); + + if (this.options.cardView) { + html.push('
'); + } + html.push(''); + + return html.join(' '); + }; + + BootstrapTable.prototype.initBody = function (fixedScroll) { + var that = this, + html = [], + data = this.getData(); + + this.trigger('pre-body', data); + + this.$body = this.$el.find('>tbody'); + if (!this.$body.length) { + this.$body = $('').appendTo(this.$el); + } + + //Fix #389 Bootstrap-table-flatJSON is not working + + if (!this.options.pagination || this.options.sidePagination === 'server') { + this.pageFrom = 1; + this.pageTo = data.length; + } + + var trFragments = $(document.createDocumentFragment()); + var hasTr; + + for (var i = this.pageFrom - 1; i < this.pageTo; i++) { + var item = data[i]; + var tr = this.initRow(item, i, data, trFragments); + hasTr = hasTr || !!tr; + if (tr&&tr!==true) { + trFragments.append(tr); + } + } + + // show no records + if (!hasTr) { + trFragments.append('' + + sprintf('%s', + this.$header.find('th').length, + this.options.formatNoMatches()) + + ''); + } + + this.$body.html(trFragments); + + if (!fixedScroll) { + this.scrollTo(0); + } + + // click to select by column + this.$body.find('> tr[data-index] > td').off('click dblclick').on('click dblclick', function (e) { + var $td = $(this), + $tr = $td.parent(), + item = that.data[$tr.data('index')], + index = $td[0].cellIndex, + fields = that.getVisibleFields(), + field = fields[that.options.detailView && !that.options.cardView ? index - 1 : index], + column = that.columns[getFieldIndex(that.columns, field)], + value = getItemField(item, field, that.options.escape); + + if ($td.find('.detail-icon').length) { + return; + } + + that.trigger(e.type === 'click' ? 'click-cell' : 'dbl-click-cell', field, value, item, $td); + that.trigger(e.type === 'click' ? 'click-row' : 'dbl-click-row', item, $tr, field); + + // if click to select - then trigger the checkbox/radio click + if (e.type === 'click' && that.options.clickToSelect && column.clickToSelect) { + var $selectItem = $tr.find(sprintf('[name="%s"]', that.options.selectItemName)); + if ($selectItem.length) { + $selectItem[0].click(); // #144: .trigger('click') bug + } + } + }); + + this.$body.find('> tr[data-index] > td > .detail-icon').off('click').on('click', function () { + var $this = $(this), + $tr = $this.parent().parent(), + index = $tr.data('index'), + row = data[index]; // Fix #980 Detail view, when searching, returns wrong row + + // remove and update + if ($tr.next().is('tr.detail-view')) { + $this.find('i').attr('class', sprintf('%s %s', that.options.iconsPrefix, that.options.icons.detailOpen)); + that.trigger('collapse-row', index, row); + $tr.next().remove(); + } else { + $this.find('i').attr('class', sprintf('%s %s', that.options.iconsPrefix, that.options.icons.detailClose)); + $tr.after(sprintf('', $tr.find('td').length)); + var $element = $tr.next().find('td'); + var content = calculateObjectValue(that.options, that.options.detailFormatter, [index, row, $element], ''); + if($element.length === 1) { + $element.append(content); + } + that.trigger('expand-row', index, row, $element); + } + that.resetView(); + return false; + }); + + this.$selectItem = this.$body.find(sprintf('[name="%s"]', this.options.selectItemName)); + this.$selectItem.off('click').on('click', function (event) { + event.stopImmediatePropagation(); + + var $this = $(this), + checked = $this.prop('checked'), + row = that.data[$this.data('index')]; + + if (that.options.maintainSelected && $(this).is(':radio')) { + $.each(that.options.data, function (i, row) { + row[that.header.stateField] = false; + }); + } + + row[that.header.stateField] = checked; + + if (that.options.singleSelect) { + that.$selectItem.not(this).each(function () { + that.data[$(this).data('index')][that.header.stateField] = false; + }); + that.$selectItem.filter(':checked').not(this).prop('checked', false); + } + + that.updateSelected(); + that.trigger(checked ? 'check' : 'uncheck', row, $this); + }); + + $.each(this.header.events, function (i, events) { + if (!events) { + return; + } + // fix bug, if events is defined with namespace + if (typeof events === 'string') { + events = calculateObjectValue(null, events); + } + + var field = that.header.fields[i], + fieldIndex = $.inArray(field, that.getVisibleFields()); + + if (that.options.detailView && !that.options.cardView) { + fieldIndex += 1; + } + + for (var key in events) { + that.$body.find('>tr:not(.no-records-found)').each(function () { + var $tr = $(this), + $td = $tr.find(that.options.cardView ? '.card-view' : 'td').eq(fieldIndex), + index = key.indexOf(' '), + name = key.substring(0, index), + el = key.substring(index + 1), + func = events[key]; + + $td.find(el).off(name).on(name, function (e) { + var index = $tr.data('index'), + row = that.data[index], + value = row[field]; + + func.apply(this, [e, value, row, index]); + }); + }); + } + }); + + this.updateSelected(); + this.resetView(); + + this.trigger('post-body', data); + }; + + BootstrapTable.prototype.initServer = function (silent, query, url) { + var that = this, + data = {}, + params = { + searchText: this.searchText, + sortName: this.options.sortName, + sortOrder: this.options.sortOrder + }, + request; + + if (this.options.pagination) { + params.pageSize = this.options.pageSize === this.options.formatAllRows() ? + this.options.totalRows : this.options.pageSize; + params.pageNumber = this.options.pageNumber; + } + + if (!(url || this.options.url) && !this.options.ajax) { + return; + } + + if (this.options.queryParamsType === 'limit') { + params = { + search: params.searchText, + sort: params.sortName, + order: params.sortOrder + }; + + if (this.options.pagination) { + params.offset = this.options.pageSize === this.options.formatAllRows() ? + 0 : this.options.pageSize * (this.options.pageNumber - 1); + params.limit = this.options.pageSize === this.options.formatAllRows() ? + this.options.totalRows : this.options.pageSize; + } + } + + if (!($.isEmptyObject(this.filterColumnsPartial))) { + params.filter = JSON.stringify(this.filterColumnsPartial, null); + } + + data = calculateObjectValue(this.options, this.options.queryParams, [params], data); + + $.extend(data, query || {}); + + // false to stop request + if (data === false) { + return; + } + + if (!silent) { + this.$tableLoading.show(); + } + request = $.extend({}, calculateObjectValue(null, this.options.ajaxOptions), { + type: this.options.method, + url: url || this.options.url, + data: this.options.contentType === 'application/json' && this.options.method === 'post' ? + JSON.stringify(data) : data, + cache: this.options.cache, + contentType: this.options.contentType, + dataType: this.options.dataType, + success: function (res) { + res = calculateObjectValue(that.options, that.options.responseHandler, [res], res); + + that.load(res); + that.trigger('load-success', res); + if (!silent) that.$tableLoading.hide(); + }, + error: function (res) { + that.trigger('load-error', res.status, res); + if (!silent) that.$tableLoading.hide(); + } + }); + + if (this.options.ajax) { + calculateObjectValue(this, this.options.ajax, [request], null); + } else { + if (this._xhr && this._xhr.readyState !== 4) { + this._xhr.abort(); + } + this._xhr = $.ajax(request); + } + }; + + BootstrapTable.prototype.initSearchText = function () { + if (this.options.search) { + if (this.options.searchText !== '') { + var $search = this.$toolbar.find('.search input'); + $search.val(this.options.searchText); + this.onSearch({currentTarget: $search}); + } + } + }; + + BootstrapTable.prototype.getCaret = function () { + var that = this; + + $.each(this.$header.find('th'), function (i, th) { + $(th).find('.sortable').removeClass('desc asc').addClass($(th).data('field') === that.options.sortName ? that.options.sortOrder : 'both'); + }); + }; + + BootstrapTable.prototype.updateSelected = function () { + var checkAll = this.$selectItem.filter(':enabled').length && + this.$selectItem.filter(':enabled').length === + this.$selectItem.filter(':enabled').filter(':checked').length; + + this.$selectAll.add(this.$selectAll_).prop('checked', checkAll); + + this.$selectItem.each(function () { + $(this).closest('tr')[$(this).prop('checked') ? 'addClass' : 'removeClass']('selected'); + }); + }; + + BootstrapTable.prototype.updateRows = function () { + var that = this; + + this.$selectItem.each(function () { + that.data[$(this).data('index')][that.header.stateField] = $(this).prop('checked'); + }); + }; + + BootstrapTable.prototype.resetRows = function () { + var that = this; + + $.each(this.data, function (i, row) { + that.$selectAll.prop('checked', false); + that.$selectItem.prop('checked', false); + if (that.header.stateField) { + row[that.header.stateField] = false; + } + }); + this.initHiddenRows(); + }; + + BootstrapTable.prototype.trigger = function (name) { + var args = Array.prototype.slice.call(arguments, 1); + + name += '.bs.table'; + this.options[BootstrapTable.EVENTS[name]].apply(this.options, args); + this.$el.trigger($.Event(name), args); + + this.options.onAll(name, args); + this.$el.trigger($.Event('all.bs.table'), [name, args]); + }; + + BootstrapTable.prototype.resetHeader = function () { + // fix #61: the hidden table reset header bug. + // fix bug: get $el.css('width') error sometime (height = 500) + clearTimeout(this.timeoutId_); + this.timeoutId_ = setTimeout($.proxy(this.fitHeader, this), this.$el.is(':hidden') ? 100 : 0); + }; + + BootstrapTable.prototype.fitHeader = function () { + var that = this, + fixedBody, + scrollWidth, + focused, + focusedTemp; + + if (that.$el.is(':hidden')) { + that.timeoutId_ = setTimeout($.proxy(that.fitHeader, that), 100); + return; + } + fixedBody = this.$tableBody.get(0); + + scrollWidth = fixedBody.scrollWidth > fixedBody.clientWidth && + fixedBody.scrollHeight > fixedBody.clientHeight + this.$header.outerHeight() ? + getScrollBarWidth() : 0; + + this.$el.css('margin-top', -this.$header.outerHeight()); + + focused = $(':focus'); + if (focused.length > 0) { + var $th = focused.parents('th'); + if ($th.length > 0) { + var dataField = $th.attr('data-field'); + if (dataField !== undefined) { + var $headerTh = this.$header.find("[data-field='" + dataField + "']"); + if ($headerTh.length > 0) { + $headerTh.find(":input").addClass("focus-temp"); + } + } + } + } + + this.$header_ = this.$header.clone(true, true); + this.$selectAll_ = this.$header_.find('[name="btSelectAll"]'); + this.$tableHeader.css({ + 'margin-right': scrollWidth + }).find('table').css('width', this.$el.outerWidth()) + .html('').attr('class', this.$el.attr('class')) + .append(this.$header_); + + + focusedTemp = $('.focus-temp:visible:eq(0)'); + if (focusedTemp.length > 0) { + focusedTemp.focus(); + this.$header.find('.focus-temp').removeClass('focus-temp'); + } + + // fix bug: $.data() is not working as expected after $.append() + this.$header.find('th[data-field]').each(function (i) { + that.$header_.find(sprintf('th[data-field="%s"]', $(this).data('field'))).data($(this).data()); + }); + + var visibleFields = this.getVisibleFields(), + $ths = this.$header_.find('th'); + + this.$body.find('>tr:first-child:not(.no-records-found) > *').each(function (i) { + var $this = $(this), + index = i; + + if (that.options.detailView && !that.options.cardView) { + if (i === 0) { + that.$header_.find('th.detail').find('.fht-cell').width($this.innerWidth()); + } + index = i - 1; + } + + var $th = that.$header_.find(sprintf('th[data-field="%s"]', visibleFields[index])); + if ($th.length > 1) { + $th = $($ths[$this[0].cellIndex]); + } + + $th.find('.fht-cell').width($this.innerWidth()); + }); + // horizontal scroll event + // TODO: it's probably better improving the layout than binding to scroll event + this.$tableBody.off('scroll').on('scroll', function () { + that.$tableHeader.scrollLeft($(this).scrollLeft()); + + if (that.options.showFooter && !that.options.cardView) { + that.$tableFooter.scrollLeft($(this).scrollLeft()); + } + }); + that.trigger('post-header'); + }; + + BootstrapTable.prototype.resetFooter = function () { + var that = this, + data = that.getData(), + html = []; + + if (!this.options.showFooter || this.options.cardView) { //do nothing + return; + } + + if (!this.options.cardView && this.options.detailView) { + html.push('
 
'); + } + + $.each(this.columns, function (i, column) { + var key, + falign = '', // footer align style + valign = '', + csses = [], + style = {}, + class_ = sprintf(' class="%s"', column['class']); + + if (!column.visible) { + return; + } + + if (that.options.cardView && (!column.cardVisible)) { + return; + } + + falign = sprintf('text-align: %s; ', column.falign ? column.falign : column.align); + valign = sprintf('vertical-align: %s; ', column.valign); + + style = calculateObjectValue(null, that.options.footerStyle); + + if (style && style.css) { + for (key in style.css) { + csses.push(key + ': ' + style.css[key]); + } + } + + html.push(''); + html.push('
'); + + html.push(calculateObjectValue(column, column.footerFormatter, [data], ' ') || ' '); + + html.push('
'); + html.push('
'); + html.push(''); + html.push(''); + }); + + this.$tableFooter.find('tr').html(html.join('')); + this.$tableFooter.show(); + clearTimeout(this.timeoutFooter_); + this.timeoutFooter_ = setTimeout($.proxy(this.fitFooter, this), + this.$el.is(':hidden') ? 100 : 0); + }; + + BootstrapTable.prototype.fitFooter = function () { + var that = this, + $footerTd, + elWidth, + scrollWidth; + + clearTimeout(this.timeoutFooter_); + if (this.$el.is(':hidden')) { + this.timeoutFooter_ = setTimeout($.proxy(this.fitFooter, this), 100); + return; + } + + elWidth = this.$el.css('width'); + scrollWidth = elWidth > this.$tableBody.width() ? getScrollBarWidth() : 0; + + this.$tableFooter.css({ + 'margin-right': scrollWidth + }).find('table').css('width', elWidth) + .attr('class', this.$el.attr('class')); + + $footerTd = this.$tableFooter.find('td'); + + this.$body.find('>tr:first-child:not(.no-records-found) > *').each(function (i) { + var $this = $(this); + + $footerTd.eq(i).find('.fht-cell').width($this.innerWidth()); + }); + }; + + BootstrapTable.prototype.toggleColumn = function (index, checked, needUpdate) { + if (index === -1) { + return; + } + this.columns[index].visible = checked; + this.initHeader(); + this.initSearch(); + this.initPagination(); + this.initBody(); + + if (this.options.showColumns) { + var $items = this.$toolbar.find('.keep-open input').prop('disabled', false); + + if (needUpdate) { + $items.filter(sprintf('[value="%s"]', index)).prop('checked', checked); + } + + if ($items.filter(':checked').length <= this.options.minimumCountColumns) { + $items.filter(':checked').prop('disabled', true); + } + } + }; + + BootstrapTable.prototype.getVisibleFields = function () { + var that = this, + visibleFields = []; + + $.each(this.header.fields, function (j, field) { + var column = that.columns[getFieldIndex(that.columns, field)]; + + if (!column.visible) { + return; + } + visibleFields.push(field); + }); + return visibleFields; + }; + + // PUBLIC FUNCTION DEFINITION + // ======================= + + BootstrapTable.prototype.resetView = function (params) { + var padding = 0; + + if (params && params.height) { + this.options.height = params.height; + } + + this.$selectAll.prop('checked', this.$selectItem.length > 0 && + this.$selectItem.length === this.$selectItem.filter(':checked').length); + + if (this.options.height) { + var toolbarHeight = this.$toolbar.outerHeight(true), + paginationHeight = this.$pagination.outerHeight(true), + height = this.options.height - toolbarHeight - paginationHeight; + + this.$tableContainer.css('height', height + 'px'); + } + + if (this.options.cardView) { + // remove the element css + this.$el.css('margin-top', '0'); + this.$tableContainer.css('padding-bottom', '0'); + this.$tableFooter.hide(); + return; + } + + if (this.options.showHeader && this.options.height) { + this.$tableHeader.show(); + this.resetHeader(); + padding += this.$header.outerHeight(); + } else { + this.$tableHeader.hide(); + this.trigger('post-header'); + } + + if (this.options.showFooter) { + this.resetFooter(); + if (this.options.height) { + padding += this.$tableFooter.outerHeight() + 1; + } + } + + // Assign the correct sortable arrow + this.getCaret(); + this.$tableContainer.css('padding-bottom', padding + 'px'); + this.trigger('reset-view'); + }; + + BootstrapTable.prototype.getData = function (useCurrentPage) { + return (this.searchText || !$.isEmptyObject(this.filterColumns) || !$.isEmptyObject(this.filterColumnsPartial)) ? + (useCurrentPage ? this.data.slice(this.pageFrom - 1, this.pageTo) : this.data) : + (useCurrentPage ? this.options.data.slice(this.pageFrom - 1, this.pageTo) : this.options.data); + }; + + BootstrapTable.prototype.load = function (data) { + var fixedScroll = false; + + // #431: support pagination + if (this.options.sidePagination === 'server') { + this.options.totalRows = data[this.options.totalField]; + fixedScroll = data.fixedScroll; + data = data[this.options.dataField]; + } else if (!$.isArray(data)) { // support fixedScroll + fixedScroll = data.fixedScroll; + data = data.data; + } + + this.initData(data); + this.initSearch(); + this.initPagination(); + this.initBody(fixedScroll); + }; + + BootstrapTable.prototype.append = function (data) { + this.initData(data, 'append'); + this.initSearch(); + this.initPagination(); + this.initSort(); + this.initBody(true); + }; + + BootstrapTable.prototype.prepend = function (data) { + this.initData(data, 'prepend'); + this.initSearch(); + this.initPagination(); + this.initSort(); + this.initBody(true); + }; + + BootstrapTable.prototype.remove = function (params) { + var len = this.options.data.length, + i, row; + + if (!params.hasOwnProperty('field') || !params.hasOwnProperty('values')) { + return; + } + + for (i = len - 1; i >= 0; i--) { + row = this.options.data[i]; + + if (!row.hasOwnProperty(params.field)) { + continue; + } + if ($.inArray(row[params.field], params.values) !== -1) { + this.options.data.splice(i, 1); + if (this.options.sidePagination === 'server') { + this.options.totalRows -= 1; + } + } + } + + if (len === this.options.data.length) { + return; + } + + this.initSearch(); + this.initPagination(); + this.initSort(); + this.initBody(true); + }; + + BootstrapTable.prototype.removeAll = function () { + if (this.options.data.length > 0) { + this.options.data.splice(0, this.options.data.length); + this.initSearch(); + this.initPagination(); + this.initBody(true); + } + }; + + BootstrapTable.prototype.getRowByUniqueId = function (id) { + var uniqueId = this.options.uniqueId, + len = this.options.data.length, + dataRow = null, + i, row, rowUniqueId; + + for (i = len - 1; i >= 0; i--) { + row = this.options.data[i]; + + if (row.hasOwnProperty(uniqueId)) { // uniqueId is a column + rowUniqueId = row[uniqueId]; + } else if(row._data.hasOwnProperty(uniqueId)) { // uniqueId is a row data property + rowUniqueId = row._data[uniqueId]; + } else { + continue; + } + + if (typeof rowUniqueId === 'string') { + id = id.toString(); + } else if (typeof rowUniqueId === 'number') { + if ((Number(rowUniqueId) === rowUniqueId) && (rowUniqueId % 1 === 0)) { + id = parseInt(id); + } else if ((rowUniqueId === Number(rowUniqueId)) && (rowUniqueId !== 0)) { + id = parseFloat(id); + } + } + + if (rowUniqueId === id) { + dataRow = row; + break; + } + } + + return dataRow; + }; + + BootstrapTable.prototype.removeByUniqueId = function (id) { + var len = this.options.data.length, + row = this.getRowByUniqueId(id); + + if (row) { + this.options.data.splice(this.options.data.indexOf(row), 1); + } + + if (len === this.options.data.length) { + return; + } + + this.initSearch(); + this.initPagination(); + this.initBody(true); + }; + + BootstrapTable.prototype.updateByUniqueId = function (params) { + var that = this; + var allParams = $.isArray(params) ? params : [ params ]; + + $.each(allParams, function(i, params) { + var rowId; + + if (!params.hasOwnProperty('id') || !params.hasOwnProperty('row')) { + return; + } + + rowId = $.inArray(that.getRowByUniqueId(params.id), that.options.data); + + if (rowId === -1) { + return; + } + $.extend(that.options.data[rowId], params.row); + }); + + this.initSearch(); + this.initPagination(); + this.initSort(); + this.initBody(true); + }; + + BootstrapTable.prototype.insertRow = function (params) { + if (!params.hasOwnProperty('index') || !params.hasOwnProperty('row')) { + return; + } + this.data.splice(params.index, 0, params.row); + this.initSearch(); + this.initPagination(); + this.initSort(); + this.initBody(true); + }; + + BootstrapTable.prototype.updateRow = function (params) { + var that = this; + var allParams = $.isArray(params) ? params : [ params ]; + + $.each(allParams, function(i, params) { + if (!params.hasOwnProperty('index') || !params.hasOwnProperty('row')) { + return; + } + $.extend(that.options.data[params.index], params.row); + }); + + this.initSearch(); + this.initPagination(); + this.initSort(); + this.initBody(true); + }; + + BootstrapTable.prototype.initHiddenRows = function () { + this.hiddenRows = []; + }; + + BootstrapTable.prototype.showRow = function (params) { + this.toggleRow(params, true); + }; + + BootstrapTable.prototype.hideRow = function (params) { + this.toggleRow(params, false); + }; + + BootstrapTable.prototype.toggleRow = function (params, visible) { + var row, index; + + if (params.hasOwnProperty('index')) { + row = this.getData()[params.index]; + } else if (params.hasOwnProperty('uniqueId')) { + row = this.getRowByUniqueId(params.uniqueId); + } + + if (!row) { + return; + } + + index = $.inArray(row, this.hiddenRows); + + if (!visible && index === -1) { + this.hiddenRows.push(row); + } else if (visible && index > -1) { + this.hiddenRows.splice(index, 1); + } + this.initBody(true); + }; + + BootstrapTable.prototype.getHiddenRows = function (show) { + var that = this, + data = this.getData(), + rows = []; + + $.each(data, function (i, row) { + if ($.inArray(row, that.hiddenRows) > -1) { + rows.push(row); + } + }); + this.hiddenRows = rows; + return rows; + }; + + BootstrapTable.prototype.mergeCells = function (options) { + var row = options.index, + col = $.inArray(options.field, this.getVisibleFields()), + rowspan = options.rowspan || 1, + colspan = options.colspan || 1, + i, j, + $tr = this.$body.find('>tr'), + $td; + + if (this.options.detailView && !this.options.cardView) { + col += 1; + } + + $td = $tr.eq(row).find('>td').eq(col); + + if (row < 0 || col < 0 || row >= this.data.length) { + return; + } + + for (i = row; i < row + rowspan; i++) { + for (j = col; j < col + colspan; j++) { + $tr.eq(i).find('>td').eq(j).hide(); + } + } + + $td.attr('rowspan', rowspan).attr('colspan', colspan).show(); + }; + + BootstrapTable.prototype.updateCell = function (params) { + if (!params.hasOwnProperty('index') || + !params.hasOwnProperty('field') || + !params.hasOwnProperty('value')) { + return; + } + this.data[params.index][params.field] = params.value; + + if (params.reinit === false) { + return; + } + this.initSort(); + this.initBody(true); + }; + + BootstrapTable.prototype.getOptions = function () { + return this.options; + }; + + BootstrapTable.prototype.getSelections = function () { + var that = this; + + return $.grep(this.options.data, function (row) { + // fix #2424: from html with checkbox + return row[that.header.stateField] === true; + }); + }; + + BootstrapTable.prototype.getAllSelections = function () { + var that = this; + + return $.grep(this.options.data, function (row) { + return row[that.header.stateField]; + }); + }; + + BootstrapTable.prototype.checkAll = function () { + this.checkAll_(true); + }; + + BootstrapTable.prototype.uncheckAll = function () { + this.checkAll_(false); + }; + + BootstrapTable.prototype.checkInvert = function () { + var that = this; + var rows = that.$selectItem.filter(':enabled'); + var checked = rows.filter(':checked'); + rows.each(function() { + $(this).prop('checked', !$(this).prop('checked')); + }); + that.updateRows(); + that.updateSelected(); + that.trigger('uncheck-some', checked); + checked = that.getSelections(); + that.trigger('check-some', checked); + }; + + BootstrapTable.prototype.checkAll_ = function (checked) { + var rows; + if (!checked) { + rows = this.getSelections(); + } + this.$selectAll.add(this.$selectAll_).prop('checked', checked); + this.$selectItem.filter(':enabled').prop('checked', checked); + this.updateRows(); + if (checked) { + rows = this.getSelections(); + } + this.trigger(checked ? 'check-all' : 'uncheck-all', rows); + }; + + BootstrapTable.prototype.check = function (index) { + this.check_(true, index); + }; + + BootstrapTable.prototype.uncheck = function (index) { + this.check_(false, index); + }; + + BootstrapTable.prototype.check_ = function (checked, index) { + var $el = this.$selectItem.filter(sprintf('[data-index="%s"]', index)).prop('checked', checked); + this.data[index][this.header.stateField] = checked; + this.updateSelected(); + this.trigger(checked ? 'check' : 'uncheck', this.data[index], $el); + }; + + BootstrapTable.prototype.checkBy = function (obj) { + this.checkBy_(true, obj); + }; + + BootstrapTable.prototype.uncheckBy = function (obj) { + this.checkBy_(false, obj); + }; + + BootstrapTable.prototype.checkBy_ = function (checked, obj) { + if (!obj.hasOwnProperty('field') || !obj.hasOwnProperty('values')) { + return; + } + + var that = this, + rows = []; + $.each(this.options.data, function (index, row) { + if (!row.hasOwnProperty(obj.field)) { + return false; + } + if ($.inArray(row[obj.field], obj.values) !== -1) { + var $el = that.$selectItem.filter(':enabled') + .filter(sprintf('[data-index="%s"]', index)).prop('checked', checked); + row[that.header.stateField] = checked; + rows.push(row); + that.trigger(checked ? 'check' : 'uncheck', row, $el); + } + }); + this.updateSelected(); + this.trigger(checked ? 'check-some' : 'uncheck-some', rows); + }; + + BootstrapTable.prototype.destroy = function () { + this.$el.insertBefore(this.$container); + $(this.options.toolbar).insertBefore(this.$el); + this.$container.next().remove(); + this.$container.remove(); + this.$el.html(this.$el_.html()) + .css('margin-top', '0') + .attr('class', this.$el_.attr('class') || ''); // reset the class + }; + + BootstrapTable.prototype.showLoading = function () { + this.$tableLoading.show(); + }; + + BootstrapTable.prototype.hideLoading = function () { + this.$tableLoading.hide(); + }; + + BootstrapTable.prototype.togglePagination = function () { + this.options.pagination = !this.options.pagination; + var button = this.$toolbar.find('button[name="paginationSwitch"] i'); + if (this.options.pagination) { + button.attr("class", this.options.iconsPrefix + " " + this.options.icons.paginationSwitchDown); + } else { + button.attr("class", this.options.iconsPrefix + " " + this.options.icons.paginationSwitchUp); + } + this.updatePagination(); + }; + + BootstrapTable.prototype.refresh = function (params) { + if (params && params.url) { + this.options.url = params.url; + } + if (params && params.pageNumber) { + this.options.pageNumber = params.pageNumber; + } + if (params && params.pageSize) { + this.options.pageSize = params.pageSize; + } + this.initServer(params && params.silent, + params && params.query, params && params.url); + this.trigger('refresh', params); + }; + + BootstrapTable.prototype.resetWidth = function () { + if (this.options.showHeader && this.options.height) { + this.fitHeader(); + } + if (this.options.showFooter) { + this.fitFooter(); + } + }; + + BootstrapTable.prototype.showColumn = function (field) { + this.toggleColumn(getFieldIndex(this.columns, field), true, true); + }; + + BootstrapTable.prototype.hideColumn = function (field) { + this.toggleColumn(getFieldIndex(this.columns, field), false, true); + }; + + BootstrapTable.prototype.getHiddenColumns = function () { + return $.grep(this.columns, function (column) { + return !column.visible; + }); + }; + + BootstrapTable.prototype.getVisibleColumns = function () { + return $.grep(this.columns, function (column) { + return column.visible; + }); + }; + + BootstrapTable.prototype.toggleAllColumns = function (visible) { + $.each(this.columns, function (i, column) { + this.columns[i].visible = visible; + }); + + this.initHeader(); + this.initSearch(); + this.initPagination(); + this.initBody(); + if (this.options.showColumns) { + var $items = this.$toolbar.find('.keep-open input').prop('disabled', false); + + if ($items.filter(':checked').length <= this.options.minimumCountColumns) { + $items.filter(':checked').prop('disabled', true); + } + } + }; + + BootstrapTable.prototype.showAllColumns = function () { + this.toggleAllColumns(true); + }; + + BootstrapTable.prototype.hideAllColumns = function () { + this.toggleAllColumns(false); + }; + + BootstrapTable.prototype.filterBy = function (columns) { + this.filterColumns = $.isEmptyObject(columns) ? {} : columns; + this.options.pageNumber = 1; + this.initSearch(); + this.updatePagination(); + }; + + BootstrapTable.prototype.scrollTo = function (value) { + if (typeof value === 'string') { + value = value === 'bottom' ? this.$tableBody[0].scrollHeight : 0; + } + if (typeof value === 'number') { + this.$tableBody.scrollTop(value); + } + if (typeof value === 'undefined') { + return this.$tableBody.scrollTop(); + } + }; + + BootstrapTable.prototype.getScrollPosition = function () { + return this.scrollTo(); + }; + + BootstrapTable.prototype.selectPage = function (page) { + if (page > 0 && page <= this.options.totalPages) { + this.options.pageNumber = page; + this.updatePagination(); + } + }; + + BootstrapTable.prototype.prevPage = function () { + if (this.options.pageNumber > 1) { + this.options.pageNumber--; + this.updatePagination(); + } + }; + + BootstrapTable.prototype.nextPage = function () { + if (this.options.pageNumber < this.options.totalPages) { + this.options.pageNumber++; + this.updatePagination(); + } + }; + + BootstrapTable.prototype.toggleView = function () { + this.options.cardView = !this.options.cardView; + this.initHeader(); + // Fixed remove toolbar when click cardView button. + //that.initToolbar(); + this.initBody(); + this.trigger('toggle', this.options.cardView); + }; + + BootstrapTable.prototype.refreshOptions = function (options) { + //If the objects are equivalent then avoid the call of destroy / init methods + if (compareObjects(this.options, options, true)) { + return; + } + this.options = $.extend(this.options, options); + this.trigger('refresh-options', this.options); + this.destroy(); + this.init(); + }; + + BootstrapTable.prototype.resetSearch = function (text) { + var $search = this.$toolbar.find('.search input'); + $search.val(text || ''); + this.onSearch({currentTarget: $search}); + }; + + BootstrapTable.prototype.expandRow_ = function (expand, index) { + var $tr = this.$body.find(sprintf('> tr[data-index="%s"]', index)); + if ($tr.next().is('tr.detail-view') === (expand ? false : true)) { + $tr.find('> td > .detail-icon').click(); + } + }; + + BootstrapTable.prototype.expandRow = function (index) { + this.expandRow_(true, index); + }; + + BootstrapTable.prototype.collapseRow = function (index) { + this.expandRow_(false, index); + }; + + BootstrapTable.prototype.expandAllRows = function (isSubTable) { + if (isSubTable) { + var $tr = this.$body.find(sprintf('> tr[data-index="%s"]', 0)), + that = this, + detailIcon = null, + executeInterval = false, + idInterval = -1; + + if (!$tr.next().is('tr.detail-view')) { + $tr.find('> td > .detail-icon').click(); + executeInterval = true; + } else if (!$tr.next().next().is('tr.detail-view')) { + $tr.next().find(".detail-icon").click(); + executeInterval = true; + } + + if (executeInterval) { + try { + idInterval = setInterval(function () { + detailIcon = that.$body.find("tr.detail-view").last().find(".detail-icon"); + if (detailIcon.length > 0) { + detailIcon.click(); + } else { + clearInterval(idInterval); + } + }, 1); + } catch (ex) { + clearInterval(idInterval); + } + } + } else { + var trs = this.$body.children(); + for (var i = 0; i < trs.length; i++) { + this.expandRow_(true, $(trs[i]).data("index")); + } + } + }; + + BootstrapTable.prototype.collapseAllRows = function (isSubTable) { + if (isSubTable) { + this.expandRow_(false, 0); + } else { + var trs = this.$body.children(); + for (var i = 0; i < trs.length; i++) { + this.expandRow_(false, $(trs[i]).data("index")); + } + } + }; + + BootstrapTable.prototype.updateFormatText = function (name, text) { + if (this.options[sprintf('format%s', name)]) { + if (typeof text === 'string') { + this.options[sprintf('format%s', name)] = function () { + return text; + }; + } else if (typeof text === 'function') { + this.options[sprintf('format%s', name)] = text; + } + } + this.initToolbar(); + this.initPagination(); + this.initBody(); + }; + + // BOOTSTRAP TABLE PLUGIN DEFINITION + // ======================= + + var allowedMethods = [ + 'getOptions', + 'getSelections', 'getAllSelections', 'getData', + 'load', 'append', 'prepend', 'remove', 'removeAll', + 'insertRow', 'updateRow', 'updateCell', 'updateByUniqueId', 'removeByUniqueId', + 'getRowByUniqueId', 'showRow', 'hideRow', 'getHiddenRows', + 'mergeCells', + 'checkAll', 'uncheckAll', 'checkInvert', + 'check', 'uncheck', + 'checkBy', 'uncheckBy', + 'refresh', + 'resetView', + 'resetWidth', + 'destroy', + 'showLoading', 'hideLoading', + 'showColumn', 'hideColumn', 'getHiddenColumns', 'getVisibleColumns', + 'showAllColumns', 'hideAllColumns', + 'filterBy', + 'scrollTo', + 'getScrollPosition', + 'selectPage', 'prevPage', 'nextPage', + 'togglePagination', + 'toggleView', + 'refreshOptions', + 'resetSearch', + 'expandRow', 'collapseRow', 'expandAllRows', 'collapseAllRows', + 'updateFormatText' + ]; + + $.fn.bootstrapTable = function (option) { + var value, + args = Array.prototype.slice.call(arguments, 1); + + this.each(function () { + var $this = $(this), + data = $this.data('bootstrap.table'), + options = $.extend({}, BootstrapTable.DEFAULTS, $this.data(), + typeof option === 'object' && option); + + if (typeof option === 'string') { + if ($.inArray(option, allowedMethods) < 0) { + throw new Error("Unknown method: " + option); + } + + if (!data) { + return; + } + + value = data[option].apply(data, args); + + if (option === 'destroy') { + $this.removeData('bootstrap.table'); + } + } + + if (!data) { + $this.data('bootstrap.table', (data = new BootstrapTable(this, options))); + } + }); + + return typeof value === 'undefined' ? this : value; + }; + + $.fn.bootstrapTable.Constructor = BootstrapTable; + $.fn.bootstrapTable.defaults = BootstrapTable.DEFAULTS; + $.fn.bootstrapTable.columnDefaults = BootstrapTable.COLUMN_DEFAULTS; + $.fn.bootstrapTable.locales = BootstrapTable.LOCALES; + $.fn.bootstrapTable.methods = allowedMethods; + $.fn.bootstrapTable.utils = { + sprintf: sprintf, + getFieldIndex: getFieldIndex, + compareObjects: compareObjects, + calculateObjectValue: calculateObjectValue, + getItemField: getItemField, + objectKeys: objectKeys, + isIEBrowser: isIEBrowser + }; + + // BOOTSTRAP TABLE INIT + // ======================= + + $(function () { + $('[data-toggle="table"]').bootstrapTable(); + }); +})(jQuery); diff --git a/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/clear-password-modal.png b/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/clear-password-modal.png new file mode 100644 index 00000000..4ee17b28 Binary files /dev/null and b/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/clear-password-modal.png differ diff --git a/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/context-delete.png b/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/context-delete.png new file mode 100644 index 00000000..dbf64a31 Binary files /dev/null and b/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/context-delete.png differ diff --git a/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/context-menu.png b/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/context-menu.png new file mode 100644 index 00000000..d8887f75 Binary files /dev/null and b/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/context-menu.png differ diff --git a/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/context-update.png b/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/context-update.png new file mode 100644 index 00000000..dca7b952 Binary files /dev/null and b/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/context-update.png differ diff --git a/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/create-form.png b/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/create-form.png new file mode 100644 index 00000000..b032257e Binary files /dev/null and b/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/create-form.png differ diff --git a/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/create-success-modal.png b/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/create-success-modal.png new file mode 100644 index 00000000..05f966f6 Binary files /dev/null and b/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/create-success-modal.png differ diff --git a/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/credential-create.png b/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/credential-create.png new file mode 100644 index 00000000..2f5e9869 Binary files /dev/null and b/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/credential-create.png differ diff --git a/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/detail-view.png b/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/detail-view.png new file mode 100644 index 00000000..65952d8b Binary files /dev/null and b/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/detail-view.png differ diff --git a/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/filter.png b/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/filter.png new file mode 100644 index 00000000..bac937fe Binary files /dev/null and b/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/filter.png differ diff --git a/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/inline-update.png b/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/inline-update.png new file mode 100644 index 00000000..3b4fa94c Binary files /dev/null and b/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/inline-update.png differ diff --git a/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/multi-delete-confirm.png b/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/multi-delete-confirm.png new file mode 100644 index 00000000..9a667cb1 Binary files /dev/null and b/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/multi-delete-confirm.png differ diff --git a/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/multi-delete.png b/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/multi-delete.png new file mode 100644 index 00000000..11ef669c Binary files /dev/null and b/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/multi-delete.png differ diff --git a/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/show-password.png b/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/show-password.png new file mode 100644 index 00000000..0601064f Binary files /dev/null and b/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/show-password.png differ diff --git a/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/table.png b/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/table.png new file mode 100644 index 00000000..7a397eda Binary files /dev/null and b/deployment-apps/rest-storage-passwords-manager/appserver/static/img/credential_management-tour:enterprise/table.png differ diff --git a/deployment-apps/rest-storage-passwords-manager/appserver/static/password-crud.js b/deployment-apps/rest-storage-passwords-manager/appserver/static/password-crud.js new file mode 100644 index 00000000..240e4aac --- /dev/null +++ b/deployment-apps/rest-storage-passwords-manager/appserver/static/password-crud.js @@ -0,0 +1,1168 @@ +'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(); + +}); diff --git a/deployment-apps/rest-storage-passwords-manager/bin/README b/deployment-apps/rest-storage-passwords-manager/bin/README new file mode 100644 index 00000000..9a70db09 --- /dev/null +++ b/deployment-apps/rest-storage-passwords-manager/bin/README @@ -0,0 +1 @@ +This is where you put any scripts you want to add to this app. diff --git a/deployment-apps/rest-storage-passwords-manager/default/app.conf b/deployment-apps/rest-storage-passwords-manager/default/app.conf new file mode 100644 index 00000000..f4c82401 --- /dev/null +++ b/deployment-apps/rest-storage-passwords-manager/default/app.conf @@ -0,0 +1,20 @@ +# +# Splunk app configuration file +# + +[install] +is_configured = 0 +state_change_requires_restart = false + +[ui] +is_visible = 1 +label = REST storage/passwords Manager for Splunk + +[launcher] +author = Scott Haskell +description = An intuitive, full-featured, Javascript CRUD interface to the storage/passwords REST endpoint. +version = 1.0.8 + +[package] +id = rest-storage-passwords-manager +check_for_updates = 1 \ No newline at end of file diff --git a/deployment-apps/rest-storage-passwords-manager/default/data/ui/nav/default.xml b/deployment-apps/rest-storage-passwords-manager/default/data/ui/nav/default.xml new file mode 100644 index 00000000..9ce890a1 --- /dev/null +++ b/deployment-apps/rest-storage-passwords-manager/default/data/ui/nav/default.xml @@ -0,0 +1,4 @@ + diff --git a/deployment-apps/rest-storage-passwords-manager/default/data/ui/panels/credential-management.xml b/deployment-apps/rest-storage-passwords-manager/default/data/ui/panels/credential-management.xml new file mode 100644 index 00000000..d0f96ba0 --- /dev/null +++ b/deployment-apps/rest-storage-passwords-manager/default/data/ui/panels/credential-management.xml @@ -0,0 +1,8 @@ + + +
    +
    +
    +
    + + \ No newline at end of file diff --git a/deployment-apps/rest-storage-passwords-manager/default/data/ui/views/README b/deployment-apps/rest-storage-passwords-manager/default/data/ui/views/README new file mode 100644 index 00000000..6cf74f0b --- /dev/null +++ b/deployment-apps/rest-storage-passwords-manager/default/data/ui/views/README @@ -0,0 +1 @@ +Add all the views that your app needs in this directory diff --git a/deployment-apps/rest-storage-passwords-manager/default/data/ui/views/credential_management.xml b/deployment-apps/rest-storage-passwords-manager/default/data/ui/views/credential_management.xml new file mode 100644 index 00000000..b350a381 --- /dev/null +++ b/deployment-apps/rest-storage-passwords-manager/default/data/ui/views/credential_management.xml @@ -0,0 +1,13 @@ + + + + + +
    +
    +
    +
    + +
    +
    +
    \ No newline at end of file diff --git a/deployment-apps/rest-storage-passwords-manager/default/data/ui/views/help.xml b/deployment-apps/rest-storage-passwords-manager/default/data/ui/views/help.xml new file mode 100644 index 00000000..a5ef7188 --- /dev/null +++ b/deployment-apps/rest-storage-passwords-manager/default/data/ui/views/help.xml @@ -0,0 +1,47 @@ + + + +

    Splunk REST storage/passwords Manager for Splunk

    +

    About

    +

    The password manager app provides a Javascript CRUD interface to the Splunk storage/passwords REST endpoint.

    +

    Dependencies

    +

    To store passwords the user must have the admin_all_objects capability enabled within an assigned role. To read passwords a user must have list_storage_passwords capabilty enabled within an assigned role.

    +

    Usage

    +

    The Credential Management dashboard provides a CRUD interface to create, update and delete credentials to be used within Splunk apps.

    +

    Create Credential

    +

    Simply click the create button to reveal the credential creation form.

    +

    Alt text

    +

    Fill out the form specifying a username, password and optionally a realm. The realm can be used as a descriptor for the credential or left blank; e.g., prod or dev. The form will populate with sane defaults for owner, read users, write users, app scope and sharing. You can update them to whatever you like, including the target app context, before you hit create.

    +

    Alt text

    +

    Alt text

    +

    Once created, the dashboard will be refreshed automatically.

    +

    Alt text

    +

    Update Credential

    +

    Right click on a table entry to reveal a context menu that allows you to update the credential.

    +

    Alt text

    +

    Alternately, you can click the detail view (plus icon) in the table to update the credential.

    +

    Alt text

    +

    The update form will be rendered under the selected row in the table. You can change the password, any of the permissions or the app context when updating. The realm is the only field that cannot be changed. This is a limitation of the storage/passwords REST endpoint, not the app. You don't have to set the password to update the ACL's on the credential or move between apps. Simply choose new permissions or app scope and hit update.

    +

    Alt text

    +

    Delete Credential

    +

    Right click on a table entry to reveal a context menu that allows you to delete the credential.

    +

    Alt text

    +

    Alternately, select any individual credential or select all using the checkbox in the header column and press the delte button.

    +

    Alt text

    +

    Alt text

    +

    Reveal Clear Password

    +

    Click the eye icon to view the plain text password.

    +

    Alt text

    +

    Alt text

    +

    Using Stored Passwords

    +

    Please see this awesome blog post on using your newly stored credentials. When all else fails, dig into dev.splunk.com for more details.

    +

    Support

    +

    Feature Requests

    +

    Please submit feature requests through Github using the enhancement label so they can be tracked and discussed.

    +

    Bugs

    +

    Please submit bugs through Github using the bug label so they can be tracked and discussed.

    +
    For all other inquiries
    +

    Scott Haskell (shaskell@splunk.com)

    +
    Code hosted at Github
    +
    +
    \ No newline at end of file diff --git a/deployment-apps/rest-storage-passwords-manager/default/ui-tour.conf b/deployment-apps/rest-storage-passwords-manager/default/ui-tour.conf new file mode 100644 index 00000000..636659ff --- /dev/null +++ b/deployment-apps/rest-storage-passwords-manager/default/ui-tour.conf @@ -0,0 +1,26 @@ +[credential_management-tour:enterprise] +context = rest-storage-passwords-manager +imgPath = /credential_management-tour:enterprise +label = rest-storage-passwords-manager-tour +type = image +doneURL = /app/rest-storage-passwords-manager/credential_management +imageCaption1 = Create +imageName1 = credential-create.png +skipText = Skip tour +imageCaption2 = Specify username, password, realm (optional - e.g., prod or dev), read/write permissions, app scope and sharing. +imageName2 = create-form.png +imageCaption3 = Right click on row to update via context menu +imageName3 = context-update.png +imageCaption4 = Use the detail view to update a row +imageName4 = detail-view.png +imageCaption5 = Update password, permissions, app scope or sharing for the credential +imageName5 = inline-update.png +imageCaption6 = Right click on a row to delete via the context menu +imageName6 = context-delete.png +imageCaption7 = Delete multiple credentials by selecting individual rows or use the select all checkbox in the table header and click delete +imageName7 = multi-delete.png +imageCaption8 = Reveal the clear text password for a credential +imageName8 = show-password.png +imageCaption9 = The Dude abides! +imageName9 = clear-password-modal.png +viewed = 0 \ No newline at end of file diff --git a/deployment-apps/rest-storage-passwords-manager/default/web.conf b/deployment-apps/rest-storage-passwords-manager/default/web.conf new file mode 100644 index 00000000..eebb5f8b --- /dev/null +++ b/deployment-apps/rest-storage-passwords-manager/default/web.conf @@ -0,0 +1,3 @@ +[expose:configs_conf-PASSWORDS_MOVE] +methods = POST +pattern = configs/conf-passwords/*/move \ No newline at end of file diff --git a/deployment-apps/rest-storage-passwords-manager/metadata/default.meta b/deployment-apps/rest-storage-passwords-manager/metadata/default.meta new file mode 100644 index 00000000..f173c10d --- /dev/null +++ b/deployment-apps/rest-storage-passwords-manager/metadata/default.meta @@ -0,0 +1,12 @@ +[] +export = system + +[views/credential_management] +access = read : [ * ] +export = none +owner = nobody + +[views/help] +access = read : [ * ] +export = none +owner = nobody \ No newline at end of file diff --git a/deployment-apps/rest-storage-passwords-manager/splunkbase.manifest b/deployment-apps/rest-storage-passwords-manager/splunkbase.manifest new file mode 100644 index 00000000..95f3a91b --- /dev/null +++ b/deployment-apps/rest-storage-passwords-manager/splunkbase.manifest @@ -0,0 +1,194 @@ +{ + "version": "1.0", + "date": "2022-11-14T09:53:21.380255565Z", + "hashAlgorithm": "SHA-256", + "app": { + "id": 4013, + "version": "1.0.8", + "files": [ + { + "path": "README.md", + "hash": "17917df1b2742bf6ced6529ad9b5a388f830ef8e2b0bc8cde7a73f6359e6c2c5" + }, + { + "path": "appserver/static/Modal.js", + "hash": "86c6704ab58ca8527cafe352f15381cf28dc0493accb8e335a461c93f93d4f59" + }, + { + "path": "appserver/static/bootstrap-btn-danger.css", + "hash": "92f3372b358f048e72cf443ec43b65ba9b0281e81d1da8a13328ed99166e23c9" + }, + { + "path": "appserver/static/bootstrap-dropdown.css", + "hash": "17c1446fbbcf629f3c9c6f07babcd6e672ab6c68f48333ee1c628152785754c7" + }, + { + "path": "appserver/static/bootstrap-dropdown.js", + "hash": "1e27ab65eddd056cf3bf185c8a4784c53e5657faeadbb6742830d0f1367f638f" + }, + { + "path": "appserver/static/bootstrap-table-contextmenu.js", + "hash": "4498c41efcf6a551260efc7bbdc37cb53b83ecb6c7d8d62e99f2ac563ee5183d" + }, + { + "path": "appserver/static/bootstrap-table.css", + "hash": "72805bd7db12be1db2918f9f41096b92c48f618865a7d43ff3bedfa9223ebe42" + }, + { + "path": "appserver/static/bootstrap-table.js", + "hash": "e477192a88ad98341e4db3122706c526f50e5353b3d81a619813e2eafcdce717" + }, + { + "path": "appserver/static/img/credential_management-tour:enterprise/clear-password-modal.png", + "hash": "00a55d412d8ccd7e57404fe348020e465b3b570b7823a62a6862b7afb9c47123" + }, + { + "path": "appserver/static/img/credential_management-tour:enterprise/context-delete.png", + "hash": "8acd50c1563a144572925e929790602c86cf4b40a3a95f04344ae9269c84752d" + }, + { + "path": "appserver/static/img/credential_management-tour:enterprise/context-menu.png", + "hash": "f00f0a3e8d8898bf04f0574fcb48d5cffa409bdb14bd0d8741f7994a9396eda1" + }, + { + "path": "appserver/static/img/credential_management-tour:enterprise/context-update.png", + "hash": "43bf7a6c56c9d57305fc0d61dbade90ac212dca546e23e54422ca69711e5cc75" + }, + { + "path": "appserver/static/img/credential_management-tour:enterprise/create-form.png", + "hash": "2c6656d39f432bf3ae612e9e83b17b2b96aaeb08aa9c8caf687ef80773a68309" + }, + { + "path": "appserver/static/img/credential_management-tour:enterprise/create-success-modal.png", + "hash": "ecaf138a7d423cba326d9b96665e0bfbbc85c979a687c6b44ba9741f589546d0" + }, + { + "path": "appserver/static/img/credential_management-tour:enterprise/credential-create.png", + "hash": "c27abf488122cee4851f055db2324c39a022c08ccb5d37acb8a7461d12ded463" + }, + { + "path": "appserver/static/img/credential_management-tour:enterprise/detail-view.png", + "hash": "37091e1a66a36d50ce8f46d710f110843c161c834e177a0acd1f80d1d06e6452" + }, + { + "path": "appserver/static/img/credential_management-tour:enterprise/filter.png", + "hash": "ad0363ff48bd0f3553f1d968e96e29eb1f732b006cda2686272449cda4bf1e05" + }, + { + "path": "appserver/static/img/credential_management-tour:enterprise/inline-update.png", + "hash": "9f5911e98e630a49663bffd1d03e5e1bf01ca5dbba6537144d2e6482da0fbdb0" + }, + { + "path": "appserver/static/img/credential_management-tour:enterprise/multi-delete-confirm.png", + "hash": "c9adaf61556c5267c6d79a497d5892c0f08fb3f64ffe5e15954b5cf6a65ece1b" + }, + { + "path": "appserver/static/img/credential_management-tour:enterprise/multi-delete.png", + "hash": "a2b6a9ccca6b61fbc640d521a7ad229a9de0e28ab7663323bc854cc6f418c3d7" + }, + { + "path": "appserver/static/img/credential_management-tour:enterprise/show-password.png", + "hash": "0ed1bb7e0b6b1d5d55a9f03eda9db4b22783719f2ec971a26da61aeee4ab6365" + }, + { + "path": "appserver/static/img/credential_management-tour:enterprise/table.png", + "hash": "77b7c9f782d629121f8b5f359b9816eb852f3c99dd868106827ed90ad072c763" + }, + { + "path": "appserver/static/password-crud.js", + "hash": "097bd3e3c74f0ff8e2b834c00b71141ba56c8dc945e91cd6a41ed585ee06247e" + }, + { + "path": "bin/README", + "hash": "597cdad620bec4e52e0e8adc3cad99de9b3ce45da0dd18e4159e1009c976e957" + }, + { + "path": "default/app.conf", + "hash": "2c081ec42551be5ff60e81cece40a5a2529c69b1538cbc88b63899a59c759ced" + }, + { + "path": "default/data/ui/nav/default.xml", + "hash": "61ff3213833d58ffe188165b2c471bcf190a817544a6b0c5e15395b45e8de40c" + }, + { + "path": "default/data/ui/panels/credential-management.xml", + "hash": "3f94d92418394f18c6679aa6ac261faf312e7bfadfdcd04f8eb0f1b90211bb08" + }, + { + "path": "default/data/ui/views/README", + "hash": "4ccd9dc2dca5bd634f7c07ad1749e4e63a7969c84e2eff83517256f7c884cd29" + }, + { + "path": "default/data/ui/views/credential_management.xml", + "hash": "cb253d7f6279d28091bb008737d400f62f3072f5e8395bc84d619bf990da5c1a" + }, + { + "path": "default/data/ui/views/help.xml", + "hash": "d6ea7a342026d1c2f159d2b31b3bef8da2da3ec683f9c801b3163f331212bf00" + }, + { + "path": "default/ui-tour.conf", + "hash": "9adafad611a6408b1448e300f9c33d9f085c4c65c2a6c7d943e948ed4a4c13c9" + }, + { + "path": "default/web.conf", + "hash": "5f7f8165052a710dadd4c8a3ee3575027bf3ec9c19a8673526b4f23725f01e41" + }, + { + "path": "metadata/default.meta", + "hash": "da00a88bf168a3f7079ee944d539511865d3e704d8be37ceb0027217955f7649" + }, + { + "path": "static/appIcon.png", + "hash": "7e2aa1f79791147b9390a927d5c90eda59147c97b127968c165497e152470066" + }, + { + "path": "static/appIconAlt.png", + "hash": "7e2aa1f79791147b9390a927d5c90eda59147c97b127968c165497e152470066" + }, + { + "path": "static/appIconAlt_2x.png", + "hash": "ab60db107ac936dc61e74632c3db46797d265b32410be3631c21cdfef5cfd33b" + }, + { + "path": "static/appIcon_2x.png", + "hash": "ab60db107ac936dc61e74632c3db46797d265b32410be3631c21cdfef5cfd33b" + }, + { + "path": "static/appLogo.png", + "hash": "0f6bf549bed7bdabe188535cd756399d49fa6834a53670c20627466049952973" + }, + { + "path": "static/appLogo_2x.png", + "hash": "997037f671cb741a1c55f122a74188c0124ed9c4b820e731b93bb62e58a15e83" + } + ] + }, + "products": [ + { + "platform": "splunk", + "product": "enterprise", + "versions": [ + "7.0", + "7.1", + "7.2", + "7.3", + "8.0", + "8.1", + "8.2", + "9.0" + ], + "architectures": [ + "x86_64" + ], + "operatingSystems": [ + "windows", + "linux", + "macos", + "freebsd", + "solaris", + "aix" + ] + } + ] +} \ No newline at end of file diff --git a/deployment-apps/rest-storage-passwords-manager/static/appIcon.png b/deployment-apps/rest-storage-passwords-manager/static/appIcon.png new file mode 100644 index 00000000..e3f55c60 Binary files /dev/null and b/deployment-apps/rest-storage-passwords-manager/static/appIcon.png differ diff --git a/deployment-apps/rest-storage-passwords-manager/static/appIconAlt.png b/deployment-apps/rest-storage-passwords-manager/static/appIconAlt.png new file mode 100644 index 00000000..e3f55c60 Binary files /dev/null and b/deployment-apps/rest-storage-passwords-manager/static/appIconAlt.png differ diff --git a/deployment-apps/rest-storage-passwords-manager/static/appIconAlt_2x.png b/deployment-apps/rest-storage-passwords-manager/static/appIconAlt_2x.png new file mode 100644 index 00000000..1523d4e0 Binary files /dev/null and b/deployment-apps/rest-storage-passwords-manager/static/appIconAlt_2x.png differ diff --git a/deployment-apps/rest-storage-passwords-manager/static/appIcon_2x.png b/deployment-apps/rest-storage-passwords-manager/static/appIcon_2x.png new file mode 100644 index 00000000..1523d4e0 Binary files /dev/null and b/deployment-apps/rest-storage-passwords-manager/static/appIcon_2x.png differ diff --git a/deployment-apps/rest-storage-passwords-manager/static/appLogo.png b/deployment-apps/rest-storage-passwords-manager/static/appLogo.png new file mode 100644 index 00000000..f7768ac7 Binary files /dev/null and b/deployment-apps/rest-storage-passwords-manager/static/appLogo.png differ diff --git a/deployment-apps/rest-storage-passwords-manager/static/appLogo_2x.png b/deployment-apps/rest-storage-passwords-manager/static/appLogo_2x.png new file mode 100644 index 00000000..6ea1b008 Binary files /dev/null and b/deployment-apps/rest-storage-passwords-manager/static/appLogo_2x.png differ