diff --git a/README.md b/README.md index 4dbd380..aaca60a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ The plugins that ship with [FOG Project](https://github.com/FOGProject/fogproject). One repository, one release, per-plugin versions in each manifest. -This is the source of truth for the 15 bundled plugins. The FOG build pins a +This is the source of truth for the 16 bundled plugins. The FOG build pins a revision of this repository and lays its contents into `packages/web/lib/plugins/` in the tarball. @@ -87,6 +87,7 @@ the rest. | Plugin | Version | What it does | |---|---|---| | `capone` | 1.6.0 | Automation plugin — image a host straight from PXE without registering it | +| `directorygroups` | 1.6.0 | Group hosts by Active Directory OU, across one or more domains | | `helloworld` | 1.6.0 | Skeleton example: config, model, manager, page, hooks, JS | | `ldap` | 1.6.0 | Authenticate FOG users against an LDAP or AD directory | | `location` | 1.6.0 | Serve images from the storage node nearest a host — multi-site installs | diff --git a/directorygroups/config/plugin.config.php b/directorygroups/config/plugin.config.php new file mode 100644 index 0000000..27bc6bd --- /dev/null +++ b/directorygroups/config/plugin.config.php @@ -0,0 +1,32 @@ + + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +/** + * Plugin configuration file. + * + * @category Config + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +$fog_plugin = []; +$fog_plugin['name'] = 'directorygroups'; +$fog_plugin['description'] = _( + 'Directory Groups: group hosts by Active Directory OU, across one or ' + . 'more domains.' +); +$fog_plugin['menuicon'] = 'fas fa-folder-tree fa-fw'; +$fog_plugin['version'] = '1.6.0'; +$fog_plugin['fog_min'] = '1.6.0'; +$fog_plugin['author'] = 'Tom Elliott'; +$fog_plugin['homepage'] = 'https://fogproject.org'; diff --git a/directorygroups/js/fog.directorydomain.add.js b/directorygroups/js/fog.directorydomain.add.js new file mode 100644 index 0000000..fb42c8e --- /dev/null +++ b/directorygroups/js/fog.directorydomain.add.js @@ -0,0 +1,3 @@ +(function($) { + $('#directorydomain-create-form').wireCreateForm(); +})(jQuery); diff --git a/directorygroups/js/fog.directorydomain.edit.js b/directorygroups/js/fog.directorydomain.edit.js new file mode 100644 index 0000000..10031eb --- /dev/null +++ b/directorygroups/js/fog.directorydomain.edit.js @@ -0,0 +1,109 @@ +$(function() { + // --------------------------------------------------------------- + // GENERAL TAB + $.registerGeneralTab({ + nameInputSel: '#name', + formSel: '#directorydomain-general-form' + }); + + // --------------------------------------------------------------- + // ORGANIZATIONAL UNITS TAB + // + // The whole tree, client side: listOUs() already read every page of it, + // so search and scroll need no further round trip. Indented by depth + // and kept in the server's root-first order, so it reads as a tree. + var table = $('#directorydomain-ous-table'), + sendBtn = $('#directorydomain-ous-send'), + loaded = false, + dt; + + function load() { + if (loaded || !table.length) { + return; + } + loaded = true; + $.get( + '../management/index.php?node=directorydomain&sub=getOUs&id=' + + table.data('domain-id'), + function(res) { + dt = table.DataTable({ + data: res.data, + ordering: false, + pageLength: 25, + columns: [ + {data: 'dn'}, + {data: 'name'}, + {data: 'dn'}, + {data: 'groupID'} + ], + columnDefs: [ + { + targets: 0, + searchable: false, + render: function(data, type, row) { + if (row.groupID) { + return ''; + } + return ''; + } + }, + { + targets: 1, + render: function(data, type, row) { + if (type !== 'display') { + return data; + } + return ' ' + + $.escapeHtml(data) + ''; + } + }, + { + targets: 3, + searchable: false, + render: function(data) { + if (!data) { + return ''; + } + return '' + $.escapeHtml(String(data)) + ''; + } + } + ] + }); + }, + 'json' + ).fail(function(jqXHR) { + loaded = false; + $.notifyFromAPI(jqXHR.responseJSON, jqXHR); + }); + } + + // Fetched when the tab is first shown, not with the page: a slow or + // unreachable directory must not hold up the General tab. + $('a[href="#directorydomain-ous"]').on('shown.bs.tab', load); + if (window.location.hash === '#directorydomain-ous') { + load(); + } + + sendBtn.on('click', function() { + if (!dt) { + return; + } + var ous = dt.$('input.ou-pick:checked').map(function() { + return this.value; + }).get(); + sendBtn.prop('disabled', true); + $.apiCall('POST', table.data('update-url'), {ous: ous}, function(err) { + sendBtn.prop('disabled', false); + if (err) { + return; + } + dt.destroy(); + table.find('tbody').remove(); + loaded = false; + load(); + }); + }); +}); diff --git a/directorygroups/js/fog.directorydomain.export.js b/directorygroups/js/fog.directorydomain.export.js new file mode 100644 index 0000000..5cba31e --- /dev/null +++ b/directorygroups/js/fog.directorydomain.export.js @@ -0,0 +1,14 @@ +(function($) { + // No bindPwd column: the export strips it (AddDirectoryGroupsAPI). + $('#directorydomain-export-table').registerExportTable([ + {data: 'name'}, + {data: 'description', visible: false}, + {data: 'createdBy', visible: false}, + {data: 'createdTime', visible: false}, + {data: 'uri'}, + {data: 'bindDN'}, + {data: 'caCert', visible: false}, + {data: 'baseDN'}, + {data: 'ouBase', visible: false} + ]); +})(jQuery); diff --git a/directorygroups/js/fog.directorydomain.list.js b/directorygroups/js/fog.directorydomain.list.js new file mode 100644 index 0000000..33b1d6e --- /dev/null +++ b/directorygroups/js/fog.directorydomain.list.js @@ -0,0 +1,18 @@ +(function($) { + $.registerListPage({ + order: [ + [0, 'asc'] + ], + columns: [ + {data: 'mainlink'}, + {data: 'uri'} + ], + rowId: 'id', + columnDefs: [ + { + responsivePriority: -1, + targets: 0 + } + ] + }); +})(jQuery); diff --git a/directorygroups/js/fog.directorygroups.add.js b/directorygroups/js/fog.directorygroups.add.js new file mode 100644 index 0000000..fcc7268 --- /dev/null +++ b/directorygroups/js/fog.directorygroups.add.js @@ -0,0 +1,3 @@ +(function($) { + $('#directorygroups-create-form').wireCreateForm(); +})(jQuery); diff --git a/directorygroups/js/fog.directorygroups.edit.js b/directorygroups/js/fog.directorygroups.edit.js new file mode 100644 index 0000000..07e0681 --- /dev/null +++ b/directorygroups/js/fog.directorygroups.edit.js @@ -0,0 +1,42 @@ +$(function() { + // --------------------------------------------------------------- + // GENERAL TAB + $.registerGeneralTab({ + nameInputSel: '#ou', + formSel: '#directorygroups-general-form' + }); + + // --------------------------------------------------------------- + // HOST TAB + var directorygroupsHostsTable = $.registerAssociationTab({ + slug: 'directorygroups-host', + item: 'host', + sub: 'getHostsList', + columns: [ + {data: 'name'}, + {data: 'association'} + ], + columnDefs: [ + { + responsivePriority: -1, + // Aisle 097: this tab supplies its own column-0 render, so it + // bypasses the server-side escape in FOGController's mainLink + // formatter and concatenated the name in raw. Mitigated today + // only by isHostnameSafe() on the write side -- do not rely on + // that staying the sole gate. + render: function(data, type, row) { + return '' + + $.escapeHtml(data) + + ''; + }, + targets: 0 + } + ] + }); + + if (Common.search && Common.search.length > 0) { + directorygroupsHostsTable.search(Common.search).draw(); + } +}); diff --git a/directorygroups/js/fog.directorygroups.export.js b/directorygroups/js/fog.directorygroups.export.js new file mode 100644 index 0000000..7d328cf --- /dev/null +++ b/directorygroups/js/fog.directorygroups.export.js @@ -0,0 +1,9 @@ +(function($) { + $('#directorygroups-export-table').registerExportTable([ + {data: 'name'}, + {data: 'description', visible: false}, + {data: 'createdBy', visible: false}, + {data: 'createdTime', visible: false}, + {data: 'ou'} + ]); +})(jQuery); diff --git a/directorygroups/js/fog.directorygroups.host.edit.js b/directorygroups/js/fog.directorygroups.host.edit.js new file mode 100644 index 0000000..1bc4a91 --- /dev/null +++ b/directorygroups/js/fog.directorygroups.host.edit.js @@ -0,0 +1,112 @@ +(function($) { + // The whole tab -- Update wiring plus the create-and-associate button and + // modal -- is one shared helper (fog.common.js). These nine plugin tabs are + // the same card with a different noun, and each used to carry its own copy + // of the submit wiring. + $.registerSelectTab({ + slug: 'host-directorygroups', + send: 'directorygroups-send', + node: 'directorygroups' + }); +})(jQuery); +(function($) { + // --------------------------------------------------------------- + // ACTIVE DIRECTORY TAB: Browse Directory + // + // Lists a domain's OUs; a click on one fills the tab's Domain Name and + // Organizational Unit fields. The admin still clicks Update to save. + var browseBtn = $('#directorygroups-browse'), + modal = $('#directorygroups-browse-modal'), + domainSel = $('#directorygroups-browse-domain'), + table = $('#directorygroups-browse-table'), + domains = {}, + dt; + + if (!browseBtn.length) { + return; + } + + function showDomain(id) { + if (dt) { + dt.destroy(); + table.find('tbody').remove(); + dt = null; + } + $.get( + '../management/index.php?node=directorydomain&sub=getOUs&id=' + id, + function(res) { + dt = table.DataTable({ + data: res.data, + ordering: false, + pageLength: 25, + columns: [ + {data: 'name'}, + {data: 'dn'} + ], + columnDefs: [ + { + targets: 0, + render: function(data, type, row) { + if (type !== 'display') { + return data; + } + return ' ' + + $.escapeHtml(data) + ''; + } + } + ] + }); + }, + 'json' + ).fail(function(jqXHR) { + $.notifyFromAPI(jqXHR.responseJSON, jqXHR); + }); + } + + table.on('click', 'tbody tr', function() { + if (!dt) { + return; + } + var row = dt.row(this).data(); + if (!row) { + return; + } + $('#adDomain').val(domains[domainSel.val()]); + $('#adOU').val(row.dn); + modal.modal('hide'); + }); + + domainSel.on('change', function() { + showDomain(this.value); + }); + + browseBtn.on('click', function(e) { + e.preventDefault(); + modal.modal('show'); + if (domainSel.children().length) { + return; + } + $.get( + '../management/index.php?node=directorydomain&sub=getDomains', + function(res) { + var current = ($('#adDomain').val() || '').toLowerCase(), + pick = null; + $.each(res.data, function(i, d) { + domains[d.id] = d.name; + domainSel.append($('