Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions directorygroups/src/Hooks/AddDirectoryDomainConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ public function __construct()
parent::__construct();
$this->registerInstalled([
['DIRECTORY_CONNECTION', 'connection'],
['DIRECTORY_JOIN_ACCOUNT', 'joinAccount'],
['SETTINGS_HIDDEN', 'hideSettings'],
]);
}
Expand All @@ -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.
*
Expand Down
9 changes: 5 additions & 4 deletions directorygroups/src/Hooks/AddDirectoryGroupsAPI.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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.
*
Expand All @@ -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);
}
)
);
Expand Down
23 changes: 22 additions & 1 deletion directorygroups/src/Items/DirectoryDomain.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
*
Expand Down
4 changes: 4 additions & 0 deletions directorygroups/src/Managers/DirectoryGroupsManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ''",
];
}
/**
Expand Down
22 changes: 19 additions & 3 deletions directorygroups/src/Pages/DirectoryDomainManagement.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
}
/**
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
Expand Down
147 changes: 147 additions & 0 deletions tests/directorygroups-join-account.test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<?php
/**
* A Directory Domain's join account, and where its password must not go.
*
* GH-1887. A domain can carry the account its hosts join with. Core asks
* for it through DIRECTORY_JOIN_ACCOUNT on every path that hands a join
* account to a client, and the domain's account beats the host's own when
* the domain has one.
*
* What this pins:
* - joinAccount() answers only when both the user and the password are set;
* - the listener is registered for the event;
* - the join password is stripped from the API in the `always` tier and
* dropped from the export, the same as the bind password;
* - the columns arrive by appended ALTER steps.
*
* Requires no database.
*
* PHP version 7.4+
*
* @category Tests
* @package FOGProject
* @author Tom Elliott <tommygunsster@gmail.com>
* @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";
}
Loading