From 683c669bb25d73fc3f5d4367891b49a0d2ea1c58 Mon Sep 17 00:00:00 2001 From: Bernhard Schmitt Date: Tue, 26 Sep 2017 10:47:10 +0200 Subject: [PATCH 1/9] BUGFIX: Reset roles before collecting new ones to allow access downgrades --- .../Security/Authentication/Provider/LdapProvider.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Classes/Security/Authentication/Provider/LdapProvider.php b/Classes/Security/Authentication/Provider/LdapProvider.php index 5a0a334..98e4772 100644 --- a/Classes/Security/Authentication/Provider/LdapProvider.php +++ b/Classes/Security/Authentication/Provider/LdapProvider.php @@ -134,6 +134,7 @@ protected function createAccountForCredentials(array $credentials) */ protected function setRoles(Account $account, array $ldapSearchResult) { + $this->resetRoles($account); $this->setDefaultRoles($account); $this->setRolesMappedToUserDn($account, $ldapSearchResult); $this->setRolesBasedOnGroupMembership($account, $ldapSearchResult); @@ -141,6 +142,14 @@ protected function setRoles(Account $account, array $ldapSearchResult) $this->accountRepository->update($account); } + /** + * @param Account $account + */ + protected function resetRoles(Account $account) + { + $account->setRoles([]); + } + /** * Set all default roles * From b7f03a7fcc9e7f347d43c772f9eff9a640379f2f Mon Sep 17 00:00:00 2001 From: Rens Admiraal Date: Mon, 13 Nov 2017 21:40:40 +0100 Subject: [PATCH 2/9] TASK: Replace bind providers by symfony/ldap The configuration of the bind providers lead to a lot of confusion so we replace those by the symfony/ldap component. This change is the first kickstart to a working setup with simplified configuration format. It also changes the way a user is authenticated from using a single filter to directly fetch an account to a query that first fetches a possible account and then uses the fetched 'dn' to authenticate the user. Additionaly a provider first pragmatic Neos backend provider is added. --- .../Provider/NeosBackendLdapProvider.php | 48 ++++++ .../BindProvider/AbstractBindProvider.php | 116 -------------- .../BindProvider/ActiveDirectoryBind.php | 65 -------- .../BindProvider/BindProviderInterface.php | 55 ------- Classes/Service/BindProvider/LdapBind.php | 71 --------- Classes/Service/DirectoryService.php | 147 +++++++----------- Configuration/Settings.yaml.ldap.example | 44 +++--- composer.json | 6 +- 8 files changed, 133 insertions(+), 419 deletions(-) create mode 100644 Classes/Security/Authentication/Provider/NeosBackendLdapProvider.php delete mode 100644 Classes/Service/BindProvider/AbstractBindProvider.php delete mode 100644 Classes/Service/BindProvider/ActiveDirectoryBind.php delete mode 100644 Classes/Service/BindProvider/BindProviderInterface.php delete mode 100644 Classes/Service/BindProvider/LdapBind.php diff --git a/Classes/Security/Authentication/Provider/NeosBackendLdapProvider.php b/Classes/Security/Authentication/Provider/NeosBackendLdapProvider.php new file mode 100644 index 0000000..cf26bb4 --- /dev/null +++ b/Classes/Security/Authentication/Provider/NeosBackendLdapProvider.php @@ -0,0 +1,48 @@ +createUser( + $credentials['username'], + '', + $credentials['username'], + $credentials['username'], + [], + $this->name + ); + + return $user->getAccounts()->get(0); + } + +} diff --git a/Classes/Service/BindProvider/AbstractBindProvider.php b/Classes/Service/BindProvider/AbstractBindProvider.php deleted file mode 100644 index a7ce481..0000000 --- a/Classes/Service/BindProvider/AbstractBindProvider.php +++ /dev/null @@ -1,116 +0,0 @@ -linkIdentifier = $linkIdentifier; - $this->options = $options; - } - - /** - * Return the ldap connection identifier. - * - * @return resource - */ - public function getLinkIdentifier() - { - return $this->linkIdentifier; - } - - /** - * Return the filtered username for directory search. - * - * @param string $username - * @return string - */ - public function filterUsername($username) - { - return $username; - } - - /** - * Bind to the directory server. Returns void but throws exception on failure. - * - * @param string $userDn The DN of the user. - * @param string $password The user's password. - * @throws \Exception - */ - protected function bindWithDn($userDn, $password) - { - try { - $bindIsSuccessful = ldap_bind($this->linkIdentifier, $userDn, $password); - } catch (\Exception $exception) { - $bindIsSuccessful = false; - } - - if (!$bindIsSuccessful) { - throw new \Exception('Failed to bind with DN: "' . $userDn . '"', 1327763970); - } - } - - /** - * Bind anonymously to the directory server. Returns void but throws exception on failure. - * - * @throws \Exception - */ - protected function bindAnonymously() - { - try { - $bindIsSuccessful = ldap_bind($this->linkIdentifier); - } catch (\Exception $exception) { - $bindIsSuccessful = false; - } - - if (!$bindIsSuccessful) { - throw new \Exception('Failed to bind anonymously', 1327763970); - } - } - - /** - * Verify the given user is known to the directory server and has valid credentials. - * Does not return output but throws an exception if the credentials are invalid. - * - * @param string $dn The DN of the user. - * @param string $password The user's password. - * @throws \Exception - */ - public function verifyCredentials($dn, $password) - { - $this->bindWithDn($dn, $password); - } - -} diff --git a/Classes/Service/BindProvider/ActiveDirectoryBind.php b/Classes/Service/BindProvider/ActiveDirectoryBind.php deleted file mode 100644 index 088c4fa..0000000 --- a/Classes/Service/BindProvider/ActiveDirectoryBind.php +++ /dev/null @@ -1,65 +0,0 @@ -options['domain'])) { - if (!strpos($username, '\\')) { - $username = $this->options['domain'] . '\\' . $username; - } - } - - if (!empty($this->options['usernameSuffix'])) { - if (!strpos($username, '@')) { - $username = $username . '@' . $this->options['usernameSuffix']; - } - } - - $this->bindWithDn($username, $password); - } - - /** - * Return username in format used for directory search - * - * @param string $username - * @return string - */ - public function filterUsername($username) - { - if (!empty($this->options['domain'])) { - $usernameWithoutDomain = array_pop(explode('\\', $username)); - $username = $this->options['filter']['ignoreDomain'] ? $usernameWithoutDomain : addcslashes($username, '\\'); - } - return $username; - } - -} - diff --git a/Classes/Service/BindProvider/BindProviderInterface.php b/Classes/Service/BindProvider/BindProviderInterface.php deleted file mode 100644 index 5a9021d..0000000 --- a/Classes/Service/BindProvider/BindProviderInterface.php +++ /dev/null @@ -1,55 +0,0 @@ -options, 'bind.dn'); - if (!empty($username) && !empty($password)) { - // if credentials are given, use them to authenticate - $this->bindWithDn(sprintf($bindDn, $username), $password); - return; - } - - $bindPassword = Arrays::getValueByPath($this->options, 'bind.password'); - if (!empty($bindPassword)) { - // if the settings specify a bind password, we are safe to assume no anonymous authentication is needed - $this->bindWithDn($bindDn, $bindPassword); - } - - $anonymousBind = Arrays::getValueByPath($this->options, 'bind.anonymous'); - if ($anonymousBind === true) { - // if allowed, bind without username or password - $this->bindAnonymously(); - } - } - -} diff --git a/Classes/Service/DirectoryService.php b/Classes/Service/DirectoryService.php index 4348197..dea7ebe 100644 --- a/Classes/Service/DirectoryService.php +++ b/Classes/Service/DirectoryService.php @@ -13,10 +13,8 @@ use Neos\Flow\Annotations as Flow; use Neos\Flow\Error\Exception; -use Neos\Ldap\Service\BindProvider\ActiveDirectoryBind; -use Neos\Ldap\Service\BindProvider\LdapBind; -use Neos\Utility\Arrays; -use Neos\Ldap\Service\BindProvider\BindProviderInterface; +use Symfony\Component\Ldap\Adapter\ExtLdap\Adapter; +use Symfony\Component\Ldap\Ldap; /** * A simple Ldap authentication service @@ -36,9 +34,14 @@ class DirectoryService protected $options; /** - * @var \Neos\Ldap\Service\BindProvider\BindProviderInterface + * @var Ldap */ - protected $bindProvider; + protected $connection; + + /** + * @var Ldap + */ + protected $readConnection; /** * @param string $name @@ -49,70 +52,51 @@ public function __construct($name, array $options) { $this->name = $name; $this->options = $options; - } - /** - * @return resource - */ - public function getConnection() - { $this->ldapConnect(); - return $this->bindProvider->getLinkIdentifier(); } /** - * Initialize the Ldap server connection - * - * Connect to the server and set communication options. Further bindings will be done - * by a server specific bind provider. - * - * @throws Exception + * @return Ldap */ - public function ldapConnect() + public function getReadConnection() { - if ($this->bindProvider instanceof BindProviderInterface) { - // Already connected - return; + if ($this->readConnection) { + return $this->readConnection; } - $bindProviderClass = LdapBind::class; - $connectionType = Arrays::getValueByPath($this->options, 'type'); - if ($connectionType === 'ActiveDirectory') { - $bindProviderClass = ActiveDirectoryBind::class; - } - if (!class_exists($bindProviderClass)) { - throw new Exception("Bind provider '$bindProviderClass' for the service '$this->name' could not be resolved.", 1327756744); + if (!$this->connection) { + $this->ldapConnect(); } - $connection = ldap_connect($this->options['host'], $this->options['port']); - $this->bindProvider = new $bindProviderClass($connection, $this->options); + $this->readConnection = clone $this->connection; + if (!empty($this->options['connection']['bind'])) { + $this->readConnection->bind( + $this->options['connection']['bind']['dn'], + $this->options['connection']['bind']['password'] + ); + } - $this->setLdapOptions(); + return $this->readConnection; } /** - * Set the Ldap options configured in the settings. - * - * Loops over the ldapOptions array, and finds the corresponding Ldap option by prefixing - * LDAP_OPT_ to the uppercased array key. + * Initialize the Ldap server connection * - * Example: - * protocol_version: 3 - * Becomes: - * LDAP_OPT_PROTOCOL_VERSION 3 + * Connect to the server and set communication options. Further bindings will be done + * by a server specific bind provider. * - * @return void + * @throws Exception */ - protected function setLdapOptions() + protected function ldapConnect() { - if (!isset($this->options['ldapOptions']) || !is_array($this->options['ldapOptions'])) { - return; + if ($this->connection) { + return $this->connection; } - foreach ($this->options['ldapOptions'] as $ldapOption => $value) { - $constantName = 'LDAP_OPT_' . strtoupper($ldapOption); - ldap_set_option($this->bindProvider->getLinkIdentifier(), constant($constantName), $value); - } + $adapter = new Adapter($this->options['connection']['options']); + $this->connection = new Ldap($adapter); + return $this->connection; } /** @@ -125,36 +109,24 @@ protected function setLdapOptions() */ public function authenticate($username, $password) { - $this->bind($username, $password); - - $searchResult = @ldap_search( - $this->bindProvider->getLinkIdentifier(), - $this->options['baseDn'], - sprintf($this->options['filter']['account'], $this->bindProvider->filterUsername($username)) - ); + $result = $this->getReadConnection() + ->query( + $this->options['baseDn'], + sprintf($this->options['query']['account'], $username), + ['filter' => $this->options['filter']['account']] + ) + ->execute() + ->toArray(); - if (!$searchResult) { - throw new Exception('Error during Ldap user search: ' . ldap_errno($this->bindProvider->getLinkIdentifier()), 1443798372); + if (!isset($result[0])) { + throw new \Exception('User not found'); } - $entries = ldap_get_entries($this->bindProvider->getLinkIdentifier(), $searchResult); - if (empty($entries) || !isset($entries[0])) { - throw new Exception('Error while authenticating: authenticated user could not be fetched from the directory', 1488289104); - } + $this->connection->bind($result[0]->getDn(), $password); - return $entries[0]; - } + $userData = ['dn' => $result[0]->getDn()]; - /** - * @param string|null $username - * @param string|null $password - * @return void - * @throws Exception - */ - public function bind($username = null, $password = null) - { - $this->ldapConnect(); - $this->bindProvider->bind($username, $password); + return $userData; } /** @@ -164,23 +136,20 @@ public function bind($username = null, $password = null) */ public function getMemberOf($dn) { - $searchResult = @ldap_search( - $this->bindProvider->getLinkIdentifier(), - $this->options['baseDn'], - sprintf($this->options['filter']['memberOf'], $dn) - ); - - if (!$searchResult) { - throw new Exception('Error during Ldap group search: ' . ldap_errno($this->bindProvider->getLinkIdentifier()), 1443476083); + try { + $searchResult = $this->getReadConnection() + ->query( + $this->options['baseDn'], + sprintf($this->options['query']['memberOf'], $dn), + ['filter' => $this->options['filter']['group']] + ) + ->execute() + ->toArray(); + } catch (\Exception $exception) { + throw new Exception('Error during Ldap group search: ' . $exception->getMessage(), 1443476083); } - return array_map( - function (array $memberOf) { return $memberOf['dn']; }, - array_filter( - ldap_get_entries($this->bindProvider->getLinkIdentifier(), $searchResult), - function ($element) { return is_array($element); } - ) - ); + return $searchResult; } } diff --git a/Configuration/Settings.yaml.ldap.example b/Configuration/Settings.yaml.ldap.example index 12641eb..6ac9cff 100644 --- a/Configuration/Settings.yaml.ldap.example +++ b/Configuration/Settings.yaml.ldap.example @@ -6,36 +6,36 @@ Neos: LdapProvider: provider: Neos\Ldap\Security\Authentication\Provider\LdapProvider providerOptions: - host: localhost - port: 389 + connection: + options: + host: localhost + port: 389 + options: + # All PHP Ldap options can be set here. Make the constant lowercase + # and remove the ldap_opt_ prefix. + # Example: LDAP_OPT_PROTOCOL_VERSION becomes protocol_version + protocol_version: 3 + network_timeout: 10 - baseDn: dc=my-domain,dc=com - - # How to authenticate towards the server. Normally this is a given - # service account and password. Other options are also available, - # consult the bind provider class LdapBind for more examples. - bind: - dn: 'uid=ldapserviceuser,dc=example,dc=com' - password: 'secret' - anonymous: FALSE + # How to authenticate towards the server. Normally this is a given + # service account and password. Other options are also available, + # consult the bind provider class LdapBind for more examples. + bind: + dn: 'uid=ldapserviceuser,dc=example,dc=com' + password: 'secret' - # All PHP Ldap options can be set here. Make the constant lowercase - # and remove the ldap_opt_ prefix. - # Example: LDAP_OPT_PROTOCOL_VERSION becomes protocol_version - ldapOptions: - protocol_version: 3 - network_timeout: 10 + baseDn: dc=my-domain,dc=com + # Define what properties are really fetched from the directory filter: + account: ['dn'] + group: ['dn'] + + query: # %s will be replaced with the username / dn provided account: '(uid=%s)' memberOf: '(&(objectClass=posixGroup)(memberUid=%s))' - # this will use the filter with domain, set it to yes to remove it for search - ignoreDomain: TRUE - - # will be prefixed to a given username if no other domain was specified - domain: 'MY-DOMAIN' Neos: Ldap: diff --git a/composer.json b/composer.json index 7550876..1e3f9fa 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,8 @@ ], "require": { "neos/flow": "^4.0", - "ext-ldap": "*" + "ext-ldap": "*", + "symfony/ldap": "^3.3" }, "replace": { "typo3/ldap": "self.version" @@ -18,6 +19,9 @@ } }, "extra": { + "neos": { + "package-key": "Neos.Ldap" + }, "branch-alias": { "dev-master": "1.0.x-dev" } From 766023ddcb89488a5dfc7337026c99f2bac6cdf4 Mon Sep 17 00:00:00 2001 From: Rens Admiraal Date: Thu, 16 Nov 2017 00:05:05 +0100 Subject: [PATCH 3/9] TASK: Add AD support --- .../Authentication/Provider/LdapProvider.php | 4 +- .../Provider/NeosBackendLdapProvider.php | 35 ++++++++++++-- Classes/Service/DirectoryService.php | 33 +++++++++---- Configuration/Settings.yaml.ad.example | 48 +++++++++---------- Configuration/Settings.yaml.ldap.example | 26 ++++------ 5 files changed, 89 insertions(+), 57 deletions(-) diff --git a/Classes/Security/Authentication/Provider/LdapProvider.php b/Classes/Security/Authentication/Provider/LdapProvider.php index 98e4772..9397871 100644 --- a/Classes/Security/Authentication/Provider/LdapProvider.php +++ b/Classes/Security/Authentication/Provider/LdapProvider.php @@ -89,7 +89,7 @@ public function authenticate(TokenInterface $authenticationToken) // Retrieve or create account for the credentials $account = $this->accountRepository->findActiveByAccountIdentifierAndAuthenticationProviderName($credentials['username'], $this->name); if ($account === null) { - $account = $this->createAccountForCredentials($credentials); + $account = $this->createAccount($credentials, $ldapUser); $this->emitAccountCreated($account, $ldapUser); } @@ -115,7 +115,7 @@ public function authenticate(TokenInterface $authenticationToken) * @param array $credentials array containing username and password * @return Account */ - protected function createAccountForCredentials(array $credentials) + protected function createAccount(array $credentials, array $ldapSearchResult) { $account = new Account(); $account->setAccountIdentifier($credentials['username']); diff --git a/Classes/Security/Authentication/Provider/NeosBackendLdapProvider.php b/Classes/Security/Authentication/Provider/NeosBackendLdapProvider.php index cf26bb4..85a9f0d 100644 --- a/Classes/Security/Authentication/Provider/NeosBackendLdapProvider.php +++ b/Classes/Security/Authentication/Provider/NeosBackendLdapProvider.php @@ -11,8 +11,11 @@ * source code. */ +use Neos\Eel\CompilingEvaluator; +use Neos\Eel\Context; use Neos\Flow\Annotations as Flow; use Neos\Flow\Security\Account; +use Neos\Utility\Arrays; /** * Ldap Authentication provider @@ -22,22 +25,48 @@ class NeosBackendLdapProvider extends LdapProvider { + /** + * @var CompilingEvaluator + * @Flow\Inject + */ + protected $eelEvaluator; + /** * Create a new account for the given credentials. Return null if you * do not want to create a new account, that is, only authenticate * existing accounts from the database and fail on new logins. * * @param array $credentials array containing username and password + * @param array $ldapSearchResult * @return Account */ - protected function createAccountForCredentials(array $credentials) + protected function createAccount(array $credentials, array $ldapSearchResult) { + $mapping = Arrays::arrayMergeRecursiveOverrule( + [ + 'firstName' => 'user.givenName[0]', + 'lastName' => 'user.sn[0]' + ], + isset($this->options['mapping']) ? $this->options['mapping'] : [] + ); + + $eelContext = new Context(['user' => $ldapSearchResult]); + + try { + $firstName = $this->eelEvaluator->evaluate($mapping['firstName'], $eelContext); + $lastName = $this->eelEvaluator->evaluate($mapping['lastName'], $eelContext); + } catch (\Exception $exception) { + // todo : add logging + $firstName = 'none'; + $lastName = 'none'; + } + $userService = new \Neos\Neos\Domain\Service\UserService(); $user = $userService->createUser( $credentials['username'], '', - $credentials['username'], - $credentials['username'], + $firstName, + $lastName, [], $this->name ); diff --git a/Classes/Service/DirectoryService.php b/Classes/Service/DirectoryService.php index dea7ebe..ff97a3e 100644 --- a/Classes/Service/DirectoryService.php +++ b/Classes/Service/DirectoryService.php @@ -13,6 +13,7 @@ use Neos\Flow\Annotations as Flow; use Neos\Flow\Error\Exception; +use Neos\Utility\Arrays; use Symfony\Component\Ldap\Adapter\ExtLdap\Adapter; use Symfony\Component\Ldap\Ldap; @@ -69,12 +70,16 @@ public function getReadConnection() $this->ldapConnect(); } - $this->readConnection = clone $this->connection; - if (!empty($this->options['connection']['bind'])) { - $this->readConnection->bind( - $this->options['connection']['bind']['dn'], - $this->options['connection']['bind']['password'] - ); + try { + $this->readConnection = clone $this->connection; + if (!empty($this->options['connection']['bind'])) { + $this->readConnection->bind( + $this->options['connection']['bind']['dn'], + $this->options['connection']['bind']['password'] + ); + } + } catch (\Exception $exception) { + \Neos\Flow\var_dump($exception, 'bind exception'); } return $this->readConnection; @@ -113,7 +118,7 @@ public function authenticate($username, $password) ->query( $this->options['baseDn'], sprintf($this->options['query']['account'], $username), - ['filter' => $this->options['filter']['account']] + ['filter' => $this->options['attributesFilter']['account']] ) ->execute() ->toArray(); @@ -122,9 +127,17 @@ public function authenticate($username, $password) throw new \Exception('User not found'); } - $this->connection->bind($result[0]->getDn(), $password); + try { + $this->connection->bind($result[0]->getDn(), $password); + } catch (\Exception $exception) { + \Neos\Flow\var_dump($exception); + die(); + } - $userData = ['dn' => $result[0]->getDn()]; + $userData = Arrays::arrayMergeRecursiveOverrule( + $result[0]->getAttributes(), + ['dn' => $result[0]->getDn()] + ); return $userData; } @@ -141,7 +154,7 @@ public function getMemberOf($dn) ->query( $this->options['baseDn'], sprintf($this->options['query']['memberOf'], $dn), - ['filter' => $this->options['filter']['group']] + ['filter' => $this->options['attributesFilter']['group']] ) ->execute() ->toArray(); diff --git a/Configuration/Settings.yaml.ad.example b/Configuration/Settings.yaml.ad.example index 73d7232..f187e16 100644 --- a/Configuration/Settings.yaml.ad.example +++ b/Configuration/Settings.yaml.ad.example @@ -3,48 +3,46 @@ Neos: security: authentication: providers: - ActiveDirectoryProvider: - provider: Neos\Ldap\Security\Authentication\Provider\LdapProvider + 'Neos.Neos:Backend': + provider: Neos\Ldap\Security\Authentication\Provider\NeosBackendLdapProvider providerOptions: - host: localhost - port: 389 - baseDn: dc=my-domain,dc=com + connection: + options: + host: localhost + port: 389 + options: + protocol_version: 3 + network_timeout: 10 + referrals: 0 + bind: + dn: 'username@domain.com' + #dn: 'DOMAIN\user' + password: 'secret' - type: 'ActiveDirectory' + baseDn: dc=my-domain,dc=com - # All PHP Ldap options can be set here. Make the constant lowercase - # and remove the ldap_opt_ prefix. - # Example: LDAP_OPT_PROTOCOL_VERSION becomes protocol_version - ldapOptions: - protocol_version: 3 - network_timeout: 10 - referrals: 0 + attributesFilter: + account: ['dn'] + group: ['dn'] - filter: - # %s will be replaced with the username / dn provided + query: account: '(samaccountname=%s)' - memberOf: '(&(member=%s)(objectClass=group))' - - # this will use the filter with domain, set it to yes to remove it for search - ignoreDomain: FALSE - - # will be prefixed to a given username if no other domain was specified - domain: 'MY-DOMAIN' + memberOf: '(member:1.2.840.113556.1.4.1941:=%s)' Neos: Ldap: roles: - default: [] + default: - 'Neos.Neos:RestrictedEditor' # map group memberships to roles - groupMapping: [] + groupMapping: 'Neos.Neos:Administrator': - 'CN=Administrators,OU=Groups,DC=domain,DC=tld' 'Neos.Neos:Editor': - 'CN=Editors,OU=Groups,DC=domain,DC=tld' # map certain users to roles - userMapping: [] + userMapping: 'Neos.Neos:Administrator': - 'CN=Admin,OU=Users,DC=domain,DC=tld' 'Neos.Neos:Editor': diff --git a/Configuration/Settings.yaml.ldap.example b/Configuration/Settings.yaml.ldap.example index 6ac9cff..ae65d28 100644 --- a/Configuration/Settings.yaml.ldap.example +++ b/Configuration/Settings.yaml.ldap.example @@ -1,55 +1,47 @@ +--- + Neos: Flow: security: authentication: providers: - LdapProvider: - provider: Neos\Ldap\Security\Authentication\Provider\LdapProvider + 'Neos.Neos:Backend': + provider: Neos\Ldap\Security\Authentication\Provider\NeosBackendLdapProvider providerOptions: + connection: options: host: localhost port: 389 options: - # All PHP Ldap options can be set here. Make the constant lowercase - # and remove the ldap_opt_ prefix. - # Example: LDAP_OPT_PROTOCOL_VERSION becomes protocol_version protocol_version: 3 network_timeout: 10 - - # How to authenticate towards the server. Normally this is a given - # service account and password. Other options are also available, - # consult the bind provider class LdapBind for more examples. bind: dn: 'uid=ldapserviceuser,dc=example,dc=com' password: 'secret' baseDn: dc=my-domain,dc=com - # Define what properties are really fetched from the directory filter: account: ['dn'] group: ['dn'] query: - # %s will be replaced with the username / dn provided account: '(uid=%s)' memberOf: '(&(objectClass=posixGroup)(memberUid=%s))' - Neos: Ldap: roles: - default: [] + default: - 'Neos.Neos:RestrictedEditor' - # map group memberships to roles - groupMapping: [] + groupMapping: 'Neos.Neos:Administrator': - 'CN=Administrators,OU=Groups,DC=domain,DC=tld' 'Neos.Neos:Editor': - 'CN=Editors,OU=Groups,DC=domain,DC=tld' - # map certain users to roles - userMapping: [] + - 'CN=Administrators,OU=Groups,DC=domain,DC=tld' + userMapping: 'Neos.Neos:Administrator': - 'CN=Admin,OU=Users,DC=domain,DC=tld' 'Neos.Neos:Editor': From 67e1123990f2ad8bdff2f463ad7c225718ef580d Mon Sep 17 00:00:00 2001 From: Raffael Comi Date: Fri, 21 Sep 2018 15:42:09 +0200 Subject: [PATCH 4/9] TASK: Reorder code according to code style (alphabetically) --- .gitattributes | 1 + Classes/Command/UtilityCommandController.php | 46 +++---- .../Authentication/Provider/LdapProvider.php | 124 +++++++++--------- .../Provider/NeosBackendLdapProvider.php | 4 +- Classes/Service/DirectoryService.php | 108 +++++++-------- Configuration/Settings.yaml.ad.example | 25 ++-- Configuration/Settings.yaml.ldap.example | 13 +- composer.json | 7 +- 8 files changed, 164 insertions(+), 164 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d3c2122 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.yaml.example linguist-language=YAML diff --git a/Classes/Command/UtilityCommandController.php b/Classes/Command/UtilityCommandController.php index c4534a0..24cda49 100644 --- a/Classes/Command/UtilityCommandController.php +++ b/Classes/Command/UtilityCommandController.php @@ -23,7 +23,6 @@ */ class UtilityCommandController extends CommandController { - /** * @Flow\InjectConfiguration(path="security.authentication.providers", package="Neos.Flow") * @var array @@ -36,53 +35,54 @@ class UtilityCommandController extends CommandController protected $options; /** - * Simple bind command to test if a bind is possible at all + * Try authenticating a user using a DirectoryService that's connected to a directory * - * @param string $username Username to be used while binding - * @param string $password Password to be used while binding + * @param string $username The username to authenticate + * @param string $password The password to use while authenticating * @param string $providerName Name of the authentication provider to use * @param string $settingsFile Path to a yaml file containing the settings to use for testing purposes + * * @return void */ - public function bindCommand($username = null, $password = null, $providerName = null, $settingsFile = null) + public function authenticateCommand($username, $password, $providerName = null, $settingsFile = null) { $directoryService = $this->getDirectoryService($providerName, $settingsFile); try { - if ($username === null && $password === null) { - $result = ldap_bind($directoryService->getConnection()); - $this->outputLine('Anonymous bind attempt %s', [$result === false ? 'failed' : 'succeeded']); - if ($result === false) { - $this->quit(1); - } - } else { - $directoryService->bind($username, $password); - $this->outputLine('Bind successful with user %s, using password is %s', [$username, $password === null ? 'NO' : 'YES']); - } + $directoryService->authenticate($username, $password); + $this->outputLine('Successfully authenticated %s with given password', [$username]); } catch (\Exception $exception) { - $this->outputLine('Failed to bind with username %s, using password is %s', [$username, $password === null ? 'NO' : 'YES']); $this->outputLine($exception->getMessage()); $this->quit(1); } } /** - * Try authenticating a user using a DirectoryService that's connected to a directory + * Simple bind command to test if a bind is possible at all * - * @param string $username The username to authenticate - * @param string $password The password to use while authenticating + * @param string $username Username to be used while binding + * @param string $password Password to be used while binding * @param string $providerName Name of the authentication provider to use * @param string $settingsFile Path to a yaml file containing the settings to use for testing purposes * @return void */ - public function authenticateCommand($username, $password, $providerName = null, $settingsFile = null) + public function bindCommand($username = null, $password = null, $providerName = null, $settingsFile = null) { $directoryService = $this->getDirectoryService($providerName, $settingsFile); try { - $directoryService->authenticate($username, $password); - $this->outputLine('Successfully authenticated %s with given password', [$username]); + if ($username === null && $password === null) { + $result = ldap_bind($directoryService->getConnection()); + $this->outputLine('Anonymous bind attempt %s', [$result === false ? 'failed' : 'succeeded']); + if ($result === false) { + $this->quit(1); + } + } else { + $directoryService->bind($username, $password); + $this->outputLine('Bind successful with user %s, using password is %s', [$username, $password === null ? 'NO' : 'YES']); + } } catch (\Exception $exception) { + $this->outputLine('Failed to bind with username %s, using password is %s', [$username, $password === null ? 'NO' : 'YES']); $this->outputLine($exception->getMessage()); $this->quit(1); } @@ -95,7 +95,7 @@ public function authenticateCommand($username, $password, $providerName = null, * @param string $baseDn The base dn to search in * @param string $providerName Name of the authentication provider to use * @param string $settingsFile Path to a yaml file containing the settings to use for testing purposes - * @param string $displayColumns Comma separated list of columns to show, like: dn,objectclass + * @param string $displayColumns Comma separated list of columns to show, like: dn,objectclass * @return void */ public function queryCommand( diff --git a/Classes/Security/Authentication/Provider/LdapProvider.php b/Classes/Security/Authentication/Provider/LdapProvider.php index 9397871..6facb2c 100644 --- a/Classes/Security/Authentication/Provider/LdapProvider.php +++ b/Classes/Security/Authentication/Provider/LdapProvider.php @@ -28,29 +28,28 @@ */ class LdapProvider extends PersistedUsernamePasswordProvider { - /** - * @Flow\InjectConfiguration(path="roles", package="Neos.Ldap") - * @var array + * @var DirectoryService */ - protected $rolesConfiguration; + protected $directoryService; /** * @Flow\Inject - * @var PolicyService + * @var SecurityLoggerInterface */ - protected $policyService; + protected $logger; /** - * @var DirectoryService + * @Flow\Inject + * @var PolicyService */ - protected $directoryService; + protected $policyService; /** - * @Flow\Inject - * @var SecurityLoggerInterface + * @Flow\InjectConfiguration(path="roles", package="Neos.Ldap") + * @var array */ - protected $logger; + protected $rolesConfiguration; /** * @param string $name The name of this authentication provider @@ -68,8 +67,8 @@ public function __construct($name, array $options) * cached on the last successful login for the user to authenticate. * * @param TokenInterface $authenticationToken The token to be authenticated - * @throws UnsupportedAuthenticationTokenException * @return void + * @throws UnsupportedAuthenticationTokenException */ public function authenticate(TokenInterface $authenticationToken) { @@ -107,6 +106,36 @@ public function authenticate(TokenInterface $authenticationToken) } } + /** + * @Flow\Signal + * @param Account $account + * @param array $ldapSearchResult + * @return void + */ + public function emitAccountAuthenticated(Account $account, array $ldapSearchResult) + { + } + + /** + * @Flow\Signal + * @param Account $account + * @param array $ldapSearchResult + * @return void + */ + public function emitAccountCreated(Account $account, array $ldapSearchResult) + { + } + + /** + * @Flow\Signal + * @param Account $account + * @param array $ldapSearchResult + * @return void + */ + public function emitRolesSet(Account $account, array $ldapSearchResult) + { + } + /** * Create a new account for the given credentials. Return null if you * do not want to create a new account, that is, only authenticate @@ -125,26 +154,9 @@ protected function createAccount(array $credentials, array $ldapSearchResult) } /** - * Sets the roles for the Ldap account. - * Extend this Provider class and implement this method to update the party - * * @param Account $account - * @param array $ldapSearchResult * @return void */ - protected function setRoles(Account $account, array $ldapSearchResult) - { - $this->resetRoles($account); - $this->setDefaultRoles($account); - $this->setRolesMappedToUserDn($account, $ldapSearchResult); - $this->setRolesBasedOnGroupMembership($account, $ldapSearchResult); - - $this->accountRepository->update($account); - } - - /** - * @param Account $account - */ protected function resetRoles(Account $account) { $account->setRoles([]); @@ -154,6 +166,7 @@ protected function resetRoles(Account $account) * Set all default roles * * @param Account $account + * @return void */ protected function setDefaultRoles(Account $account) { @@ -167,22 +180,21 @@ protected function setDefaultRoles(Account $account) } /** - * Map configured roles based on user dn + * Sets the roles for the Ldap account. + * Extend this Provider class and implement this method to update the party * * @param Account $account * @param array $ldapSearchResult + * @return void */ - protected function setRolesMappedToUserDn(Account $account, array $ldapSearchResult) + protected function setRoles(Account $account, array $ldapSearchResult) { - if (!is_array($this->rolesConfiguration['userMapping'])) { - return; - } + $this->resetRoles($account); + $this->setDefaultRoles($account); + $this->setRolesMappedToUserDn($account, $ldapSearchResult); + $this->setRolesBasedOnGroupMembership($account, $ldapSearchResult); - foreach ($this->rolesConfiguration['userMapping'] as $roleIdentifier => $userDns) { - if (in_array($ldapSearchResult['dn'], $userDns)) { - $account->addRole($this->policyService->getRole($roleIdentifier)); - } - } + $this->accountRepository->update($account); } /** @@ -190,6 +202,7 @@ protected function setRolesMappedToUserDn(Account $account, array $ldapSearchRes * * @param Account $account * @param array $ldapSearchResult + * @return void */ protected function setRolesBasedOnGroupMembership(Account $account, array $ldapSearchResult) { @@ -206,33 +219,22 @@ protected function setRolesBasedOnGroupMembership(Account $account, array $ldapS } /** + * Map configured roles based on user dn + * * @param Account $account * @param array $ldapSearchResult * @return void - * @Flow\Signal - */ - public function emitAccountCreated(Account $account, array $ldapSearchResult) - { - } - - /** - * @param Account $account - * @param array $ldapSearchResult - * @return void - * @Flow\Signal */ - public function emitAccountAuthenticated(Account $account, array $ldapSearchResult) + protected function setRolesMappedToUserDn(Account $account, array $ldapSearchResult) { - } + if (!is_array($this->rolesConfiguration['userMapping'])) { + return; + } - /** - * @param Account $account - * @param array $ldapSearchResult - * @return void - * @Flow\Signal - */ - public function emitRolesSet(Account $account, array $ldapSearchResult) - { + foreach ($this->rolesConfiguration['userMapping'] as $roleIdentifier => $userDns) { + if (in_array($ldapSearchResult['dn'], $userDns)) { + $account->addRole($this->policyService->getRole($roleIdentifier)); + } + } } - } diff --git a/Classes/Security/Authentication/Provider/NeosBackendLdapProvider.php b/Classes/Security/Authentication/Provider/NeosBackendLdapProvider.php index 85a9f0d..9031492 100644 --- a/Classes/Security/Authentication/Provider/NeosBackendLdapProvider.php +++ b/Classes/Security/Authentication/Provider/NeosBackendLdapProvider.php @@ -24,10 +24,9 @@ */ class NeosBackendLdapProvider extends LdapProvider { - /** - * @var CompilingEvaluator * @Flow\Inject + * @var CompilingEvaluator */ protected $eelEvaluator; @@ -73,5 +72,4 @@ protected function createAccount(array $credentials, array $ldapSearchResult) return $user->getAccounts()->get(0); } - } diff --git a/Classes/Service/DirectoryService.php b/Classes/Service/DirectoryService.php index ff97a3e..0f87411 100644 --- a/Classes/Service/DirectoryService.php +++ b/Classes/Service/DirectoryService.php @@ -23,6 +23,10 @@ */ class DirectoryService { + /** + * @var Ldap + */ + protected $connection; /** * @var string @@ -34,11 +38,6 @@ class DirectoryService */ protected $options; - /** - * @var Ldap - */ - protected $connection; - /** * @var Ldap */ @@ -57,53 +56,6 @@ public function __construct($name, array $options) $this->ldapConnect(); } - /** - * @return Ldap - */ - public function getReadConnection() - { - if ($this->readConnection) { - return $this->readConnection; - } - - if (!$this->connection) { - $this->ldapConnect(); - } - - try { - $this->readConnection = clone $this->connection; - if (!empty($this->options['connection']['bind'])) { - $this->readConnection->bind( - $this->options['connection']['bind']['dn'], - $this->options['connection']['bind']['password'] - ); - } - } catch (\Exception $exception) { - \Neos\Flow\var_dump($exception, 'bind exception'); - } - - return $this->readConnection; - } - - /** - * Initialize the Ldap server connection - * - * Connect to the server and set communication options. Further bindings will be done - * by a server specific bind provider. - * - * @throws Exception - */ - protected function ldapConnect() - { - if ($this->connection) { - return $this->connection; - } - - $adapter = new Adapter($this->options['connection']['options']); - $this->connection = new Ldap($adapter); - return $this->connection; - } - /** * Authenticate a username / password against the Ldap server * @@ -143,8 +95,8 @@ public function authenticate($username, $password) } /** - * @param string $dn User or group DN. - * @return array group DN => CN mapping + * @param string $dn User or group DN. + * @return array group DN => CN mapping * @throws Exception */ public function getMemberOf($dn) @@ -165,4 +117,52 @@ public function getMemberOf($dn) return $searchResult; } + /** + * @return Ldap + */ + public function getReadConnection() + { + if ($this->readConnection) { + return $this->readConnection; + } + + if (!$this->connection) { + $this->ldapConnect(); + } + + try { + $this->readConnection = clone $this->connection; + if (!empty($this->options['connection']['bind'])) { + $this->readConnection->bind( + $this->options['connection']['bind']['dn'], + $this->options['connection']['bind']['password'] + ); + } + } catch (\Exception $exception) { + \Neos\Flow\var_dump($exception, 'bind exception'); + } + + return $this->readConnection; + } + + /** + * Initialize the Ldap server connection + * + * Connect to the server and set communication options. Further bindings will be done + * by a server specific bind provider. + * + * @return Ldap + * @throws Exception + */ + protected function ldapConnect() + { + if ($this->connection) { + return $this->connection; + } + + $adapter = new Adapter($this->options['connection']['options']); + $this->connection = new Ldap($adapter); + + return $this->connection; + } } diff --git a/Configuration/Settings.yaml.ad.example b/Configuration/Settings.yaml.ad.example index f187e16..a4e37ef 100644 --- a/Configuration/Settings.yaml.ad.example +++ b/Configuration/Settings.yaml.ad.example @@ -1,3 +1,5 @@ +--- + Neos: Flow: security: @@ -6,31 +8,30 @@ Neos: 'Neos.Neos:Backend': provider: Neos\Ldap\Security\Authentication\Provider\NeosBackendLdapProvider providerOptions: + attributesFilter: + account: ['dn'] + group: ['dn'] + + baseDn: dc=my-domain,dc=com connection: + bind: + dn: 'username@domain.com' + #dn: 'DOMAIN\user' + password: 'secret' + options: host: localhost port: 389 options: - protocol_version: 3 network_timeout: 10 + protocol_version: 3 referrals: 0 - bind: - dn: 'username@domain.com' - #dn: 'DOMAIN\user' - password: 'secret' - - baseDn: dc=my-domain,dc=com - - attributesFilter: - account: ['dn'] - group: ['dn'] query: account: '(samaccountname=%s)' memberOf: '(member:1.2.840.113556.1.4.1941:=%s)' -Neos: Ldap: roles: default: diff --git a/Configuration/Settings.yaml.ldap.example b/Configuration/Settings.yaml.ldap.example index ae65d28..0d8e658 100644 --- a/Configuration/Settings.yaml.ldap.example +++ b/Configuration/Settings.yaml.ldap.example @@ -8,19 +8,19 @@ Neos: 'Neos.Neos:Backend': provider: Neos\Ldap\Security\Authentication\Provider\NeosBackendLdapProvider providerOptions: + baseDn: dc=my-domain,dc=com connection: + bind: + dn: 'uid=ldapserviceuser,dc=example,dc=com' + password: 'secret' + options: host: localhost port: 389 options: - protocol_version: 3 network_timeout: 10 - bind: - dn: 'uid=ldapserviceuser,dc=example,dc=com' - password: 'secret' - - baseDn: dc=my-domain,dc=com + protocol_version: 3 filter: account: ['dn'] @@ -30,7 +30,6 @@ Neos: account: '(uid=%s)' memberOf: '(&(objectClass=posixGroup)(memberUid=%s))' -Neos: Ldap: roles: default: diff --git a/composer.json b/composer.json index 1e3f9fa..3537aeb 100644 --- a/composer.json +++ b/composer.json @@ -2,12 +2,11 @@ "name": "neos/ldap", "type": "neos-package", "description": "Ldap Authentication for Flow", - "license": [ - "MIT" - ], + "license": "MIT", "require": { - "neos/flow": "^4.0", "ext-ldap": "*", + + "neos/flow": "^4.0", "symfony/ldap": "^3.3" }, "replace": { From b1da8744f89361137c089ab06d1ae02371e212d8 Mon Sep 17 00:00:00 2001 From: Raffael Comi Date: Tue, 18 Sep 2018 14:56:23 +0200 Subject: [PATCH 5/9] TASK: Further work on refactoring --- Classes/Command/UtilityCommandController.php | 271 ++++++++---------- .../Authentication/Provider/LdapProvider.php | 268 ++++++++++++----- .../Provider/NeosBackendLdapProvider.php | 36 +-- Classes/Service/DirectoryService.php | 180 ++++++------ Configuration/Settings.yaml | 1 + Configuration/Settings.yaml.ad.example | 50 ---- Configuration/Settings.yaml.example | 97 +++++++ Configuration/Settings.yaml.ldap.example | 47 --- Readme.md | 105 +++++++ Readme.rst | 96 ------- ...urity.authentication.providers.schema.yaml | 199 +++++++++++++ .../Settings.Neos.Ldap.roles.schema.yaml | 29 ++ composer.json | 11 +- 13 files changed, 861 insertions(+), 529 deletions(-) delete mode 100644 Configuration/Settings.yaml.ad.example create mode 100644 Configuration/Settings.yaml.example delete mode 100644 Configuration/Settings.yaml.ldap.example create mode 100644 Readme.md delete mode 100644 Readme.rst create mode 100644 Resources/Private/Schema/Settings.Neos.Flow.security.authentication.providers.schema.yaml create mode 100644 Resources/Private/Schema/Settings.Neos.Ldap.roles.schema.yaml diff --git a/Classes/Command/UtilityCommandController.php b/Classes/Command/UtilityCommandController.php index 24cda49..5490f1a 100644 --- a/Classes/Command/UtilityCommandController.php +++ b/Classes/Command/UtilityCommandController.php @@ -11,12 +11,17 @@ * source code. */ -use Symfony\Component\Yaml\Yaml; use Neos\Flow\Annotations as Flow; use Neos\Flow\Cli\CommandController; +use Neos\Flow\Mvc\Exception\StopActionException; +use Neos\Flow\Security\Exception\MissingConfigurationException; +use Neos\Ldap\Service\DirectoryService; use Neos\Utility\Arrays; use Neos\Utility\Files; -use Neos\Ldap\Service\DirectoryService; +use Symfony\Component\Ldap\Entry; +use Symfony\Component\Ldap\Exception\ConnectionException; +use Symfony\Component\Yaml\Exception\ParseException; +use Symfony\Component\Yaml\Yaml; /** * Command controller to test settings and query the directory @@ -25,208 +30,180 @@ class UtilityCommandController extends CommandController { /** * @Flow\InjectConfiguration(path="security.authentication.providers", package="Neos.Flow") - * @var array + * @var mixed[][] */ protected $authenticationProvidersConfiguration; - /** - * @var array - */ - protected $options; - - /** - * Try authenticating a user using a DirectoryService that's connected to a directory - * - * @param string $username The username to authenticate - * @param string $password The password to use while authenticating - * @param string $providerName Name of the authentication provider to use - * @param string $settingsFile Path to a yaml file containing the settings to use for testing purposes - * - * @return void - */ - public function authenticateCommand($username, $password, $providerName = null, $settingsFile = null) - { - $directoryService = $this->getDirectoryService($providerName, $settingsFile); - - try { - $directoryService->authenticate($username, $password); - $this->outputLine('Successfully authenticated %s with given password', [$username]); - } catch (\Exception $exception) { - $this->outputLine($exception->getMessage()); - $this->quit(1); - } - } - /** * Simple bind command to test if a bind is possible at all * - * @param string $username Username to be used while binding - * @param string $password Password to be used while binding - * @param string $providerName Name of the authentication provider to use - * @param string $settingsFile Path to a yaml file containing the settings to use for testing purposes + * @param string|null $username Username to be used while binding + * @param string|null $password Password to be used while binding + * @param string|null $providerName Name of the authentication provider to use + * @param string|null $settingsFile Path to a yaml file containing the settings to use for testing purposes * @return void + * @throws StopActionException */ - public function bindCommand($username = null, $password = null, $providerName = null, $settingsFile = null) - { - $directoryService = $this->getDirectoryService($providerName, $settingsFile); - + public function bindCommand( + string $username = null, + string $password = null, + string $providerName = null, + string $settingsFile = null + ) { + $options = $this->getOptions($providerName, $settingsFile); + $bindDn = isset($options['bind']['dn']) + ? sprintf($options['bind']['dn'], $username ?? '') + : null + ; + $message = 'Attempt to bind ' . ($bindDn === null ? 'anonymously' : 'to ' . $bindDn); + if ($password !== null) { + $message .= ', using password,'; + } try { - if ($username === null && $password === null) { - $result = ldap_bind($directoryService->getConnection()); - $this->outputLine('Anonymous bind attempt %s', [$result === false ? 'failed' : 'succeeded']); - if ($result === false) { - $this->quit(1); - } - } else { - $directoryService->bind($username, $password); - $this->outputLine('Bind successful with user %s, using password is %s', [$username, $password === null ? 'NO' : 'YES']); - } - } catch (\Exception $exception) { - $this->outputLine('Failed to bind with username %s, using password is %s', [$username, $password === null ? 'NO' : 'YES']); + new DirectoryService($options, $username, $password); + $this->outputLine($message . ' succeeded'); + } catch (ConnectionException $exception) { + $this->outputLine($message . ' failed'); $this->outputLine($exception->getMessage()); $this->quit(1); + // quit always throws StopActionException, so we cannot get here + return; } } /** * Query the directory * - * @param string $query The query to use, for example (objectclass=*) * @param string $baseDn The base dn to search in - * @param string $providerName Name of the authentication provider to use - * @param string $settingsFile Path to a yaml file containing the settings to use for testing purposes - * @param string $displayColumns Comma separated list of columns to show, like: dn,objectclass + * @param string $query The query to use, for example "(objectclass=*)" + * @param string|null $providerName Name of the authentication provider to use + * @param string|null $settingsFile Path to a yaml file containing the settings to use for testing purposes + * @param string|null $displayColumns Comma separated list of columns to show, like "cn,objectclass" + * @param string|null $username Username to be used to bind + * @param string|null $password Password to be used to bind + * * @return void + * @throws StopActionException */ public function queryCommand( - $query, - $baseDn = null, - $providerName = null, - $settingsFile = null, - $displayColumns = 'dn' + string $baseDn, + string $query, + string $providerName = null, + string $settingsFile = null, + string $displayColumns = null, + string $username = null, + string $password = null ) { - $directoryService = $this->getDirectoryService($providerName, $settingsFile); - - if ($baseDn === null) { - $baseDn = Arrays::getValueByPath($this->options, 'baseDn'); - } + $options = $this->getOptions($providerName, $settingsFile); - $this->outputLine('Query: %s', [$query]); $this->outputLine('Base DN: %s', [$baseDn]); + $this->outputLine('Query: %s', [$query]); - $searchResult = @ldap_search( - $directoryService->getConnection(), - $baseDn, - $query - ); + $columns = $displayColumns === null ? null : Arrays::trimExplode(',', $displayColumns); - if ($searchResult === false) { - $this->outputLine(ldap_error($directoryService->getConnection())); + try { + $directoryService = new DirectoryService($options, $username, $password); + $entries = $directoryService->query($baseDn, $query, $columns); + } catch (MissingConfigurationException $exception) { + // We check for baseDn above, so this will never be thrown + /** @var Entry[] $entries */ + } catch (\RuntimeException $exception) { + // line above can be replaced by the following line when we require PHP 7.1 + // } catch (ConnectionException | \Symfony\Component\Ldap\Exception\LdapException $exception) { + $this->outputLine($exception->getMessage()); $this->quit(1); + // quit always throws StopActionException, so we cannot get here + return; } - - $this->outputLdapSearchResultTable($directoryService->getConnection(), $searchResult, $displayColumns); - } - - /** - * @param string $providerName Name of the authentication provider to use - * @param string $settingsFile Path to a yaml file containing the settings to use for testing purposes - * @return DirectoryService - * @throws \Neos\Flow\Mvc\Exception\StopActionException - */ - protected function getDirectoryService($providerName, $settingsFile) - { - $directoryServiceOptions = $this->getOptions($providerName, $settingsFile); - if (!is_array($directoryServiceOptions)) { - $this->outputLine('No configuration found for given providerName / settingsFile'); - $this->quit(3); - } - - return new DirectoryService('cli', $directoryServiceOptions); + $this->outputEntriesTable($entries); } /** * Load options by provider name or by a settings file (first has precedence) * - * @param string $providerName Name of the authentication provider to use - * @param string $settingsFile Path to a yaml file containing the settings to use for testing purposes - * @return array|mixed - * @throws \Neos\Flow\Mvc\Exception\StopActionException + * @param string|null $providerName Name of the authentication provider to use + * @param string|null $settingsFile Path to a yaml file containing the settings to use for testing purposes + * @return mixed[] + * @throws StopActionException */ - protected function getOptions($providerName = null, $settingsFile = null) + protected function getOptions(string $providerName = null, string $settingsFile = null) : array { - if ($providerName !== null && array_key_exists($providerName, $this->authenticationProvidersConfiguration)) { - $this->options = $this->authenticationProvidersConfiguration[$providerName]['providerOptions']; - return $this->options; + if ($providerName !== null) { + if (isset($this->authenticationProvidersConfiguration[$providerName]['providerOptions']) + && \is_array($this->authenticationProvidersConfiguration[$providerName]['providerOptions']) + ) { + return $this->authenticationProvidersConfiguration[$providerName]['providerOptions']; + } + $this->outputLine('No configuration found for given providerName'); + if ($settingsFile === null) { + $this->quit(3); + // quit always throws StopActionException, so we cannot get here + return []; + } } if ($settingsFile !== null) { - if (!file_exists($settingsFile)) { + if (!\file_exists($settingsFile)) { $this->outputLine('Could not find settings file on path %s', [$settingsFile]); $this->quit(1); + // quit always throws StopActionException, so we cannot get here + return []; + } + try { + // Yaml::parseFile() introduced in symfony/yaml 3.4.0 + // When above is required, we can drop dependency on neos/utility-files + $directoryServiceOptions = method_exists(Yaml::class, 'parseFile') + ? Yaml::parseFile($settingsFile) + : Yaml::parse(Files::getFileContents($settingsFile)) + ; + } catch (ParseException $exception) { + $this->outputLine($exception->getMessage()); + $this->quit(3); + // quit always throws StopActionException, so we cannot get here + return []; } - $this->options = Yaml::parse(Files::getFileContents($settingsFile)); - return $this->options; + if (!\is_array($directoryServiceOptions)) { + $this->outputLine('No configuration found in given settingsFile'); + $this->quit(3); + // quit always throws StopActionException, so we cannot get here + return []; + } + return $directoryServiceOptions; } - $this->outputLine('Neither providerName or settingsFile is passed as argument. You need to pass one of those.'); + $this->outputLine( + 'Neither providerName nor settingsFile is passed as argument. You need to pass one of those.' + ); $this->quit(1); + // quit always throws StopActionException, so we cannot get here + return []; } /** - * Outputs a table for given search result + * Outputs a table for given entries * - * @param resource $connection - * @param resource $searchResult - * @param $displayColumns + * @param Entry[] $entries * @return void */ - protected function outputLdapSearchResultTable($connection, $searchResult, $displayColumns) + protected function outputEntriesTable(array $entries) { - $headers = []; + $headers = ['dn']; $rows = []; - $displayColumns = Arrays::trimExplode(',', $displayColumns); - - $entries = ldap_get_entries($connection, $searchResult); - $this->outputLine('%s results found', [$entries['count']]); - - foreach ($entries as $index => $ldapSearchResult) { - if ($index === 'count') { - continue; - } - - if ($headers === []) { - foreach ($ldapSearchResult as $propertyName => $propertyValue) { - if (is_integer($propertyName)) { - continue; - } - if ($displayColumns === null || in_array($propertyName, $displayColumns)) { - $headers[] = $propertyName; - } - } - } + $this->outputLine('%s results found', [\count($entries)]); - $row = []; - foreach ($ldapSearchResult as $propertyName => $propertyValue) { - if (is_integer($propertyName)) { - continue; - } - if ($displayColumns !== null && !in_array($propertyName, $displayColumns)) { - continue; + foreach ($entries as $index => $entry) { + $rows[$index] = ['dn' => $entry->getDn()]; + foreach ($entry->getAttributes() as $propertyName => $propertyValue) { + if ($index === 0) { + $headers[] = $propertyName; } - if (isset($propertyValue['count'])) { - unset($propertyValue['count']); - } - - if (is_array($propertyValue)) { - $row[$propertyName] = implode(", ", $propertyValue); - } else { - $row[$propertyName] = $propertyValue; - } + $rows[$index][$propertyName] = \is_array($propertyValue) + ? implode(', ', $propertyValue) + : $propertyValue + ; } - $rows[] = $row; } $this->output->outputTable($rows, $headers); diff --git a/Classes/Security/Authentication/Provider/LdapProvider.php b/Classes/Security/Authentication/Provider/LdapProvider.php index 6facb2c..9d68711 100644 --- a/Classes/Security/Authentication/Provider/LdapProvider.php +++ b/Classes/Security/Authentication/Provider/LdapProvider.php @@ -12,14 +12,18 @@ */ use Neos\Flow\Annotations as Flow; -use Neos\Flow\Log\SecurityLoggerInterface; +use Neos\Flow\Persistence\Exception\IllegalObjectTypeException; use Neos\Flow\Security\Account; use Neos\Flow\Security\Authentication\Provider\PersistedUsernamePasswordProvider; use Neos\Flow\Security\Authentication\Token\UsernamePassword; use Neos\Flow\Security\Authentication\TokenInterface; +use Neos\Flow\Security\Exception\InvalidAuthenticationStatusException; +use Neos\Flow\Security\Exception\MissingConfigurationException; +use Neos\Flow\Security\Exception\NoSuchRoleException; use Neos\Flow\Security\Exception\UnsupportedAuthenticationTokenException; use Neos\Flow\Security\Policy\PolicyService; use Neos\Ldap\Service\DirectoryService; +use Symfony\Component\Ldap\Exception\LdapException; /** * Ldap Authentication provider @@ -29,16 +33,10 @@ class LdapProvider extends PersistedUsernamePasswordProvider { /** - * @var DirectoryService + * @var DirectoryService|null */ protected $directoryService; - /** - * @Flow\Inject - * @var SecurityLoggerInterface - */ - protected $logger; - /** * @Flow\Inject * @var PolicyService @@ -47,109 +45,145 @@ class LdapProvider extends PersistedUsernamePasswordProvider /** * @Flow\InjectConfiguration(path="roles", package="Neos.Ldap") - * @var array + * @var mixed[] */ protected $rolesConfiguration; /** - * @param string $name The name of this authentication provider - * @param array $options Additional configuration options - */ - public function __construct($name, array $options) - { - parent::__construct($name, $options); - $this->directoryService = new DirectoryService($name, $options); - } - - /** - * Authenticate the current token. If it's not possible to connect to the LDAP server the provider - * tries to authenticate against cached credentials in the database that were - * cached on the last successful login for the user to authenticate. + * Authenticate the current token. If it's not possible to connect to the LDAP server the provider tries to + * authenticate against cached credentials in the database that were cached on the last successful login for the + * user to authenticate. * * @param TokenInterface $authenticationToken The token to be authenticated * @return void * @throws UnsupportedAuthenticationTokenException + * @throws MissingConfigurationException */ public function authenticate(TokenInterface $authenticationToken) { if (!($authenticationToken instanceof UsernamePassword)) { - throw new UnsupportedAuthenticationTokenException('This provider cannot authenticate the given token.', 1217339840); + throw new UnsupportedAuthenticationTokenException( + 'This provider cannot authenticate the given token.', + 1217339840 + ); } $credentials = $authenticationToken->getCredentials(); - if (!is_array($credentials) || !isset($credentials['username'])) { - $authenticationToken->setAuthenticationStatus(TokenInterface::NO_CREDENTIALS_GIVEN); + if (!\is_array($credentials) || !isset($credentials['username'], $credentials['password'])) { + try { + $authenticationToken->setAuthenticationStatus(TokenInterface::NO_CREDENTIALS_GIVEN); + } catch (InvalidAuthenticationStatusException $exception) { + // This exception is never thrown + } return; } + // Retrieve account for the credentials + $account = $this->accountRepository->findActiveByAccountIdentifierAndAuthenticationProviderName( + $credentials['username'], + $this->name + ); try { - $ldapUser = $this->directoryService->authenticate($credentials['username'], $credentials['password']); + $this->directoryService = new DirectoryService( + $this->options, + $credentials['username'], + $credentials['password'] + ); + $ldapUserData = $this->directoryService->getUserData($credentials['username']); - // Retrieve or create account for the credentials - $account = $this->accountRepository->findActiveByAccountIdentifierAndAuthenticationProviderName($credentials['username'], $this->name); + // Create account if not existent if ($account === null) { - $account = $this->createAccount($credentials, $ldapUser); - $this->emitAccountCreated($account, $ldapUser); + $account = $this->createAccount($credentials, $ldapUserData); + if ($account === null) { + throw new LdapException('Only existing accounts allowed'); + } + $this->emitAccountCreated($account, $ldapUserData); + } + } catch (\RuntimeException $exception) { + // line above can be replaced by the following line when we require PHP 7.1 + // } catch (\Symfony\Component\Ldap\Exception\ConnectionException | LdapException $exception) { + try { + $authenticationToken->setAuthenticationStatus(TokenInterface::WRONG_CREDENTIALS); + if ($account !== null) { + $account->authenticationAttempted(TokenInterface::WRONG_CREDENTIALS); + $this->accountRepository->update($account); + $this->persistenceManager->whitelistObject($account); + } + } catch (InvalidAuthenticationStatusException $exception) { + // This exception is never thrown + } catch (IllegalObjectTypeException $exception) { + // This exception is never thrown } + return; + } - // Map security roles to account - $this->setRoles($account, $ldapUser); - $this->emitRolesSet($account, $ldapUser); + // Map security roles to account + $this->setRoles($account, $ldapUserData); + $this->emitRolesSet($account, $ldapUserData); - // Mark authentication successful + // Mark authentication successful + try { + $account->authenticationAttempted(TokenInterface::AUTHENTICATION_SUCCESSFUL); + $this->accountRepository->update($account); $authenticationToken->setAuthenticationStatus(TokenInterface::AUTHENTICATION_SUCCESSFUL); - $authenticationToken->setAccount($account); - $this->emitAccountAuthenticated($account, $ldapUser); - } catch (\Exception $exception) { - $this->logger->log('Authentication failed: ' . $exception->getMessage(), LOG_ALERT); - $authenticationToken->setAuthenticationStatus(TokenInterface::WRONG_CREDENTIALS); + } catch (InvalidAuthenticationStatusException $exception) { + // This exception is never thrown + } catch (IllegalObjectTypeException $exception) { + // This exception is never thrown } + $this->persistenceManager->whitelistObject($account); + $authenticationToken->setAccount($account); + $this->emitAccountAuthenticated($account, $ldapUserData); } /** * @Flow\Signal * @param Account $account - * @param array $ldapSearchResult + * @param string[][] $ldapUserData * @return void */ - public function emitAccountAuthenticated(Account $account, array $ldapSearchResult) + public function emitAccountCreated(Account $account, array $ldapUserData) { } /** * @Flow\Signal * @param Account $account - * @param array $ldapSearchResult + * @param string[][] $ldapUserData * @return void */ - public function emitAccountCreated(Account $account, array $ldapSearchResult) + public function emitAccountAuthenticated(Account $account, array $ldapUserData) { } /** * @Flow\Signal * @param Account $account - * @param array $ldapSearchResult + * @param string[][] $ldapUserData * @return void */ - public function emitRolesSet(Account $account, array $ldapSearchResult) + public function emitRolesSet(Account $account, array $ldapUserData) { } /** - * Create a new account for the given credentials. Return null if you - * do not want to create a new account, that is, only authenticate - * existing accounts from the database and fail on new logins. + * Create a new account for the given credentials. Return null if you do not want to create a new account, that is, + * only authenticate existing accounts from the database and fail on new logins. * - * @param array $credentials array containing username and password - * @return Account + * @param string[] $credentials array containing username and password + * @param string[][] $ldapUserData + * @return Account|null */ - protected function createAccount(array $credentials, array $ldapSearchResult) + protected function createAccount(array $credentials, array $ldapUserData) { $account = new Account(); $account->setAccountIdentifier($credentials['username']); $account->setAuthenticationProviderName($this->name); - $this->accountRepository->add($account); + try { + $this->accountRepository->add($account); + } catch (IllegalObjectTypeException $exception) { + // This exception is never thrown + } return $account; } @@ -159,7 +193,11 @@ protected function createAccount(array $credentials, array $ldapSearchResult) */ protected function resetRoles(Account $account) { - $account->setRoles([]); + try { + $account->setRoles([]); + } catch (\InvalidArgumentException $exception) { + // This exception is never thrown + } } /** @@ -170,50 +208,78 @@ protected function resetRoles(Account $account) */ protected function setDefaultRoles(Account $account) { - if (!is_array($this->rolesConfiguration['default'])) { + if (!\is_array($this->rolesConfiguration['default'])) { return; } foreach ($this->rolesConfiguration['default'] as $roleIdentifier) { - $account->addRole($this->policyService->getRole($roleIdentifier)); + try { + $account->addRole($this->policyService->getRole($roleIdentifier)); + } catch (\InvalidArgumentException $exception) { + // This exception is never thrown + } catch (NoSuchRoleException $exception) { + // We ignore invalid roles + // todo: logging + continue; + } } } /** - * Sets the roles for the Ldap account. + * Sets the roles for the Ldap account + * * Extend this Provider class and implement this method to update the party * * @param Account $account - * @param array $ldapSearchResult + * @param string[][] $ldapUserData * @return void */ - protected function setRoles(Account $account, array $ldapSearchResult) + protected function setRoles(Account $account, array $ldapUserData) { $this->resetRoles($account); $this->setDefaultRoles($account); - $this->setRolesMappedToUserDn($account, $ldapSearchResult); - $this->setRolesBasedOnGroupMembership($account, $ldapSearchResult); + $this->setRolesByUserProperties($account, $ldapUserData); + $this->setRolesByUserDn($account, $ldapUserData['dn'][0]); + try { + $this->setRolesByGroupDns($account, $this->directoryService->getGroupDnsOfUser($ldapUserData['dn'][0])); + } catch (\Exception $exception) { + // line above can be replaced by the following line when we require PHP 7.1 + // } catch (MissingConfigurationException | \Symfony\Component\Ldap\Exception\LdapException $exception) { + // If groups cannot be retrieved, they won't get set + // todo: logging + } - $this->accountRepository->update($account); + try { + $this->accountRepository->update($account); + } catch (IllegalObjectTypeException $exception) { + // This exception is never thrown + } } /** * Map configured roles based on group membership * * @param Account $account - * @param array $ldapSearchResult + * @param string[] $groupDns * @return void */ - protected function setRolesBasedOnGroupMembership(Account $account, array $ldapSearchResult) + protected function setRolesByGroupDns(Account $account, array $groupDns) { - if (!is_array($this->rolesConfiguration['groupMapping'])) { + if (!\is_array($this->rolesConfiguration['groupMapping'])) { return; } - $memberOf = $this->directoryService->getMemberOf($ldapSearchResult['dn']); - foreach ($this->rolesConfiguration['groupMapping'] as $roleIdentifier => $groupDns) { - if (!empty(array_intersect($memberOf, $groupDns))) { - $account->addRole($this->policyService->getRole($roleIdentifier)); + foreach ($this->rolesConfiguration['groupMapping'] as $roleIdentifier => $roleGroupDns) { + if (\array_intersect($groupDns, $roleGroupDns) !== []) { + try { + $account->addRole($this->policyService->getRole($roleIdentifier)); + } catch (\InvalidArgumentException $exception) { + // This exception is never thrown + } catch (NoSuchRoleException $exception) { + // We ignore invalid roles + // todo: logging + continue; + } } } } @@ -222,18 +288,72 @@ protected function setRolesBasedOnGroupMembership(Account $account, array $ldapS * Map configured roles based on user dn * * @param Account $account - * @param array $ldapSearchResult + * @param string $userDn * @return void */ - protected function setRolesMappedToUserDn(Account $account, array $ldapSearchResult) + protected function setRolesByUserDn(Account $account, string $userDn) { - if (!is_array($this->rolesConfiguration['userMapping'])) { + if (!\is_array($this->rolesConfiguration['userMapping'])) { return; } - foreach ($this->rolesConfiguration['userMapping'] as $roleIdentifier => $userDns) { - if (in_array($ldapSearchResult['dn'], $userDns)) { - $account->addRole($this->policyService->getRole($roleIdentifier)); + foreach ($this->rolesConfiguration['userMapping'] as $roleIdentifier => $roleUserDns) { + if (\in_array($userDn, $roleUserDns, true)) { + try { + $account->addRole($this->policyService->getRole($roleIdentifier)); + } catch (\InvalidArgumentException $exception) { + // This exception is never thrown + } catch (NoSuchRoleException $exception) { + // We ignore invalid roles + // todo: logging + continue; + } + } + } + } + + /** + * Map configured roles base on user properties + * + * @param Account $account + * @param string[][] $ldapUserData + * @return void + */ + protected function setRolesByUserProperties(Account $account, array $ldapUserData) + { + if (!\is_array($this->rolesConfiguration['propertyMapping'])) { + return; + } + + foreach ($this->rolesConfiguration['propertyMapping'] as $roleIdentifier => $propertyConditions) { + try { + $role = $this->policyService->getRole($roleIdentifier); + } catch (NoSuchRoleException $e) { + // We ignore invalid roles + // todo: logging + continue; + } + + foreach ($propertyConditions as $propertyName => $conditions) { + if (!isset($ldapUserData[$propertyName])) { + continue; + } + + if (!\is_array($conditions)) { + $conditions = [$conditions]; + } + foreach ($conditions as $condition) { + foreach ($ldapUserData[$propertyName] as $value) { + if ($value === $condition || @\preg_match($condition, $value) === 1) { + try { + $account->addRole($role); + } catch (\InvalidArgumentException $e) { + // This exception is never thrown + } + continue 4; + } + } + } } } } diff --git a/Classes/Security/Authentication/Provider/NeosBackendLdapProvider.php b/Classes/Security/Authentication/Provider/NeosBackendLdapProvider.php index 9031492..35230da 100644 --- a/Classes/Security/Authentication/Provider/NeosBackendLdapProvider.php +++ b/Classes/Security/Authentication/Provider/NeosBackendLdapProvider.php @@ -15,6 +15,7 @@ use Neos\Eel\Context; use Neos\Flow\Annotations as Flow; use Neos\Flow\Security\Account; +use Neos\Neos\Domain\Service\UserService; use Neos\Utility\Arrays; /** @@ -31,37 +32,39 @@ class NeosBackendLdapProvider extends LdapProvider protected $eelEvaluator; /** - * Create a new account for the given credentials. Return null if you - * do not want to create a new account, that is, only authenticate - * existing accounts from the database and fail on new logins. - * - * @param array $credentials array containing username and password - * @param array $ldapSearchResult - * @return Account + * @Flow\Inject + * @var UserService + */ + protected $userService; + + /** + * @inheritdoc */ - protected function createAccount(array $credentials, array $ldapSearchResult) + protected function createAccount(array $credentials, array $ldapUserData) { $mapping = Arrays::arrayMergeRecursiveOverrule( [ 'firstName' => 'user.givenName[0]', - 'lastName' => 'user.sn[0]' + 'lastName' => 'user.sn[0]', ], - isset($this->options['mapping']) ? $this->options['mapping'] : [] + $this->options['mapping'] ?? [] ); - - $eelContext = new Context(['user' => $ldapSearchResult]); + $eelContext = new Context(['user' => $ldapUserData]); try { $firstName = $this->eelEvaluator->evaluate($mapping['firstName'], $eelContext); - $lastName = $this->eelEvaluator->evaluate($mapping['lastName'], $eelContext); } catch (\Exception $exception) { - // todo : add logging + // todo: logging $firstName = 'none'; + } + try { + $lastName = $this->eelEvaluator->evaluate($mapping['lastName'], $eelContext); + } catch (\Exception $exception) { + // todo: logging $lastName = 'none'; } - $userService = new \Neos\Neos\Domain\Service\UserService(); - $user = $userService->createUser( + $user = $this->userService->createUser( $credentials['username'], '', $firstName, @@ -69,7 +72,6 @@ protected function createAccount(array $credentials, array $ldapSearchResult) [], $this->name ); - return $user->getAccounts()->get(0); } } diff --git a/Classes/Service/DirectoryService.php b/Classes/Service/DirectoryService.php index 0f87411..548a432 100644 --- a/Classes/Service/DirectoryService.php +++ b/Classes/Service/DirectoryService.php @@ -12,9 +12,13 @@ */ use Neos\Flow\Annotations as Flow; -use Neos\Flow\Error\Exception; +use Neos\Flow\Security\Exception\MissingConfigurationException; use Neos\Utility\Arrays; -use Symfony\Component\Ldap\Adapter\ExtLdap\Adapter; +use Symfony\Component\Ldap\Entry; +use Symfony\Component\Ldap\Exception\ConnectionException; +use Symfony\Component\Ldap\Exception\DriverNotFoundException; +use Symfony\Component\Ldap\Exception\LdapException; +use Symfony\Component\Ldap\Exception\NotBoundException; use Symfony\Component\Ldap\Ldap; /** @@ -26,143 +30,129 @@ class DirectoryService /** * @var Ldap */ - protected $connection; + protected $ldap; /** - * @var string - */ - protected $name; - - /** - * @var array + * @var mixed[] */ protected $options; /** - * @var Ldap - */ - protected $readConnection; - - /** - * @param string $name - * @param array $options - * @throws Exception + * @param mixed[] $options + * @param string|null $username + * @param string|null $password + * @throws ConnectionException */ - public function __construct($name, array $options) + public function __construct(array $options, string $username = null, string $password = null) { - $this->name = $name; $this->options = $options; $this->ldapConnect(); + $this->ldapBind($username, $password); } /** - * Authenticate a username / password against the Ldap server - * - * @param string $username - * @param string $password - * @return array Search result from Ldap - * @throws Exception + * @param string $userDn User DN + * @return string[] Group DNs + * @throws MissingConfigurationException + * @throws LdapException */ - public function authenticate($username, $password) + public function getGroupDnsOfUser(string $userDn) : array { - $result = $this->getReadConnection() - ->query( - $this->options['baseDn'], - sprintf($this->options['query']['account'], $username), - ['filter' => $this->options['attributesFilter']['account']] - ) - ->execute() - ->toArray(); - - if (!isset($result[0])) { - throw new \Exception('User not found'); - } - - try { - $this->connection->bind($result[0]->getDn(), $password); - } catch (\Exception $exception) { - \Neos\Flow\var_dump($exception); - die(); + if (!isset($this->options['queries']['group']['baseDn'], $this->options['queries']['group']['query'])) { + throw new MissingConfigurationException('Both baseDn and query have to be set for queries.group'); } - $userData = Arrays::arrayMergeRecursiveOverrule( - $result[0]->getAttributes(), - ['dn' => $result[0]->getDn()] + $entries = $this->query( + $this->options['queries']['group']['baseDn'], + sprintf($this->options['queries']['group']['query'], $userDn), + ['dn'] ); - return $userData; + $groupDns = []; + foreach ($entries as $entry) { + $groupDns[] = $entry->getDn(); + } + + return $groupDns; } /** - * @param string $dn User or group DN. - * @return array group DN => CN mapping - * @throws Exception + * Get account data from ldap server + * + * @param string $username + * @return string[][] Search result from Ldap + * @throws MissingConfigurationException + * @throws LdapException */ - public function getMemberOf($dn) + public function getUserData(string $username) : array { - try { - $searchResult = $this->getReadConnection() - ->query( - $this->options['baseDn'], - sprintf($this->options['query']['memberOf'], $dn), - ['filter' => $this->options['attributesFilter']['group']] - ) - ->execute() - ->toArray(); - } catch (\Exception $exception) { - throw new Exception('Error during Ldap group search: ' . $exception->getMessage(), 1443476083); + if (!isset($this->options['queries']['account']['baseDn'], $this->options['queries']['account']['query'])) { + throw new MissingConfigurationException('Both baseDn and query have to be set for queries.account'); + } + + $entries = $this->query( + $this->options['queries']['account']['baseDn'], + sprintf($this->options['queries']['account']['query'], $username), + $this->options['attributesFilter'] ?? [] + ); + if ($entries === []) { + throw new LdapException('User not found'); } - return $searchResult; + return Arrays::arrayMergeRecursiveOverrule($entries[0]->getAttributes(), ['dn' => [$entries[0]->getDn()]]); } /** - * @return Ldap + * @param string $baseDn + * @param string $queryString + * @param string[]|null $filter + * @return Entry[] + * @throws LdapException */ - public function getReadConnection() + public function query(string $baseDn, string $queryString, array $filter = null) : array { - if ($this->readConnection) { - return $this->readConnection; - } - - if (!$this->connection) { - $this->ldapConnect(); - } - + $query = $this->ldap->query($baseDn, $queryString, ['filter' => $filter ?? []]); + /** @var Entry[] $entries */ try { - $this->readConnection = clone $this->connection; - if (!empty($this->options['connection']['bind'])) { - $this->readConnection->bind( - $this->options['connection']['bind']['dn'], - $this->options['connection']['bind']['password'] - ); - } - } catch (\Exception $exception) { - \Neos\Flow\var_dump($exception, 'bind exception'); + $entries = $query->execute()->toArray(); + } catch (NotBoundException $exception) { + // This exception should never be thrown, since we bind in constructor } + return $entries; + } - return $this->readConnection; + /** + * @param string|null $username + * @param string|null $password + * @return void + * @throws ConnectionException + */ + protected function ldapBind(string $username = null, string $password = null) + { + $this->ldap->bind( + (isset($this->options['bind']['dn']) + ? sprintf($this->options['bind']['dn'], $username ?? '') + : null + ), + $this->options['bind']['password'] ?? $password + ); } /** * Initialize the Ldap server connection * - * Connect to the server and set communication options. Further bindings will be done - * by a server specific bind provider. + * Connect to the server and set communication options. Further bindings will be done by a server specific bind + * provider. * - * @return Ldap - * @throws Exception + * @return void */ protected function ldapConnect() { - if ($this->connection) { - return $this->connection; + try { + $this->ldap = Ldap::create('ext_ldap', $this->options['connection'] ?? []); + } catch (DriverNotFoundException $e) { + // since we use the default driver, this cannot happen } - - $adapter = new Adapter($this->options['connection']['options']); - $this->connection = new Ldap($adapter); - - return $this->connection; } } diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index 5fef3b9..1984a78 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -3,4 +3,5 @@ Neos: roles: default: [] groupMapping: [] + propertyMapping: [] userMapping: [] diff --git a/Configuration/Settings.yaml.ad.example b/Configuration/Settings.yaml.ad.example deleted file mode 100644 index a4e37ef..0000000 --- a/Configuration/Settings.yaml.ad.example +++ /dev/null @@ -1,50 +0,0 @@ ---- - -Neos: - Flow: - security: - authentication: - providers: - 'Neos.Neos:Backend': - provider: Neos\Ldap\Security\Authentication\Provider\NeosBackendLdapProvider - providerOptions: - attributesFilter: - account: ['dn'] - group: ['dn'] - - baseDn: dc=my-domain,dc=com - - connection: - bind: - dn: 'username@domain.com' - #dn: 'DOMAIN\user' - password: 'secret' - - options: - host: localhost - port: 389 - options: - network_timeout: 10 - protocol_version: 3 - referrals: 0 - - query: - account: '(samaccountname=%s)' - memberOf: '(member:1.2.840.113556.1.4.1941:=%s)' - - Ldap: - roles: - default: - - 'Neos.Neos:RestrictedEditor' - # map group memberships to roles - groupMapping: - 'Neos.Neos:Administrator': - - 'CN=Administrators,OU=Groups,DC=domain,DC=tld' - 'Neos.Neos:Editor': - - 'CN=Editors,OU=Groups,DC=domain,DC=tld' - # map certain users to roles - userMapping: - 'Neos.Neos:Administrator': - - 'CN=Admin,OU=Users,DC=domain,DC=tld' - 'Neos.Neos:Editor': - - 'CN=Mustermann,OU=Users,DC=domain,DC=tld' diff --git a/Configuration/Settings.yaml.example b/Configuration/Settings.yaml.example new file mode 100644 index 0000000..339958a --- /dev/null +++ b/Configuration/Settings.yaml.example @@ -0,0 +1,97 @@ +--- +Neos: + Flow: + security: + authentication: + providers: + Neos.Neos:Backend: + # Either the generic LdapProvider, oder the NeosBackendLdapProvider for Neos CMS + provider: Neos\Ldap\Security\Authentication\Provider\NeosBackendLdapProvider + providerOptions: + connection: + # 'none', 'ssl', 'tls' +# encryption: none + host: localhost + # default: 3, maps to options.protocol_version +# version: 3 + # default is built automatically from host (with optional port and encryption) +# connection_string: '' + # default: `$encryption == 'ssl' ? 636 : 389` +# port: 389 + # if true, options.debug_level will be set to 7 +# debug: false + # default value for options.referrals +# referrals: false + + # All PHP Ldap options can be set here. Make the constant lowercase and remove the ldap_opt_ prefix + # Example: LDAP_OPT_PROTOCOL_VERSION becomes protocol_version +# options: +# client_controls: [] +# debug_level: 7 +# deref: 0 +# error_number: 0 +# error_string: '' +# host_name: '' +# matched_dn: '' +# network_timeout: 0 +# protocol_version: 3 +# referrals: false +# restart: false +# server_controls: [] +# sizelimit: 0 +# timelimit: 0 +# x_sasl_authcid: '' +# x_sasl_authzid: '' +# x_sasl_mech: '' +# x_sasl_realm: '' + + # How to authenticate towards the server. Normally this is a given service account and password. + # You can also bind for each user individually, using their password: %s will be replaced with username + bind: + # For AD this can also be '%s@domain.com' or 'DOMAIN\%s' + dn: CN=ldapserviceuser,OU=Users,DC=domain,DC=tld +# password: secret + + queries: + # %s will be replaced with the username provided + account: + baseDn: OU=Users,DC=domain,DC=com + query: (uid=%s) + # Must be set if groupMapping will be used, %s will be replaced by full user dn! +# group: +# baseDn: OU=Groups,DC=domain,DC=com +# query: (&(objectClass=posixGroup)(memberUid=%s)) + + # Define what attributes are really fetched from the directory +# attributesFilter: [dn] + + # If using the NeosBackendLdapProvider, the User will have his name mapped like this +# mapping: +# firstName: user.givenName[0] +# lastName: user.sn[0] + +# Ldap: +# roles: +# default: +# - Neos.Neos:RestrictedEditor + # map group memberships to roles +# groupMapping: +# Neos.Neos:Administrator: +# - CN=Administrators,OU=Groups,DC=domain,DC=tld +# Neos.Neos:Editor: +# - CN=Administrators,OU=Groups,DC=domain,DC=tld +# - CN=Editors,OU=Groups,DC=domain,DC=tld + # map certain properties to a role, can be a regular expression (including delimeters and modifiers) +# propertyMapping: +# Neos.Neos:Administrator: +# objectClass: administrator +# Neos.Neos:Editor: +# department: +# - ~.*mathematics~i +# - /computer science/ + # map certain users to roles +# userMapping: +# Neos.Neos:Administrator: +# - CN=Admin,OU=Users,DC=domain,DC=tld +# Neos.Neos:Editor: +# - CN=Mustermann,OU=Users,DC=domain,DC=tld diff --git a/Configuration/Settings.yaml.ldap.example b/Configuration/Settings.yaml.ldap.example deleted file mode 100644 index 0d8e658..0000000 --- a/Configuration/Settings.yaml.ldap.example +++ /dev/null @@ -1,47 +0,0 @@ ---- - -Neos: - Flow: - security: - authentication: - providers: - 'Neos.Neos:Backend': - provider: Neos\Ldap\Security\Authentication\Provider\NeosBackendLdapProvider - providerOptions: - baseDn: dc=my-domain,dc=com - - connection: - bind: - dn: 'uid=ldapserviceuser,dc=example,dc=com' - password: 'secret' - - options: - host: localhost - port: 389 - options: - network_timeout: 10 - protocol_version: 3 - - filter: - account: ['dn'] - group: ['dn'] - - query: - account: '(uid=%s)' - memberOf: '(&(objectClass=posixGroup)(memberUid=%s))' - - Ldap: - roles: - default: - - 'Neos.Neos:RestrictedEditor' - groupMapping: - 'Neos.Neos:Administrator': - - 'CN=Administrators,OU=Groups,DC=domain,DC=tld' - 'Neos.Neos:Editor': - - 'CN=Editors,OU=Groups,DC=domain,DC=tld' - - 'CN=Administrators,OU=Groups,DC=domain,DC=tld' - userMapping: - 'Neos.Neos:Administrator': - - 'CN=Admin,OU=Users,DC=domain,DC=tld' - 'Neos.Neos:Editor': - - 'CN=Mustermann,OU=Users,DC=domain,DC=tld' diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..b6a4c2d --- /dev/null +++ b/Readme.md @@ -0,0 +1,105 @@ +# Neos.Ldap Documentation + +## Example + +### `LoginController.php` +```php +view->assign('username', $username); + } + + /** + * @param ActionRequest $originalRequest + * @return string|void + */ + public function onAuthenticationSuccess(ActionRequest $originalRequest = null) { + $this->redirect('status'); + } + + /** + * Logs out a - possibly - currently logged in account. + * + * @return void + */ + public function logoutAction() { + $this->authenticationManager->logout(); + $this->addFlashMessage('Successfully logged out.'); + $this->redirect('index'); + } + + /** + * @return void + */ + public function statusAction() { + $this->view->assign('activeTokens', $this->securityContext->getAuthenticationTokens()); + } +} +``` + +### `Index.html` +```html + + +
+ + +
+
+ + +
+ + +
+``` + +### `Status.html` +```html +Status: Logged in
+User: {activeTokens.LdapProvider.account.accountIdentifier}
+Logout +``` + +### `Policy.yaml` +Make sure you configure the policies so that the login and logout actions are available for the user. +```yaml +resources: + methods: + My_Package_LoginController: method(My\Package\Controller\LoginController->(index|status|login|authenticate|logout)Action()) + + acls: + Everybody: + methods: + My_Package_LoginController: GRANT +``` + +## Configuration examples +You can find examples of a ``Settings.yaml`` file for Ldap and Active Directory [here](Configuration/Settings.yaml.example) in the `Configuration` folder of the +Neos.Ldap package. diff --git a/Readme.rst b/Readme.rst deleted file mode 100644 index 7bd3f63..0000000 --- a/Readme.rst +++ /dev/null @@ -1,96 +0,0 @@ -Neos Ldap Documentation -======================= - -Example LoginController ------------------------ - -LoginController.php:: - - view->assign('username', $username); - } - - /** - * @param \Neos\Flow\Mvc\ActionRequest $originalRequest - * @return string|void - */ - public function onAuthenticationSuccess(\Neos\Flow\Mvc\ActionRequest $originalRequest = NULL) { - $this->redirect('status'); - } - - /** - * Logs out a - possibly - currently logged in account. - * - * @return void - */ - public function logoutAction() { - $this->authenticationManager->logout(); - $this->addFlashMessage('Successfully logged out.'); - $this->redirect('index'); - } - - /** - * @return void - */ - public function statusAction() { - $this->view->assign('activeTokens', $this->securityContext->getAuthenticationTokens()); - } - - } - -Index.html:: - - - -
- - -
-
- - -
- - -
- -Status.html:: - - Status: Logged in
- User: {activeTokens.LdapProvider.account.accountIdentifier}
- Logout - -Make sure you configure the policies so that the login and logout actions are available for the user. For that use a Policy.yaml -like:: - - resources: - methods: - My_Package_LoginController: 'method(My\Package\Controller\LoginController->(index|status|login|authenticate|logout)Action())' - - acls: - Everybody: - methods: - My_Package_LoginController: GRANT - -Configuration examples ----------------------- - -You can find examples of a ``Settings.yaml`` file for Ldap and Active Directory in the Configuration/ folder -of the Neos.Ldap package. diff --git a/Resources/Private/Schema/Settings.Neos.Flow.security.authentication.providers.schema.yaml b/Resources/Private/Schema/Settings.Neos.Flow.security.authentication.providers.schema.yaml new file mode 100644 index 0000000..8800c3a --- /dev/null +++ b/Resources/Private/Schema/Settings.Neos.Flow.security.authentication.providers.schema.yaml @@ -0,0 +1,199 @@ +type: dictionary +additionalProperties: + type: dictionary + properties: + providerOptions: + type: dictionary + additionalProperties: false + properties: + attributesFilter: + type: array + items: + type: string + bind: + required: true + type: dictionary + additionalProperties: false + properties: + dn: + required: true + type: string + password: + type: string + connection: + type: dictionary + additionalProperties: false + properties: + connection_string: + type: string + format: uri + debug: + type: boolean + encryption: + type: string + enum: + - none + - ssl + - tls + host: + type: string + format: host-name + options: + type: dictionary + additionalProperties: false + properties: + client_controls: &controls + type: array + items: + type: dictionary + additionalProperties: false + properties: + oid: + required: true + type: string + enum: + - LDAP_CONTROL_AUTHZID_REQUEST + - LDAP_CONTROL_DONTUSECOPY + - LDAP_CONTROL_MANAGEDSAIT + - LDAP_CONTROL_PASSWORDPOLICYREQUEST + - LDAP_CONTROL_PROXY_AUTHZ + - LDAP_CONTROL_SUBENTRIES + - LDAP_CONTROL_SYNC + - LDAP_CONTROL_X_DOMAIN_SCOPE + - LDAP_CONTROL_X_EXTENDED_DN + - LDAP_CONTROL_X_INCREMENTAL_VALUES + - LDAP_CONTROL_X_PERMISSIVE_MODIFY + - LDAP_CONTROL_X_SEARCH_OPTIONS + - LDAP_CONTROL_X_TREE_DELETE + + - LDAP_CONTROL_PAGEDRESULTS + - LDAP_CONTROL_ASSERT + - LDAP_CONTROL_VALUESRETURNFILTER + - LDAP_CONTROL_PRE_READ + - LDAP_CONTROL_POST_READ + - LDAP_CONTROL_SORTREQUEST + - LDAP_CONTROL_VLVREQUEST + iscritical: + type: boolean + value: + type: + - string + - type: array + items: + type: dictionary + additionalProperties: false + properties: + attr: + required: true + type: string + oid: + type: string + reverse: + type: boolean + - type: dictionary + additionalProperties: false + properties: + after: + type: number + attrs: + type: array + items: + type: string + attrvalue: + type: string + before: + type: number + context: + type: string + cookie: + type: string + count: + type: number + filter: + type: string + offset: + type: number + size: + type: number + + debug_level: + type: integer + minimum: 0 + deref: + type: integer + error_number: + type: integer + error_string: + type: string + host_name: + type: string + matched_dn: + type: string + network_timeout: + type: integer + minimum: -1 + protocol_version: + type: integer + minimum: 1 + maximum: 3 + referrals: + type: boolean + restart: + type: boolean + server_controls: *controls + sizelimit: + type: integer + minimum: 0 + timelimit: + type: integer + minimum: 0 + x_sasl_authcid: + type: string + x_sasl_authzid: + type: string + x_sasl_mech: + type: string + x_sasl_realm: + type: string + port: + type: integer + minimum: 1 + maximum: 65535 + referrals: + type: boolean + version: + type: integer + minimum: 1 + maximum: 3 + mapping: + type: dictionary + additionalProperties: false + properties: + firstName: + type: string + lastName: + type: string + queries: + required: true + type: dictionary + additionalProperties: false + properties: + account: + required: true + type: dictionary + properties: + baseDn: + required: true + type: string + query: + required: true + type: string + group: + type: dictionary + properties: + baseDn: + required: true + type: string + query: + required: true + type: string diff --git a/Resources/Private/Schema/Settings.Neos.Ldap.roles.schema.yaml b/Resources/Private/Schema/Settings.Neos.Ldap.roles.schema.yaml new file mode 100644 index 0000000..d8b4966 --- /dev/null +++ b/Resources/Private/Schema/Settings.Neos.Ldap.roles.schema.yaml @@ -0,0 +1,29 @@ +type: dictionary +additionalProperties: false +properties: + default: + type: array + items: + type: string + groupMapping: + type: dictionary + additionalProperties: + type: array + items: + type: string + propertyMapping: + type: dictionary + additionalProperties: + type: dictionary + additionalProperties: + type: + - string + - type: array + items: + type: string + userMapping: + type: dictionary + additionalProperties: + type: array + items: + type: string diff --git a/composer.json b/composer.json index 3537aeb..e888d5c 100644 --- a/composer.json +++ b/composer.json @@ -4,10 +4,15 @@ "description": "Ldap Authentication for Flow", "license": "MIT", "require": { - "ext-ldap": "*", + "php": "^7.0", - "neos/flow": "^4.0", - "symfony/ldap": "^3.3" + "neos/eel": "^4.0 | ^5.0", + "neos/flow": "^4.0 | ^5.0", + "neos/neos": "^3.0 | ^4.0", + "neos/utility-arrays": "^4.0 | ^5.0", + "neos/utility-files": "^4.0 | ^5.0", + "symfony/ldap": "^3.1 | ^4.0", + "symfony/yaml": "<5.0" }, "replace": { "typo3/ldap": "self.version" From 4174c0b77191d2f358ffc64202792d6af4fb9a5c Mon Sep 17 00:00:00 2001 From: Raffael Comi Date: Fri, 8 Nov 2019 11:45:36 +0100 Subject: [PATCH 6/9] Add Compatibility with Flow 6.0 --- composer.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index e888d5c..0398e76 100644 --- a/composer.json +++ b/composer.json @@ -6,11 +6,11 @@ "require": { "php": "^7.0", - "neos/eel": "^4.0 | ^5.0", - "neos/flow": "^4.0 | ^5.0", - "neos/neos": "^3.0 | ^4.0", - "neos/utility-arrays": "^4.0 | ^5.0", - "neos/utility-files": "^4.0 | ^5.0", + "neos/eel": "^4.0 | ^5.0 | ^6.0", + "neos/flow": "^4.0 | ^5.0 | ^6.0", + "neos/neos": "^3.0 | ^4.0 | ^5.0", + "neos/utility-arrays": "^4.0 | ^5.0 | ^6.0", + "neos/utility-files": "^4.0 | ^5.0 | ^6.0", "symfony/ldap": "^3.1 | ^4.0", "symfony/yaml": "<5.0" }, From 78eb8ba56d341ede2ee108b596cfc47106532ec2 Mon Sep 17 00:00:00 2001 From: Karsten Dambekalns Date: Fri, 13 May 2022 18:27:15 +0200 Subject: [PATCH 7/9] Code cleanup --- Classes/Command/UtilityCommandController.php | 56 ++++--------- .../Authentication/Provider/LdapProvider.php | 79 ++++++++----------- .../Provider/NeosBackendLdapProvider.php | 3 +- Classes/Service/DirectoryService.php | 17 ++-- 4 files changed, 63 insertions(+), 92 deletions(-) diff --git a/Classes/Command/UtilityCommandController.php b/Classes/Command/UtilityCommandController.php index 5490f1a..13f58b9 100644 --- a/Classes/Command/UtilityCommandController.php +++ b/Classes/Command/UtilityCommandController.php @@ -1,4 +1,5 @@ getOptions($providerName, $settingsFile); $bindDn = isset($options['bind']['dn']) ? sprintf($options['bind']['dn'], $username ?? '') - : null - ; + : null; $message = 'Attempt to bind ' . ($bindDn === null ? 'anonymously' : 'to ' . $bindDn); if ($password !== null) { $message .= ', using password,'; @@ -66,8 +66,6 @@ public function bindCommand( $this->outputLine($message . ' failed'); $this->outputLine($exception->getMessage()); $this->quit(1); - // quit always throws StopActionException, so we cannot get here - return; } } @@ -81,9 +79,8 @@ public function bindCommand( * @param string|null $displayColumns Comma separated list of columns to show, like "cn,objectclass" * @param string|null $username Username to be used to bind * @param string|null $password Password to be used to bind - * * @return void - * @throws StopActionException + * @throws StopCommandException */ public function queryCommand( string $baseDn, @@ -93,7 +90,7 @@ public function queryCommand( string $displayColumns = null, string $username = null, string $password = null - ) { + ): void { $options = $this->getOptions($providerName, $settingsFile); $this->outputLine('Base DN: %s', [$baseDn]); @@ -104,16 +101,9 @@ public function queryCommand( try { $directoryService = new DirectoryService($options, $username, $password); $entries = $directoryService->query($baseDn, $query, $columns); - } catch (MissingConfigurationException $exception) { - // We check for baseDn above, so this will never be thrown - /** @var Entry[] $entries */ - } catch (\RuntimeException $exception) { - // line above can be replaced by the following line when we require PHP 7.1 - // } catch (ConnectionException | \Symfony\Component\Ldap\Exception\LdapException $exception) { + } catch (ConnectionException|LdapException $exception) { $this->outputLine($exception->getMessage()); $this->quit(1); - // quit always throws StopActionException, so we cannot get here - return; } $this->outputEntriesTable($entries); } @@ -123,10 +113,10 @@ public function queryCommand( * * @param string|null $providerName Name of the authentication provider to use * @param string|null $settingsFile Path to a yaml file containing the settings to use for testing purposes - * @return mixed[] - * @throws StopActionException + * @return array + * @throws StopCommandException */ - protected function getOptions(string $providerName = null, string $settingsFile = null) : array + protected function getOptions(string $providerName = null, string $settingsFile = null): array { if ($providerName !== null) { if (isset($this->authenticationProvidersConfiguration[$providerName]['providerOptions']) @@ -137,8 +127,6 @@ protected function getOptions(string $providerName = null, string $settingsFile $this->outputLine('No configuration found for given providerName'); if ($settingsFile === null) { $this->quit(3); - // quit always throws StopActionException, so we cannot get here - return []; } } @@ -146,27 +134,20 @@ protected function getOptions(string $providerName = null, string $settingsFile if (!\file_exists($settingsFile)) { $this->outputLine('Could not find settings file on path %s', [$settingsFile]); $this->quit(1); - // quit always throws StopActionException, so we cannot get here - return []; } try { // Yaml::parseFile() introduced in symfony/yaml 3.4.0 // When above is required, we can drop dependency on neos/utility-files $directoryServiceOptions = method_exists(Yaml::class, 'parseFile') ? Yaml::parseFile($settingsFile) - : Yaml::parse(Files::getFileContents($settingsFile)) - ; + : Yaml::parse(Files::getFileContents($settingsFile)); } catch (ParseException $exception) { $this->outputLine($exception->getMessage()); $this->quit(3); - // quit always throws StopActionException, so we cannot get here - return []; } if (!\is_array($directoryServiceOptions)) { $this->outputLine('No configuration found in given settingsFile'); $this->quit(3); - // quit always throws StopActionException, so we cannot get here - return []; } return $directoryServiceOptions; } @@ -175,8 +156,6 @@ protected function getOptions(string $providerName = null, string $settingsFile 'Neither providerName nor settingsFile is passed as argument. You need to pass one of those.' ); $this->quit(1); - // quit always throws StopActionException, so we cannot get here - return []; } /** @@ -185,7 +164,7 @@ protected function getOptions(string $providerName = null, string $settingsFile * @param Entry[] $entries * @return void */ - protected function outputEntriesTable(array $entries) + protected function outputEntriesTable(array $entries): void { $headers = ['dn']; $rows = []; @@ -201,8 +180,7 @@ protected function outputEntriesTable(array $entries) $rows[$index][$propertyName] = \is_array($propertyValue) ? implode(', ', $propertyValue) - : $propertyValue - ; + : $propertyValue; } } diff --git a/Classes/Security/Authentication/Provider/LdapProvider.php b/Classes/Security/Authentication/Provider/LdapProvider.php index 117e846..1eeb5a9 100644 --- a/Classes/Security/Authentication/Provider/LdapProvider.php +++ b/Classes/Security/Authentication/Provider/LdapProvider.php @@ -1,4 +1,5 @@ directoryService = new DirectoryService($name, $options); - } + protected array $rolesConfiguration = []; /** * Authenticate the current token. If it's not possible to connect to the LDAP server the provider tries to @@ -71,10 +59,11 @@ public function __construct($name, array $options) * user to authenticate. * * @param TokenInterface $authenticationToken The token to be authenticated - * @throws UnsupportedAuthenticationTokenException * @return void - * @throws UnsupportedAuthenticationTokenException * @throws MissingConfigurationException + * @throws UnsupportedAuthenticationTokenException + * @throws InvalidConfigurationTypeException + * @throws SecurityException */ public function authenticate(TokenInterface $authenticationToken) { @@ -116,19 +105,15 @@ public function authenticate(TokenInterface $authenticationToken) } $this->emitAccountCreated($account, $ldapUserData); } - } catch (\RuntimeException $exception) { - // line above can be replaced by the following line when we require PHP 7.1 - // } catch (\Symfony\Component\Ldap\Exception\ConnectionException | LdapException $exception) { + } catch (ConnectionException|LdapException $exception) { try { $authenticationToken->setAuthenticationStatus(TokenInterface::WRONG_CREDENTIALS); if ($account !== null) { $account->authenticationAttempted(TokenInterface::WRONG_CREDENTIALS); $this->accountRepository->update($account); - $this->persistenceManager->whitelistObject($account); + $this->persistenceManager->allowObject($account); } - } catch (InvalidAuthenticationStatusException $exception) { - // This exception is never thrown - } catch (IllegalObjectTypeException $exception) { + } catch (InvalidAuthenticationStatusException|IllegalObjectTypeException $exception) { // This exception is never thrown } return; @@ -143,12 +128,10 @@ public function authenticate(TokenInterface $authenticationToken) $account->authenticationAttempted(TokenInterface::AUTHENTICATION_SUCCESSFUL); $this->accountRepository->update($account); $authenticationToken->setAuthenticationStatus(TokenInterface::AUTHENTICATION_SUCCESSFUL); - } catch (InvalidAuthenticationStatusException $exception) { - // This exception is never thrown - } catch (IllegalObjectTypeException $exception) { + } catch (InvalidAuthenticationStatusException|IllegalObjectTypeException $exception) { // This exception is never thrown } - $this->persistenceManager->whitelistObject($account); + $this->persistenceManager->allowObject($account); $authenticationToken->setAccount($account); $this->emitAccountAuthenticated($account, $ldapUserData); } @@ -159,7 +142,7 @@ public function authenticate(TokenInterface $authenticationToken) * @param string[][] $ldapUserData * @return void */ - public function emitAccountCreated(Account $account, array $ldapUserData) + public function emitAccountCreated(Account $account, array $ldapUserData): void { } @@ -169,7 +152,7 @@ public function emitAccountCreated(Account $account, array $ldapUserData) * @param string[][] $ldapUserData * @return void */ - public function emitAccountAuthenticated(Account $account, array $ldapUserData) + public function emitAccountAuthenticated(Account $account, array $ldapUserData): void { } @@ -179,7 +162,7 @@ public function emitAccountAuthenticated(Account $account, array $ldapUserData) * @param string[][] $ldapUserData * @return void */ - public function emitRolesSet(Account $account, array $ldapUserData) + public function emitRolesSet(Account $account, array $ldapUserData): void { } @@ -191,7 +174,7 @@ public function emitRolesSet(Account $account, array $ldapUserData) * @param string[][] $ldapUserData * @return Account|null */ - protected function createAccount(array $credentials, array $ldapUserData) + protected function createAccount(array $credentials, array $ldapUserData): ?Account { $account = new Account(); $account->setAccountIdentifier($credentials['username']); @@ -208,7 +191,7 @@ protected function createAccount(array $credentials, array $ldapUserData) * @param Account $account * @return void */ - protected function resetRoles(Account $account) + protected function resetRoles(Account $account): void { try { $account->setRoles([]); @@ -222,8 +205,10 @@ protected function resetRoles(Account $account) * * @param Account $account * @return void + * @throws InvalidConfigurationTypeException + * @throws SecurityException */ - protected function setDefaultRoles(Account $account) + protected function setDefaultRoles(Account $account): void { if (!\is_array($this->rolesConfiguration['default'])) { return; @@ -250,8 +235,10 @@ protected function setDefaultRoles(Account $account) * @param Account $account * @param string[][] $ldapUserData * @return void + * @throws InvalidConfigurationTypeException + * @throws SecurityException */ - protected function setRoles(Account $account, array $ldapUserData) + protected function setRoles(Account $account, array $ldapUserData): void { $this->resetRoles($account); $this->setDefaultRoles($account); @@ -259,9 +246,7 @@ protected function setRoles(Account $account, array $ldapUserData) $this->setRolesByUserDn($account, $ldapUserData['dn'][0]); try { $this->setRolesByGroupDns($account, $this->directoryService->getGroupDnsOfUser($ldapUserData['dn'][0])); - } catch (\Exception $exception) { - // line above can be replaced by the following line when we require PHP 7.1 - // } catch (MissingConfigurationException | \Symfony\Component\Ldap\Exception\LdapException $exception) { + } catch (MissingConfigurationException|LdapException $exception) { // If groups cannot be retrieved, they won't get set // todo: logging } @@ -279,8 +264,10 @@ protected function setRoles(Account $account, array $ldapUserData) * @param Account $account * @param string[] $groupDns * @return void + * @throws InvalidConfigurationTypeException + * @throws SecurityException */ - protected function setRolesByGroupDns(Account $account, array $groupDns) + protected function setRolesByGroupDns(Account $account, array $groupDns): void { if (!\is_array($this->rolesConfiguration['groupMapping'])) { return; @@ -307,8 +294,10 @@ protected function setRolesByGroupDns(Account $account, array $groupDns) * @param Account $account * @param string $userDn * @return void + * @throws InvalidConfigurationTypeException + * @throws SecurityException */ - protected function setRolesByUserDn(Account $account, string $userDn) + protected function setRolesByUserDn(Account $account, string $userDn): void { if (!\is_array($this->rolesConfiguration['userMapping'])) { return; @@ -335,8 +324,10 @@ protected function setRolesByUserDn(Account $account, string $userDn) * @param Account $account * @param string[][] $ldapUserData * @return void + * @throws InvalidConfigurationTypeException + * @throws SecurityException */ - protected function setRolesByUserProperties(Account $account, array $ldapUserData) + protected function setRolesByUserProperties(Account $account, array $ldapUserData): void { if (!\is_array($this->rolesConfiguration['propertyMapping'])) { return; diff --git a/Classes/Security/Authentication/Provider/NeosBackendLdapProvider.php b/Classes/Security/Authentication/Provider/NeosBackendLdapProvider.php index 35230da..a121870 100644 --- a/Classes/Security/Authentication/Provider/NeosBackendLdapProvider.php +++ b/Classes/Security/Authentication/Provider/NeosBackendLdapProvider.php @@ -1,4 +1,5 @@ options['queries']['group']['baseDn'], $this->options['queries']['group']['query'])) { throw new MissingConfigurationException('Both baseDn and query have to be set for queries.group'); @@ -85,7 +86,7 @@ public function getGroupDnsOfUser(string $userDn) : array * @throws MissingConfigurationException * @throws LdapException */ - public function getUserData(string $username) : array + public function getUserData(string $username): array { if (!isset($this->options['queries']['account']['baseDn'], $this->options['queries']['account']['query'])) { throw new MissingConfigurationException('Both baseDn and query have to be set for queries.account'); @@ -110,7 +111,7 @@ public function getUserData(string $username) : array * @return Entry[] * @throws LdapException */ - public function query(string $baseDn, string $queryString, array $filter = null) : array + public function query(string $baseDn, string $queryString, array $filter = null): array { $query = $this->ldap->query($baseDn, $queryString, ['filter' => $filter ?? []]); /** @var Entry[] $entries */ @@ -128,7 +129,7 @@ public function query(string $baseDn, string $queryString, array $filter = null) * @return void * @throws ConnectionException */ - protected function ldapBind(string $username = null, string $password = null) + protected function ldapBind(string $username = null, string $password = null): void { $this->ldap->bind( (isset($this->options['bind']['dn']) @@ -147,7 +148,7 @@ protected function ldapBind(string $username = null, string $password = null) * * @return void */ - protected function ldapConnect() + protected function ldapConnect(): void { try { $this->ldap = Ldap::create('ext_ldap', $this->options['connection'] ?? []); From b1b603c5ca65e9e7240116b8eae15906546ccd54 Mon Sep 17 00:00:00 2001 From: Karsten Dambekalns Date: Fri, 13 May 2022 18:27:30 +0200 Subject: [PATCH 8/9] TASK: Import core migration log to composer.json This commit imports the core migration log to the "extra" section of the composer manifest. --- composer.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 71f6a1f..da57e16 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,6 @@ "license": "MIT", "require": { "php": "^7.4 || ^8.0", - "neos/eel": "^7.0 || ^8.0", "neos/flow": "^7.0 || ^8.0", "neos/neos": "^7.0 || ^8.0", @@ -25,6 +24,10 @@ }, "branch-alias": { "dev-master": "3.1.x-dev" - } + }, + "applied-flow-migrations": [ + "TYPO3.FLOW3-201209201112", + "TYPO3.FLOW3-201201261636" + ] } -} +} \ No newline at end of file From e5cdb1cce9f6ade5b83abd977fc8e1be1a9374f8 Mon Sep 17 00:00:00 2001 From: Karsten Dambekalns Date: Fri, 13 May 2022 18:27:31 +0200 Subject: [PATCH 9/9] TASK: Apply core migrations --- composer.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index da57e16..f5d4d9c 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,28 @@ }, "applied-flow-migrations": [ "TYPO3.FLOW3-201209201112", - "TYPO3.FLOW3-201201261636" + "TYPO3.FLOW3-201201261636", + "TYPO3.Form-20160601101500", + "Neos.Twitter.Bootstrap-20161124204912", + "Neos.Form-20161124205254", + "Neos.Party-20161124225257", + "Neos.Imagine-20161124231742", + "Neos.SwiftMailer-20161130105617", + "Neos.ContentRepository.Search-20161210231100", + "Neos.Seo-20170127154600", + "Neos.Flow-20180415105700", + "Neos.Neos-20180907103800", + "Neos.Neos.Ui-20190319094900", + "Neos.Flow-20190425144900", + "Neos.Flow-20190515215000", + "Neos.NodeTypes-20190917101945", + "Networkteam.Neos.MailObfuscator-20190919145400", + "Neos.NodeTypes-20200120114136", + "Neos.Flow-20200813181400", + "Neos.Flow-20201003165200", + "Neos.Flow-20201109224100", + "Neos.Flow-20201205172733", + "Neos.Flow-20201207104500" ] } } \ No newline at end of file