From 5cd0d134043657a640f832d71b2654496ce36375 Mon Sep 17 00:00:00 2001 From: Dave Mc Nicoll Date: Thu, 10 Oct 2024 13:38:28 -0400 Subject: [PATCH] - Updated to match Notes-2.x --- src/Adapter/Ldap.php | 23 ++++++----- src/Common/LdapObject.php | 54 ++++++++++++++++++------- src/Entity/Group.php | 13 ++++-- src/Entity/GroupMember.php | 21 ++++++++++ src/Entity/User.php | 11 +++-- src/EntityTrait.php | 28 +++++++++---- src/Query/Add.php | 7 ++++ src/Query/Delete.php | 12 +----- src/Query/Dn.php | 17 ++++++++ src/Query/Filter.php | 12 ++++-- src/Query/Update.php | 12 +----- src/QueryBuilder.php | 28 ++++++++++++- src/Repository.php | 67 +++++++++++++++++++++++++++++-- src/Repository/ConditionTrait.php | 8 ++++ 14 files changed, 243 insertions(+), 70 deletions(-) create mode 100644 src/Entity/GroupMember.php create mode 100644 src/Query/Add.php create mode 100644 src/Query/Dn.php diff --git a/src/Adapter/Ldap.php b/src/Adapter/Ldap.php index 3668400..f9d72a5 100644 --- a/src/Adapter/Ldap.php +++ b/src/Adapter/Ldap.php @@ -9,6 +9,7 @@ use Ulmus\{Adapter\AdapterInterface, Migration\FieldDefinition, Ulmus}; +use LDAP\Result; use Ulmus\Ldap\Common\LdapObject; use function ldap_set_option, ldap_start_tls, ldap_bind, ldap_unbind, ldap_connect, ldap_close, ldap_get_entries; @@ -28,15 +29,15 @@ class Ldap implements \Ulmus\Adapter\AdapterInterface { public bool $encrypt; public bool $forceSSL = false; - + public array $hosts; - + public string $baseDn; public string $username; - + public string $password; - + public string $accountSuffix; public string $pathCertCrt; @@ -73,11 +74,11 @@ class Ldap implements \Ulmus\Adapter\AdapterInterface { } } - public function authenticate(string $dn, string $password) : bool + public function authenticate(string $dn, string $password) : false|Result { $this->ldapObject = $this->getLdapObject(); - return $this->ldapObject->bind($dn, $password);; + return $this->ldapObject->bind($dn, $password); } public function connect() : object @@ -92,7 +93,7 @@ class Ldap implements \Ulmus\Adapter\AdapterInterface { public function bindUser() : void { if ( ! $this->ldapObject->bind($this->username, $this->password) ) { - throw new \Exception("LDAP bind failed with given user $usr."); + throw new \Exception("LDAP bind failed with given user {$this->username}."); } } @@ -118,7 +119,7 @@ class Ldap implements \Ulmus\Adapter\AdapterInterface { elseif ($this->forceSSL) { $ldapObject->startTLS(); } - + return $ldapObject; } @@ -126,11 +127,11 @@ class Ldap implements \Ulmus\Adapter\AdapterInterface { { return ""; } - + public function setup(array $configuration) : void { $configuration = array_change_key_case($configuration, \CASE_LOWER); - + if ( false === ( $this->hosts = $configuration['hosts'] ?? false ) ) { throw new AdapterConfigurationException("Your `host` setting is missing. It is a mandatory parameter for this driver."); } @@ -166,7 +167,7 @@ class Ldap implements \Ulmus\Adapter\AdapterInterface { return $value; } - + public function escapeIdentifier(string $segment, int $type, string $ignore = "") : string { switch($type) { diff --git a/src/Common/LdapObject.php b/src/Common/LdapObject.php index 73684a9..46fcaed 100644 --- a/src/Common/LdapObject.php +++ b/src/Common/LdapObject.php @@ -3,7 +3,8 @@ namespace Ulmus\Ldap\Common; use http\Exception\InvalidArgumentException; -use function ldap_set_option, ldap_start_tls, ldap_bind, ldap_unbind, ldap_connect, ldap_close, ldap_get_entries, ldap_mod_replace, ldap_count_entries, ldap_errno, ldap_error; +use LDAP\Result; +use function ldap_set_option, ldap_start_tls, ldap_bind_ext, ldap_unbind, ldap_connect, ldap_close, ldap_get_entries, ldap_mod_replace, ldap_count_entries, ldap_errno, ldap_error; class LdapObject { @@ -23,7 +24,9 @@ class LdapObject { public string $dn; - public bool $binded = false; + public false|Result $binded = false; + + public false|Result $lastQuery = false; public bool $deleteRecursively = true; @@ -55,14 +58,14 @@ class LdapObject { ldap_start_tls($this->connection); } - public function bind(? string $dn, ? string $password = null) : bool + public function bind(? string $dn, ? string $password = null) : false|Result { if ($this->binded) { throw new \Exception("LdapObject is already binded with a user. Use the unbind() method to release it."); } try { - $this->binded = ldap_bind($this->connection, $dn, $password); + $this->binded = ldap_bind_ext($this->connection, $dn, $password); } catch(\Throwable $ex) { throw new \ErrorException(sprintf("%s [ using: %s on %s ]", $ex->getMessage(), $dn, $this->host )); @@ -172,31 +175,50 @@ class LdapObject { } try { - if (false === ($queryResult = ldap_add($this->connection, $dn, $combine))) { + + $this->lastQuery = ldap_add_ext($this->connection, $dn, $combine); + + if ($this->lastQuery === false) { $this->throwLdapException(); } + else { + $this->rowCount = 1; + $this->lastInsertId = $dn; + } } catch(\Throwable $e) { $this->throwLdapException(); } - if ($queryResult) { - $this->rowCount = (int)$queryResult; - $this->lastInsertId = $dn; + return $this; + } + + public function runAddQuery(array $filter, array $dataset) + { + static::$dump && call_user_func_array(static::$dump, [ $filter, $dataset ]); + + $this->lastQuery = ldap_mod_add_ext($this->connection, $filter['dn'], $dataset); + + if ( $this->lastQuery === false ) { + $this->throwLdapException(); } + $this->rowCount = $this->lastQuery === false ? 0 : 1; + return $this; } public function runUpdateQuery(array $filter, array $dataset) { static::$dump && call_user_func_array(static::$dump, [ $filter, $dataset ]); - - if ( false === ( $queryResult = ldap_mod_replace($this->connection, $filter['dn'], $dataset) ) ) { + + $this->lastQuery = ldap_mod_replace_ext($this->connection, $filter['dn'], $dataset); + + if ( $this->lastQuery === false ) { $this->throwLdapException(); } - $this->rowCount = (int) $queryResult; + $this->rowCount = $this->lastQuery === false ? 0 : 1; return $this; } @@ -218,11 +240,13 @@ class LdapObject { } } - if ( false === ( $queryResult = ldap_delete($this->connection, $filter['dn']) ) ) { + $this->lastQuery = ldap_delete_ext($this->connection, $filter['dn']); + + if ( $this->lastQuery === false ) { $this->throwLdapException(); } - $this->rowCount = (int) $queryResult; + $this->rowCount = $this->lastQuery === false ? 0 : 1; return $this; } @@ -237,7 +261,9 @@ class LdapObject { return; } - throw new \Exception(sprintf('LDAP error #%s `%s`', $no, ldap_error($this->connection))); + ldap_get_option($this->connection, \LDAP_OPT_DIAGNOSTIC_MESSAGE, $err); + + throw new \Exception(sprintf('LDAP error #%s `%s`. %s', $no, ldap_error($this->connection), $err)); } public function closeCursor() : void {} diff --git a/src/Entity/Group.php b/src/Entity/Group.php index c6c37fd..6059d17 100644 --- a/src/Entity/Group.php +++ b/src/Entity/Group.php @@ -4,9 +4,12 @@ namespace Ulmus\Ldap\Entity; use Ulmus\Ldap\Entity\Field\{ Datetime }; -use Ulmus\Attribute\Property\Field; +use Ulmus\Attribute\Property\{Field, Filter, Relation, Virtual}; +use Ulmus\EntityCollection; use Ulmus\Ldap\Attribute\Obj\ObjectClass; +use Ulmus\Ldap\Repository; +use Ulmus\Repository\RepositoryInterface; #[ObjectClass("group")] class Group @@ -17,10 +20,10 @@ class Group public string $samaccountname; #[Field] - public array $members; + public string $name; #[Field] - public string $name; + public string $description; #[Field] public string $canonicalName; @@ -28,6 +31,10 @@ class Group #[Field(name: "objectGUID")] public string $guid; + # You will need to override this method in your entity because an adapter needs to be set for the User entity. + #[Relation(type: Relation\RelationTypeEnum::oneToMany, key: 'dn', foreignKey: "memberOf", entity: User::class)] + public EntityCollection $members; + public function __toString() : string { return $this->name; diff --git a/src/Entity/GroupMember.php b/src/Entity/GroupMember.php new file mode 100644 index 0000000..27c1aa1 --- /dev/null +++ b/src/Entity/GroupMember.php @@ -0,0 +1,21 @@ + explode('=', explode(',', $e)[0])[1], $this->memberOf); + $arr = array_map(fn($e) => explode('=', explode(',', $e)[0])[1], (array) $this->memberOf); usort($arr, 'strcasecmp'); return $arr; } + public function isMemberOf(string $name) : bool + { + return in_array(strtolower($name), array_map('strtolower', $this->memberOfGroup()), true); + } + public function brokenMigration() : bool { if ( empty($this->targetAddress) ) { diff --git a/src/EntityTrait.php b/src/EntityTrait.php index d567a1b..86fe2b0 100644 --- a/src/EntityTrait.php +++ b/src/EntityTrait.php @@ -2,8 +2,16 @@ namespace Ulmus\Ldap; -use Ulmus\{Attribute\Property\Field, Ulmus, EventTrait, Query, Common\EntityResolver, Common\EntityField}; +use Ulmus\{Attribute\Property\Field, + ConnectionAdapter, + Repository, + Ulmus, + EventTrait, + Query, + Common\EntityResolver, + Common\EntityField}; +use Notes\Attribute\Ignore; use Ulmus\Ldap\Annotation\Classes\{ ObjectClass, ObjectType, }; trait EntityTrait { @@ -18,25 +26,29 @@ trait EntityTrait { #[Field] public array $objectClass; + #[Ignore] public static function resolveEntity() : EntityResolver { return Ulmus::resolveEntity(static::class); } - public static function repository(string $alias = Repository::DEFAULT_ALIAS) : Repository + #[Ignore] + public static function repository(string $alias = Repository::DEFAULT_ALIAS, ConnectionAdapter $adapter = null) : Repository { - return new Repository(static::class, $alias); + return Ulmus::repository(static::class, $alias, $adapter); } - public static function field($name, ? string $alias = null) : EntityField + #[Ignore] + public static function field($name, null|string|false $alias = Repository::DEFAULT_ALIAS) : EntityField { - return new EntityField(static::class, $name, $alias ?: Repository::DEFAULT_ALIAS, Ulmus::resolveEntity(static::class)); + return new EntityField(static::class, $name, false, Ulmus::resolveEntity(static::class)); } - public static function fields(array $fields, ? string $alias = null) : string + #[Ignore] + public static function fields(array $fields, null|string|false $alias = Repository::DEFAULT_ALIAS, string $separator = ', ') : string { - return implode(', ', array_map(function($item) use ($alias){ - return static::field($item, $alias); + return implode(', ', array_map(function($item) { + return static::field($item, false); }, $fields)); } } diff --git a/src/Query/Add.php b/src/Query/Add.php new file mode 100644 index 0000000..a4d2a1c --- /dev/null +++ b/src/Query/Add.php @@ -0,0 +1,7 @@ + $this->dn, - ]; - } } diff --git a/src/Query/Dn.php b/src/Query/Dn.php new file mode 100644 index 0000000..ce9d023 --- /dev/null +++ b/src/Query/Dn.php @@ -0,0 +1,17 @@ + $this->dn, + ]; + } +} diff --git a/src/Query/Filter.php b/src/Query/Filter.php index 71679ed..ee3a74e 100644 --- a/src/Query/Filter.php +++ b/src/Query/Filter.php @@ -45,14 +45,20 @@ class Filter extends Fragment { return $this; } - public function render(bool $skipToken = false) : array + public function render(bool $raw = false) : array { + if ($raw) { + return [ + 'filters' => $this->conditionList + ]; + } + $stack = []; - + foreach ($this->conditionList ?? [] as $key => $item) { if ( $item instanceof Filter ) { if ( $item->conditionList ?? false ) { - $stack[] = ( $key !== 0 ? "{$item->condition} " : "" ) . "(" . implode('', $item->render($skipToken)) . ")"; + $stack[] = ( $key !== 0 ? "{$item->condition} " : "" ) . "(" . implode('', $item->render()) . ")"; } } else { diff --git a/src/Query/Update.php b/src/Query/Update.php index 953e079..5f88230 100644 --- a/src/Query/Update.php +++ b/src/Query/Update.php @@ -2,16 +2,6 @@ namespace Ulmus\Ldap\Query; -class Update extends \Ulmus\Query\Fragment { +class Update extends Dn { - public int $order = -100; - - public string $dn; - - public function render() : array - { - return [ - 'dn' => $this->dn, - ]; - } } diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index c0f75c8..e5fdc94 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -79,10 +79,34 @@ class QueryBuilder implements Ulmus\QueryBuilder\QueryBuilderInterface return $this; } + public function dn(string $dn) : self + { + if ( null === ( $update = $this->getFragment(Query\Dn::class) ) ) { + $update = new Query\Dn(); + $this->push($update); + } + + $update->dn = $dn; + + return $this; + } + + public function add(string $dn) : self + { + if ( null === ( $add = $this->getFragment(Query\Add::class) ) ) { + $add = new Query\Add(); + $this->push($add); + } + + $add->dn = $dn; + + return $this; + } + public function update(string $dn) : self { if ( null === ( $update = $this->getFragment(Query\Update::class) ) ) { - $update = new Query\Update($this); + $update = new Query\Update(); $this->push($update); } @@ -94,7 +118,7 @@ class QueryBuilder implements Ulmus\QueryBuilder\QueryBuilderInterface public function delete(string $dn) : self { if ( null === ( $delete = $this->getFragment(Query\Delete::class) ) ) { - $delete = new Query\Delete($this); + $delete = new Query\Delete(); $this->push($delete); } diff --git a/src/Repository.php b/src/Repository.php index 34c280e..f7d641c 100644 --- a/src/Repository.php +++ b/src/Repository.php @@ -2,8 +2,8 @@ namespace Ulmus\Ldap; -use Ulmus\Ldap\Annotation\Classes\{ ObjectClass, ObjectType }; -use Ulmus\{EntityCollection, Ulmus, SearchRequest, Query}; +use Ulmus\Ldap\Attribute\Obj\{ ObjectClass, ObjectType }; +use Ulmus\{EntityCollection, Event\Repository\CollectionFromQueryItemInterface, Ulmus, SearchRequest, Ldap\Query}; use Ulmus\Annotation\Property\{ Where, Having, Relation, Join, WithJoin, Relation\Ignore as RelationIgnore }; use Ulmus\Common\EntityResolver; @@ -21,17 +21,52 @@ class Repository extends \Ulmus\Repository $this->queryBuilder = new QueryBuilder(); } + public function saveAdd(object|array $entity, ? array $fieldsAndValue = null, bool $replace = false) : bool + { + if (is_array($entity)) { + $entity = (new $this->entityClass())->fromArray($entity); + } + + $dataset = $entity->toArray(); + + $primaryKeyDefinition = Ulmus::resolveEntity($this->entityClass)->getPrimaryKeyField(); + + if ( $primaryKeyDefinition === null ) { + throw new \Exception(sprintf("No primary key found for entity %s", $this->entityClass)); + } + + $diff = $fieldsAndValue ?? $this->generateWritableDataset($entity); + + if ( [] !== $diff ) { + $pkField = key($primaryKeyDefinition); + $pkFieldName = $primaryKeyDefinition[$pkField]->name ?? $pkField; + $this->where($pkFieldName, $dataset[$pkFieldName]); + + $this->updateSqlQuery($diff)->finalizeQuery(); + + $ldapObject = $this->adapter->connector()->runAddQuery($this->queryBuilder->render(), array_merge($this->queryBuilder->values ?? [], $this->queryBuilder->parameters ?? [])); + + $this->queryBuilder->reset(); + + $entity->entityFillFromDataset($dataset, true); + + return $ldapObject ? (bool) $ldapObject->rowCount : false; + } + + return false; + } + protected function selectSqlQuery() : self { if ( null === $this->queryBuilder->getFragment(Query\Select::class) ) { $this->select(array_keys($this->entityResolver->fieldList(EntityResolver::KEY_COLUMN_NAME))); } - if ( null !== $objectClass = $this->entityResolver->getAnnotationFromClassname( ObjectClass::class, false ) ) { + if ( null !== $objectClass = $this->entityResolver->getAttributeImplementing( ObjectClass::class ) ) { $this->where('objectclass', $objectClass->type); } - if ( null !== $objectClass = $this->entityResolver->getAnnotationFromClassname( ObjectType::class, false ) ) { + if ( null !== $objectClass = $this->entityResolver->getAttributeImplementing( ObjectType::class ) ) { $this->where('objecttype', $objectClass->type); } @@ -53,6 +88,7 @@ class Repository extends \Ulmus\Repository protected function updateSqlQuery(array $dataset) : self { + # Backward compatibility here ! $condition = array_pop($this->queryBuilder->where->conditionList); if ($condition[0] === 'dn') { @@ -130,6 +166,13 @@ class Repository extends \Ulmus\Repository return $this; } + public function add(string $dn) : self + { + $this->queryBuilder->add($dn); + + return $this; + } + public function escapeValue($identifier) : string { return is_object($identifier) ? $identifier : $this->adapter->adapter()->escapeIdentifier($identifier, Adapter\Ldap::IDENTIFIER_FILTER); @@ -157,4 +200,20 @@ class Repository extends \Ulmus\Repository return parent::filterServerRequest($searchRequest, false); } + + public function collectionFromQuery(? string $entityClass = null) : EntityCollection + { + $this->eventRegister(new class implements CollectionFromQueryItemInterface { + + public function execute(array &$data): array + { + # If user is member of only one group, it is received as a single string. + $data['memberOf'] = isset($data['memberOf']) ? (array) $data['memberOf'] : null; + + return $data; + } + }); + + return parent::collectionFromQuery($entityClass); + } } \ No newline at end of file diff --git a/src/Repository/ConditionTrait.php b/src/Repository/ConditionTrait.php index 1e83ed4..8a7c264 100644 --- a/src/Repository/ConditionTrait.php +++ b/src/Repository/ConditionTrait.php @@ -135,4 +135,12 @@ trait ConditionTrait return $this; } + + public function dn(string $value) : Repository + { + $this->queryBuilder->dn($value); + + return $this; + } + } \ No newline at end of file