diff --git a/directorygroups/src/Hooks/AddDirectoryDomainConnection.php b/directorygroups/src/Hooks/AddDirectoryDomainConnection.php index 85d33a9..7d8e066 100644 --- a/directorygroups/src/Hooks/AddDirectoryDomainConnection.php +++ b/directorygroups/src/Hooks/AddDirectoryDomainConnection.php @@ -79,6 +79,7 @@ public function __construct() parent::__construct(); $this->registerInstalled([ ['DIRECTORY_CONNECTION', 'connection'], + ['DIRECTORY_JOIN_ACCOUNT', 'joinAccount'], ['SETTINGS_HIDDEN', 'hideSettings'], ]); } @@ -101,6 +102,28 @@ public function connection($arguments) ? $domain->connection() : ['uri' => '', 'bindDN' => '', 'password' => '', 'caCert' => '', 'baseDN' => '']; } + /** + * Answers with the join account of the host's domain. + * + * The domain's account beats the one saved on the host when the domain + * has one (GH-1887). Registration copies the default account onto every + * host, so the other way round the domain's account would almost never + * apply. A domain with no join account, or no domain in the list, leaves + * the host's own account. + * + * @param mixed $arguments The event arguments. + * + * @return void + */ + public function joinAccount($arguments) + { + $domain = DirectoryDomain::forName($arguments['domain'] ?? ''); + $account = $domain ? $domain->joinAccount() : null; + if (null === $account) { + return; + } + list($arguments['user'], $arguments['password']) = $account; + } /** * Hides the settings the domain list replaces. * diff --git a/directorygroups/src/Hooks/AddDirectoryGroupsAPI.php b/directorygroups/src/Hooks/AddDirectoryGroupsAPI.php index 76c66ec..42f2071 100644 --- a/directorygroups/src/Hooks/AddDirectoryGroupsAPI.php +++ b/directorygroups/src/Hooks/AddDirectoryGroupsAPI.php @@ -80,7 +80,7 @@ public function injectAPIElements($arguments) ); } /** - * Declares a domain's bind password as a secret the API never emits. + * Declares a domain's bind and join passwords as secrets the API never emits. * * The 'always' tier, as the ldap plugin does for its own bind password: * only the server binds with it, so nothing has a reason to read it @@ -93,6 +93,7 @@ public function injectAPIElements($arguments) public function declareSensitiveFields($arguments) { $arguments['always']['directorydomain'][] = 'bindPwd'; + $arguments['always']['directorydomain'][] = 'joinPwd'; } /** * Refuses API writes to a directory group's core group. @@ -110,11 +111,11 @@ public function declareServerOwnedFields($arguments) $arguments['fields']['directorygroups'][] = 'groupID'; } /** - * Keeps the bind password out of the domain export. + * Keeps the bind and join passwords out of the domain export. * * The list is covered by the sensitive-field tier above. FOGPage::export() * builds its CSV from the table's own columns and does not consult that - * tier, so without this the export would carry the password. + * tier, so without this the export would carry the passwords. * * @param mixed $arguments The arguments to modify. * @@ -126,7 +127,7 @@ public function stripBindPassword($arguments) array_filter( $arguments['columns'], function ($column) { - return 'bindPwd' !== $column['dt']; + return !in_array($column['dt'], ['bindPwd', 'joinPwd'], true); } ) ); diff --git a/directorygroups/src/Items/DirectoryDomain.php b/directorygroups/src/Items/DirectoryDomain.php index 5b4ce64..32d5a10 100644 --- a/directorygroups/src/Items/DirectoryDomain.php +++ b/directorygroups/src/Items/DirectoryDomain.php @@ -60,7 +60,9 @@ class DirectoryDomain extends \FOG\Base\FOGController 'bindPwd' => 'ddBindPwd', 'caCert' => 'ddCACert', 'baseDN' => 'ddBaseDN', - 'ouBase' => 'ddOUBase' + 'ouBase' => 'ddOUBase', + 'joinUser' => 'ddJoinUser', + 'joinPwd' => 'ddJoinPwd' ]; /** * The required fields. @@ -114,6 +116,25 @@ public function connection() 'baseDN' => (string)$this->get('baseDN') ]; } + /** + * The account hosts in this domain join with, or null. + * + * Both halves must be set. A user with no password, or a password with + * no user, is not an account, and the host keeps its own (GH-1887). + * + * @return array|null [user, password] + */ + public function joinAccount() + { + $user = trim((string)$this->get('joinUser')); + $password = \FOG\Agent\DirectoryPlacement::decodeStored( + (string)$this->get('joinPwd') + ); + if ('' === $user || '' === $password) { + return null; + } + return [$user, $password]; + } /** * Where the OU browser starts. * diff --git a/directorygroups/src/Managers/DirectoryGroupsManager.php b/directorygroups/src/Managers/DirectoryGroupsManager.php index c04680f..0df79b1 100644 --- a/directorygroups/src/Managers/DirectoryGroupsManager.php +++ b/directorygroups/src/Managers/DirectoryGroupsManager.php @@ -167,6 +167,10 @@ function () { } return \FOG\Db\SchemaReconciler::applyConstraints('directorygroups'); }, + // 8, 9 - the account hosts in a domain join with (GH-1887). The + // password is stored as typed, like the bind password. + "ALTER TABLE `directoryDomains` ADD COLUMN `ddJoinUser` VARCHAR(255) NOT NULL DEFAULT ''", + "ALTER TABLE `directoryDomains` ADD COLUMN `ddJoinPwd` VARCHAR(255) NOT NULL DEFAULT ''", ]; } /** diff --git a/directorygroups/src/Pages/DirectoryDomainManagement.php b/directorygroups/src/Pages/DirectoryDomainManagement.php index c0bc92e..afc288d 100644 --- a/directorygroups/src/Pages/DirectoryDomainManagement.php +++ b/directorygroups/src/Pages/DirectoryDomainManagement.php @@ -111,7 +111,19 @@ private function _fields(array $values, $isEdit) ] + $text('caCert', _('CA Certificate Path'), '/etc/ssl/certs/example-ca.pem') + $text('baseDN', _('Computer Search Base'), 'DC=example,DC=com') - + $text('ouBase', _('OU Browse Base'), _('Blank starts at the domain root')); + + $text('ouBase', _('OU Browse Base'), _('Blank starts at the domain root')) + + $text('joinUser', _('Join Username'), _('Blank: each host joins with its own account')) + + [ + self::makeLabel($labelClass, 'joinPwd', _('Join Password')) + => self::makeInput( + 'form-control', + 'joinPwd', + $isEdit ? _('Leave blank to keep the current password') : '', + 'password', + 'joinPwd', + '' + ) + ]; } /** * The posted connection fields, trimmed. @@ -123,11 +135,12 @@ private function _fields(array $values, $isEdit) private static function _posted() { $out = []; - foreach (['name', 'description', 'uri', 'bindDN', 'caCert', 'baseDN', 'ouBase'] as $key) { + foreach (['name', 'description', 'uri', 'bindDN', 'caCert', 'baseDN', 'ouBase', 'joinUser'] as $key) { $out[$key] = trim((string)filter_input(INPUT_POST, $key)); } $out['name'] = strtolower($out['name']); $out['bindPwd'] = (string)filter_input(INPUT_POST, 'bindPwd'); + $out['joinPwd'] = (string)filter_input(INPUT_POST, 'joinPwd'); return $out; } /** @@ -217,7 +230,7 @@ function (&$serverFault) { public function directorydomainGeneral() { $values = []; - foreach (['name', 'description', 'uri', 'bindDN', 'caCert', 'baseDN', 'ouBase'] as $key) { + foreach (['name', 'description', 'uri', 'bindDN', 'caCert', 'baseDN', 'ouBase', 'joinUser'] as $key) { $values[$key] = filter_input(INPUT_POST, $key) ?: $this->obj->get($key); } $fields = $this->_fields($values, true); @@ -278,6 +291,9 @@ public function directorydomainGeneralPost() if ('' === $in['bindPwd']) { unset($in['bindPwd']); } + if ('' === $in['joinPwd']) { + unset($in['joinPwd']); + } foreach ($in as $key => $value) { $this->obj->set($key, $value); } diff --git a/tests/directorygroups-join-account.test.php b/tests/directorygroups-join-account.test.php new file mode 100644 index 0000000..a04b4cb --- /dev/null +++ b/tests/directorygroups-join-account.test.php @@ -0,0 +1,147 @@ + + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ + +namespace FOG\Agent { + /** + * Stand-in for core's stored-secret decoder: hands the value back. + */ + class DirectoryPlacement + { + /** + * Decode a stored secret. + * + * @param string $value the stored value + * + * @return string + */ + public static function decodeStored($value) + { + return (string)$value; + } + } +} + +namespace { + $root = dirname(__DIR__); + $failures = []; + $checks = 0; + + /** + * Asserts a condition. + * + * @param string $label what is being checked + * @param bool $cond the condition + * + * @return void + */ + function check($label, $cond) + { + global $failures, $checks; + $checks++; + if (!$cond) { + $failures[] = $label; + } + } + + require_once __DIR__ . '/stubs/fog-stubs.php'; + require_once $root . '/directorygroups/src/Items/DirectoryDomain.php'; + require_once $root . '/directorygroups/src/Hooks/AddDirectoryDomainConnection.php'; + require_once $root . '/directorygroups/src/Hooks/AddDirectoryGroupsAPI.php'; + + $domain = function ($user, $password) { + return (new \FOG\Plugins\DirectoryGroups\Items\DirectoryDomain()) + ->set('joinUser', $user) + ->set('joinPwd', $password); + }; + + // ------------------------------------------------ 1. what is an account + + check( + 'a user and a password make an account, the user trimmed', + ['svc-join', 'secret'] === $domain(' svc-join ', 'secret')->joinAccount() + ); + check( + 'a user with no password is not an account', + null === $domain('svc-join', '')->joinAccount() + ); + check( + 'a password with no user is not an account', + null === $domain('', 'secret')->joinAccount() + ); + + // ---------------------------------------------------- 2. the listener + + $hook = new \FOG\Plugins\DirectoryGroups\Hooks\AddDirectoryDomainConnection(); + $events = []; + foreach ($hook->registered as $pair) { + $events[$pair[0]] = $pair[1]; + } + check( + 'the plugin answers DIRECTORY_JOIN_ACCOUNT', + 'joinAccount' === ($events['DIRECTORY_JOIN_ACCOUNT'] ?? null) + ); + + // ------------------------------------------- 3. the password stays put + + $api = new \FOG\Plugins\DirectoryGroups\Hooks\AddDirectoryGroupsAPI(); + // Core hands each listener an array of references, as processEvent() + // callers do, so the writes land in the caller's variables. + $always = []; + $api->declareSensitiveFields(['always' => &$always]); + check( + 'the API never emits the join password, not even for one domain', + in_array('joinPwd', (array)($always['directorydomain'] ?? []), true) + ); + $columns = [ + ['dt' => 'name'], ['dt' => 'bindPwd'], ['dt' => 'joinUser'], ['dt' => 'joinPwd'], + ]; + $api->stripBindPassword(['columns' => &$columns]); + check( + 'the export drops both passwords and keeps the join user', + ['name', 'joinUser'] === array_column($columns, 'dt') + ); + + // -------------------------------------------------------- 4. schema + + $manager = (string)file_get_contents( + $root . '/directorygroups/src/Managers/DirectoryGroupsManager.php' + ); + check( + 'ddJoinUser and ddJoinPwd arrive by their own ALTER steps', + false !== strpos($manager, 'ADD COLUMN `ddJoinUser`') + && false !== strpos($manager, 'ADD COLUMN `ddJoinPwd`') + ); + + if (count($failures)) { + echo 'FAIL: ' . count($failures) . " of $checks checks failed.\n"; + foreach ($failures as $f) { + echo " - $f\n"; + } + exit(1); + } + echo "directorygroups-join-account: $checks checks passed\n"; +}