From a58ec011f0a65798e0780893965274ba36df07ec Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Mon, 13 Jul 2026 12:09:10 +0200 Subject: [PATCH] feat: Provide check user as an OCS API Refactor the command in a service and call the same code in a new controller as well as the existing occ command. Signed-off-by: Carl Schwan --- .../composer/composer/autoload_classmap.php | 2 + .../composer/composer/autoload_static.php | 2 + apps/user_ldap/lib/Command/CheckUser.php | 93 ++-------- .../lib/Controller/APIController.php | 55 ++++++ .../lib/Controller/ConfigAPIController.php | 5 +- .../lib/Service/CheckUserService.php | 106 +++++++++++ apps/user_ldap/openapi.json | 170 ++++++++++++++++++ openapi.json | 170 ++++++++++++++++++ 8 files changed, 523 insertions(+), 80 deletions(-) create mode 100644 apps/user_ldap/lib/Controller/APIController.php create mode 100644 apps/user_ldap/lib/Service/CheckUserService.php diff --git a/apps/user_ldap/composer/composer/autoload_classmap.php b/apps/user_ldap/composer/composer/autoload_classmap.php index a1743773f49eb..b1a7ed2f376b2 100644 --- a/apps/user_ldap/composer/composer/autoload_classmap.php +++ b/apps/user_ldap/composer/composer/autoload_classmap.php @@ -28,6 +28,7 @@ 'OCA\\User_LDAP\\Configuration' => $baseDir . '/../lib/Configuration.php', 'OCA\\User_LDAP\\Connection' => $baseDir . '/../lib/Connection.php', 'OCA\\User_LDAP\\ConnectionFactory' => $baseDir . '/../lib/ConnectionFactory.php', + 'OCA\\User_LDAP\\Controller\\APIController' => $baseDir . '/../lib/Controller/APIController.php', 'OCA\\User_LDAP\\Controller\\ConfigAPIController' => $baseDir . '/../lib/Controller/ConfigAPIController.php', 'OCA\\User_LDAP\\Controller\\RenewPasswordController' => $baseDir . '/../lib/Controller/RenewPasswordController.php', 'OCA\\User_LDAP\\Controller\\WizardController' => $baseDir . '/../lib/Controller/WizardController.php', @@ -83,6 +84,7 @@ 'OCA\\User_LDAP\\PagedResults\\TLinkId' => $baseDir . '/../lib/PagedResults/TLinkId.php', 'OCA\\User_LDAP\\Proxy' => $baseDir . '/../lib/Proxy.php', 'OCA\\User_LDAP\\Service\\BirthdateParserService' => $baseDir . '/../lib/Service/BirthdateParserService.php', + 'OCA\\User_LDAP\\Service\\CheckUserService' => $baseDir . '/../lib/Service/CheckUserService.php', 'OCA\\User_LDAP\\Service\\UpdateGroupsService' => $baseDir . '/../lib/Service/UpdateGroupsService.php', 'OCA\\User_LDAP\\Settings\\Admin' => $baseDir . '/../lib/Settings/Admin.php', 'OCA\\User_LDAP\\Settings\\Section' => $baseDir . '/../lib/Settings/Section.php', diff --git a/apps/user_ldap/composer/composer/autoload_static.php b/apps/user_ldap/composer/composer/autoload_static.php index af3ddfbdf020a..ec718ae56517c 100644 --- a/apps/user_ldap/composer/composer/autoload_static.php +++ b/apps/user_ldap/composer/composer/autoload_static.php @@ -43,6 +43,7 @@ class ComposerStaticInitUser_LDAP 'OCA\\User_LDAP\\Configuration' => __DIR__ . '/..' . '/../lib/Configuration.php', 'OCA\\User_LDAP\\Connection' => __DIR__ . '/..' . '/../lib/Connection.php', 'OCA\\User_LDAP\\ConnectionFactory' => __DIR__ . '/..' . '/../lib/ConnectionFactory.php', + 'OCA\\User_LDAP\\Controller\\APIController' => __DIR__ . '/..' . '/../lib/Controller/APIController.php', 'OCA\\User_LDAP\\Controller\\ConfigAPIController' => __DIR__ . '/..' . '/../lib/Controller/ConfigAPIController.php', 'OCA\\User_LDAP\\Controller\\RenewPasswordController' => __DIR__ . '/..' . '/../lib/Controller/RenewPasswordController.php', 'OCA\\User_LDAP\\Controller\\WizardController' => __DIR__ . '/..' . '/../lib/Controller/WizardController.php', @@ -98,6 +99,7 @@ class ComposerStaticInitUser_LDAP 'OCA\\User_LDAP\\PagedResults\\TLinkId' => __DIR__ . '/..' . '/../lib/PagedResults/TLinkId.php', 'OCA\\User_LDAP\\Proxy' => __DIR__ . '/..' . '/../lib/Proxy.php', 'OCA\\User_LDAP\\Service\\BirthdateParserService' => __DIR__ . '/..' . '/../lib/Service/BirthdateParserService.php', + 'OCA\\User_LDAP\\Service\\CheckUserService' => __DIR__ . '/..' . '/../lib/Service/CheckUserService.php', 'OCA\\User_LDAP\\Service\\UpdateGroupsService' => __DIR__ . '/..' . '/../lib/Service/UpdateGroupsService.php', 'OCA\\User_LDAP\\Settings\\Admin' => __DIR__ . '/..' . '/../lib/Settings/Admin.php', 'OCA\\User_LDAP\\Settings\\Section' => __DIR__ . '/..' . '/../lib/Settings/Section.php', diff --git a/apps/user_ldap/lib/Command/CheckUser.php b/apps/user_ldap/lib/Command/CheckUser.php index 363518673e76c..ccb218e987d58 100644 --- a/apps/user_ldap/lib/Command/CheckUser.php +++ b/apps/user_ldap/lib/Command/CheckUser.php @@ -8,10 +8,7 @@ namespace OCA\User_LDAP\Command; -use OCA\User_LDAP\Helper; -use OCA\User_LDAP\Mapping\UserMapping; -use OCA\User_LDAP\User\DeletedUsersIndex; -use OCA\User_LDAP\User_Proxy; +use OCA\User_LDAP\Service\CheckUserService; use OCP\IUserManager; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -21,11 +18,8 @@ class CheckUser extends Command { public function __construct( - protected User_Proxy $backend, - protected Helper $helper, - protected DeletedUsersIndex $dui, - protected UserMapping $mapping, - protected IUserManager $userManager, + private readonly IUserManager $userManager, + private readonly CheckUserService $checkUserService, ) { parent::__construct(); } @@ -77,7 +71,11 @@ protected function configure(): void { #[\Override] protected function execute(InputInterface $input, OutputInterface $output): int { try { - $this->assertAllowed($input->getOption('force')); + if (!$this->checkUserService->assertAllowed($input->getOption('force'))) { + throw new \Exception('Cannot check user existence, because ' + . 'disabled LDAP configurations are present.'); + } + $uid = $input->getArgument('ocName'); if ($uid !== null) { @@ -109,77 +107,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int } private function checkUser(InputInterface $input, OutputInterface $output, string $uid): int { - if ($this->backend->getLDAPAccess($uid)->stringResemblesDN($uid)) { - $username = $this->backend->dn2UserName($uid); - if ($username !== false) { - $uid = $username; - } - } - $wasMapped = $this->userWasMapped($uid); - $exists = $this->backend->userExistsOnLDAP($uid, true); - if ($exists === true) { + $result = $this->checkUserService->checkUser($uid, $input->getOption('update')); + if ($result['exists']) { $output->writeln('The user is still available on LDAP.'); - if ($input->getOption('update')) { - $this->updateUser($uid, $output); - } - return self::SUCCESS; - } - - if ($wasMapped) { - $this->dui->markUser($uid); + } elseif ($result['wasMapped']) { $output->writeln('The user does not exists on LDAP anymore.'); - $output->writeln('Clean up the user\'s remnants by: ./occ user:delete "' - . $uid . '"'); - return self::SUCCESS; - } - - throw new \Exception('The given user is not a recognized LDAP user.'); - } - - /** - * checks whether a user is actually mapped - * @param string $ocName the username as used in Nextcloud - */ - protected function userWasMapped(string $ocName): bool { - $dn = $this->mapping->getDNByName($ocName); - return $dn !== false; - } - - /** - * checks whether the setup allows reliable checking of LDAP user existence - * @throws \Exception - */ - protected function assertAllowed(bool $force): void { - if ($this->helper->haveDisabledConfigurations() && !$force) { - throw new \Exception('Cannot check user existence, because ' - . 'disabled LDAP configurations are present.'); + $output->writeln('Clean up the user\'s remnants by: ./occ user:delete "' . $uid . '"'); } - - // we don't check ldapUserCleanupInterval from config.php because this - // action is triggered manually, while the setting only controls the - // background job. - } - - private function updateUser(string $uid, OutputInterface $output): void { - try { - $access = $this->backend->getLDAPAccess($uid); - $attrs = $access->userManager->getAttributes(); - $user = $access->userManager->get($uid); - $avatarAttributes = $access->getConnection()->resolveRule('avatar'); - $baseDn = $this->helper->DNasBaseParameter($user->getDN()); - $result = $access->search('objectclass=*', $baseDn, $attrs, 1, 0); - foreach ($result[0] as $attribute => $valueSet) { - $output->writeln(' ' . $attribute . ': '); - foreach ($valueSet as $value) { - if (in_array($attribute, $avatarAttributes)) { - $value = '{ImageData}'; - } - $output->writeln(' ' . $value); - } + if (isset($result['attributes'])) { + foreach ($result['attributes'] as $attribute => $value) { + $output->writeln(' ' . $attribute . ': ' . $value); } - $access->batchApplyUserAttributes($result); - } catch (\Exception $e) { - $output->writeln('Error while trying to lookup and update attributes from LDAP'); } + return self::SUCCESS; } } diff --git a/apps/user_ldap/lib/Controller/APIController.php b/apps/user_ldap/lib/Controller/APIController.php new file mode 100644 index 0000000000000..86b0cc3ceb23c --- /dev/null +++ b/apps/user_ldap/lib/Controller/APIController.php @@ -0,0 +1,55 @@ +}, array{}> + * @throws OCSException An unexpected error happened + * @throws OCSForbiddenException Cannot check user existence + * + * 200: The user configuration as found in the LDAP server + */ + #[ApiRoute(verb: 'POST', url: '/api/v1/checkUser/{userID}')] + #[AuthorizedAdminSetting(settings: Admin::class)] + public function checkUser(string $userID): DataResponse { + if ($this->checkUserService->assertAllowed(false)) { + throw new OCSForbiddenException('Cannot check user existence, because disabled LDAP configurations are present.'); + } + + $result = $this->checkUserService->checkUser($userID, true); + return new DataResponse($result); + } +} diff --git a/apps/user_ldap/lib/Controller/ConfigAPIController.php b/apps/user_ldap/lib/Controller/ConfigAPIController.php index e1faacac902f5..e91dec074c619 100644 --- a/apps/user_ldap/lib/Controller/ConfigAPIController.php +++ b/apps/user_ldap/lib/Controller/ConfigAPIController.php @@ -314,7 +314,7 @@ public function testConfiguration(string $configID) { */ #[AuthorizedAdminSetting(settings: Admin::class)] #[ApiRoute(verb: 'POST', url: '/api/v1/config/{configID}/copy')] - public function copyConfiguration(string $configID) { + public function copyConfiguration(string $configID): DataResponse { try { $this->ensureConfigIDExists($configID); $configPrefix = $this->ldapHelper->getNextServerConfigurationPrefix(); @@ -337,8 +337,7 @@ public function copyConfiguration(string $configID) { * @param string $configID * @throws OCSNotFoundException */ - #[AuthorizedAdminSetting(settings: Admin::class)] - private function ensureConfigIDExists($configID): void { + private function ensureConfigIDExists(string $configID): void { $prefixes = $this->ldapHelper->getServerConfigurationPrefixes(); if (!in_array($configID, $prefixes, true)) { throw new OCSNotFoundException('Config ID not found'); diff --git a/apps/user_ldap/lib/Service/CheckUserService.php b/apps/user_ldap/lib/Service/CheckUserService.php new file mode 100644 index 0000000000000..5a4a222e566ad --- /dev/null +++ b/apps/user_ldap/lib/Service/CheckUserService.php @@ -0,0 +1,106 @@ +} + * @throws UserNotFoundException + * @throws \OCP\PreConditionNotMetException + */ + public function checkUser(string $uid, bool $update): int { + if ($this->backend->getLDAPAccess($uid)->stringResemblesDN($uid)) { + $username = $this->backend->dn2UserName($uid); + if ($username !== false) { + $uid = $username; + } + } + $wasMapped = $this->userWasMapped($uid); + $exists = $this->backend->userExistsOnLDAP($uid, true); + if ($exists === true) { + if ($update) { + $attributes = $this->updateUser($uid); + return ['exists' => true, 'wasMapped' => $wasMapped, 'attributes' => $attributes]; + } + return ['exists' => true, 'wasMapped' => $wasMapped]; + } + + if (!$wasMapped) { + throw new UserNotFoundException('The given user is not a recognized LDAP user.'); + } + + $this->dui->markUser($uid); + return ['exists' => true, 'wasMapped' => $wasMapped]; + } + + /** + * checks whether a user is actually mapped + * @param string $ocName the username as used in Nextcloud + */ + protected function userWasMapped(string $ocName): bool { + $dn = $this->mapping->getDNByName($ocName); + return $dn !== false; + } + + /** + * Checks whether the setup allows reliable checking of LDAP user existence + */ + public function assertAllowed(bool $force): bool { + // we don't check ldapUserCleanupInterval from config.php because this + // action is triggered manually, while the setting only controls the + // background job. + return !$this->helper->haveDisabledConfigurations() || $force; + } + + /** + * @param string $uid + * @return array The attributes + */ + private function updateUser(string $uid): array { + try { + $access = $this->backend->getLDAPAccess($uid); + $attrs = $access->userManager->getAttributes(); + $user = $access->userManager->get($uid); + $avatarAttributes = $access->getConnection()->resolveRule('avatar'); + $baseDn = $this->helper->DNasBaseParameter($user->getDN()); + $result = $access->search('objectclass=*', $baseDn, $attrs, 1, 0); + $attributes = []; + foreach ($result[0] as $attribute => $valueSet) { + foreach ($valueSet as $value) { + if (in_array($attribute, $avatarAttributes)) { + $value = '{ImageData}'; + } + $attributes[$attribute] = $value; + } + } + $access->batchApplyUserAttributes($result); + return $attributes; + } catch (\Exception $e) { + throw new \Exception('Error while trying to lookup and update attributes from LDAP', previous: $e); + } + } +} diff --git a/apps/user_ldap/openapi.json b/apps/user_ldap/openapi.json index 08e1f34dc0e9e..2d525ee87accb 100644 --- a/apps/user_ldap/openapi.json +++ b/apps/user_ldap/openapi.json @@ -47,6 +47,176 @@ } }, "paths": { + "/ocs/v2.php/apps/user_ldap/api/v1/checkUser/{userID}": { + "post": { + "operationId": "api-check-user", + "summary": "Check a user and update its attribute in the LDAP server", + "description": "This endpoint requires admin access", + "tags": [ + "api" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "userID", + "in": "path", + "description": "ID of the user", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "The user configuration as found in the LDAP server", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "exists", + "wasMapped" + ], + "properties": { + "exists": { + "type": "boolean" + }, + "wasMapped": { + "type": "boolean" + }, + "attributes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Cannot check user existence", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + }, + { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + ] + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/apps/user_ldap/api/v1/config": { "post": { "operationId": "configapi-create", diff --git a/openapi.json b/openapi.json index d329d32c3ab62..cd2adf0777a47 100644 --- a/openapi.json +++ b/openapi.json @@ -37554,6 +37554,176 @@ } } }, + "/ocs/v2.php/apps/user_ldap/api/v1/checkUser/{userID}": { + "post": { + "operationId": "user_ldap-api-check-user", + "summary": "Check a user and update its attribute in the LDAP server", + "description": "This endpoint requires admin access", + "tags": [ + "user_ldap/api" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "userID", + "in": "path", + "description": "ID of the user", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "The user configuration as found in the LDAP server", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "exists", + "wasMapped" + ], + "properties": { + "exists": { + "type": "boolean" + }, + "wasMapped": { + "type": "boolean" + }, + "attributes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Cannot check user existence", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + }, + { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + ] + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/apps/user_ldap/api/v1/config": { "post": { "operationId": "user_ldap-configapi-create",