-
Notifications
You must be signed in to change notification settings - Fork 16
Add EAS version configuration per user via perms system #74
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -214,6 +214,10 @@ public function authenticate($username, $password, $domain = null) | |
| { | ||
| global $injector, $conf; | ||
|
|
||
| // Initialize base auth state immediately so _authUser is always set, | ||
| // even if we return a policy denial before the end of this method. | ||
| parent::authenticate($username, $password, $domain); | ||
|
|
||
| $this->_logger->info( | ||
| sprintf( | ||
| '%sHorde_Core_ActiveSync_Driver::authenticate() attempt for %s%s', | ||
|
|
@@ -274,8 +278,18 @@ public function authenticate($username, $password, $domain = null) | |
| } | ||
|
|
||
| // Get the username from the registry so we capture it after any | ||
| // hooks were run on it. | ||
| $username = $GLOBALS['registry']->getAuth(); | ||
| // hooks were run on it. In some setups this can be empty even after | ||
| // successful backend auth; fall back to the client-provided username | ||
| // to avoid saving device state with a NULL user. | ||
| $registryAuth = $GLOBALS['registry']->getAuth(); | ||
| if (!empty($registryAuth)) { | ||
| $username = $registryAuth; | ||
| } else { | ||
| $this->_logger->warn(sprintf( | ||
| 'Registry auth is empty after successful ActiveSync authentication; falling back to provided username "%s".', | ||
| (string)$username | ||
| )); | ||
| } | ||
| $perms = $injector->getInstance('Horde_Perms'); | ||
| if ($perms->exists('horde:activesync')) { | ||
| // Check permissions to ActiveSync | ||
|
|
@@ -290,7 +304,7 @@ public function authenticate($username, $password, $domain = null) | |
| } | ||
| } | ||
|
|
||
| return parent::authenticate($username, $password, $domain); | ||
| return true; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -3142,6 +3156,157 @@ public function meetingResponse(array $response) | |
| return $uid; | ||
| } | ||
|
|
||
| /** | ||
| * Optionally change the supported EAS version per-user via permissions. | ||
| * | ||
| * The global conf['activesync']['version'] acts as default. | ||
| * | ||
| * @param Horde_ActiveSync $server The ActiveSync server instance. | ||
| */ | ||
| public function versionCallback(Horde_ActiveSync $server) | ||
| { | ||
| $credentials = new Horde_ActiveSync_Credentials($server); | ||
| $authUsername = !empty($credentials->username) | ||
| ? (string)$credentials->username | ||
| : null; | ||
| if ($authUsername === null) { | ||
| $get = $server->getGetVars(); | ||
| if (!empty($get['User'])) { | ||
| $authUsername = (string)$get['User']; | ||
| } | ||
| } | ||
| if ($authUsername === null && !empty($GLOBALS['registry']->getAuth())) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not needed in combination with #75 - The registry is now injected as a dependency and you can use it without accessing the global. |
||
| $authUsername = (string)$GLOBALS['registry']->getAuth(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This flattens a null or false ("Not authenticated") to an empty string. (which might be what you want) |
||
| } | ||
| if ($authUsername === null) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So you never get here. |
||
| $authUsername = $this->getUser(); | ||
| } | ||
| if (empty($authUsername)) { | ||
| $this->_logger->meta('Cannot resolve ActiveSync username for version override; skipping override.'); | ||
| return; | ||
| } | ||
|
|
||
| $username = $this->getUsernameFromEmail($authUsername); | ||
| if (empty($username)) { | ||
| $username = $authUsername; | ||
| } | ||
| $pos = strrpos($username, '\\'); | ||
| if ($pos !== false) { | ||
| $username = substr($username, $pos + 1); | ||
| } | ||
| if (empty($username)) { | ||
| $this->_logger->meta('Resolved ActiveSync username is empty after normalization; skipping version override.'); | ||
| return; | ||
| } | ||
|
|
||
| $mode = !empty($GLOBALS['conf']['activesync']['version_mode']) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be a LegacyMergedConfig object in the constructor |
||
| ? strtolower((string)$GLOBALS['conf']['activesync']['version_mode']) | ||
| : 'user'; | ||
|
|
||
| if ($mode === 'device') { | ||
| $allowed = $this->_getDeviceScopedVersionPermission($server, $username); | ||
| } else { | ||
| $perms = $GLOBALS['injector']->getInstance('Horde_Perms'); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be constructor injected |
||
| if (!$perms->exists('horde:activesync:version')) { | ||
| return; | ||
| } | ||
| $allowed = $perms->getPermissions('horde:activesync:version', $username); | ||
| } | ||
|
|
||
| $resolved = $this->_resolveRestrictedEasVersion( | ||
| $allowed, | ||
| [ | ||
| Horde_ActiveSync::VERSION_TWOFIVE, | ||
| Horde_ActiveSync::VERSION_TWELVE, | ||
| Horde_ActiveSync::VERSION_TWELVEONE, | ||
| Horde_ActiveSync::VERSION_FOURTEEN, | ||
| Horde_ActiveSync::VERSION_FOURTEENONE, | ||
| Horde_ActiveSync::VERSION_SIXTEEN, | ||
| ] | ||
| ); | ||
|
|
||
| if ($resolved === null) { | ||
| return; | ||
| } | ||
|
|
||
| $server->setSupportedVersion($resolved); | ||
| $this->_logger->meta(sprintf( | ||
| 'Restricted EAS version for %s to %s via %s mode.', | ||
| $username, | ||
| $resolved, | ||
| $mode | ||
| )); | ||
| } | ||
|
|
||
| /** | ||
| * Resolve device-scoped EAS version from hooks. | ||
| * | ||
| * Configure with conf['activesync']['version_mode'] = 'device' and implement | ||
| * hooks.php: activesync_device_version($deviceId, $user), returning: | ||
| * - string: one EAS version (e.g. "16.0") | ||
| * - array: multiple candidates (restrictive min is selected) | ||
| * - null/false/''/-1: no override | ||
| * | ||
| * @param Horde_ActiveSync $server The ActiveSync server. | ||
| * @param string $username Authenticated Horde username. | ||
| * | ||
| * @return mixed Hook return value or null if unavailable. | ||
| */ | ||
| protected function _getDeviceScopedVersionPermission(Horde_ActiveSync $server, $username) | ||
| { | ||
| $get = $server->getGetVars(); | ||
| $deviceId = !empty($get['DeviceId']) ? Horde_String::upper($get['DeviceId']) : null; | ||
| if (empty($deviceId)) { | ||
| return null; | ||
| } | ||
|
|
||
| try { | ||
| return $GLOBALS['injector']->getInstance('Horde_Core_Hooks') | ||
| ->callHook('activesync_device_version', 'horde', [$deviceId, $username]); | ||
| } catch (Horde_Exception_HookNotSet $e) { | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Resolve effective EAS version from permission value(s), restrictive mode. | ||
| * | ||
| * @param mixed $allowed Scalar or array permission value(s). | ||
| * @param array $supported Supported versions in ascending order. | ||
| * | ||
| * @return string|null The resolved version or null if no valid override. | ||
| */ | ||
| protected function _resolveRestrictedEasVersion($allowed, array $supported) | ||
| { | ||
| if ($allowed === null || $allowed === false || $allowed === '') { | ||
| return null; | ||
| } | ||
|
|
||
| $supported = array_values(array_filter(array_map('strval', $supported))); | ||
| if (empty($supported)) { | ||
| return null; | ||
| } | ||
|
|
||
| $rank = array_flip($supported); | ||
| $values = is_array($allowed) ? $allowed : [$allowed]; | ||
| $candidates = []; | ||
| foreach ($values as $value) { | ||
| $value = trim((string)$value); | ||
| if ($value === '' || !isset($rank[$value])) { | ||
| continue; | ||
| } | ||
| $candidates[$value] = $rank[$value]; | ||
| } | ||
|
|
||
| if (empty($candidates)) { | ||
| return null; | ||
| } | ||
|
|
||
| asort($candidates); | ||
| return (string)key($candidates); | ||
| } | ||
|
|
||
| /** | ||
| * Callback method called before new device is created for a user. Allows | ||
| * final check of permissions. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sets your user's username and password as parameters in the base driver. The third parameter domain is never used.
Where does your new code populate this?
I think it is risky not to do it.