diff --git a/apps/dav/lib/DAV/GroupPrincipalBackend.php b/apps/dav/lib/DAV/GroupPrincipalBackend.php index ddbd64bdda1d0..70a0099330c05 100644 --- a/apps/dav/lib/DAV/GroupPrincipalBackend.php +++ b/apps/dav/lib/DAV/GroupPrincipalBackend.php @@ -188,6 +188,10 @@ public function searchPrincipals($prefixPath, array $searchProperties, $test = ' $groups = $this->groupManager->search($value, $searchLimit); $results[] = array_reduce($groups, function (array $carry, IGroup $group) use ($restrictGroups) { + if ($group->hideFromCollaboration()) { + return $carry; + } + $gid = $group->getGID(); // is sharing restricted to groups only? if ($restrictGroups !== false) { diff --git a/apps/testing/composer/composer/autoload_classmap.php b/apps/testing/composer/composer/autoload_classmap.php index e7f1ce7446640..9be96aaf61793 100644 --- a/apps/testing/composer/composer/autoload_classmap.php +++ b/apps/testing/composer/composer/autoload_classmap.php @@ -13,6 +13,7 @@ 'OCA\\Testing\\Controller\\LockingController' => $baseDir . '/../lib/Controller/LockingController.php', 'OCA\\Testing\\Controller\\RateLimitTestController' => $baseDir . '/../lib/Controller/RateLimitTestController.php', 'OCA\\Testing\\Conversion\\ConversionProvider' => $baseDir . '/../lib/Conversion/ConversionProvider.php', + 'OCA\\Testing\\HiddenGroupBackend' => $baseDir . '/../lib/HiddenGroupBackend.php', 'OCA\\Testing\\Listener\\GetDeclarativeSettingsValueListener' => $baseDir . '/../lib/Listener/GetDeclarativeSettingsValueListener.php', 'OCA\\Testing\\Listener\\RegisterDeclarativeSettingsListener' => $baseDir . '/../lib/Listener/RegisterDeclarativeSettingsListener.php', 'OCA\\Testing\\Listener\\SetDeclarativeSettingsValueListener' => $baseDir . '/../lib/Listener/SetDeclarativeSettingsValueListener.php', diff --git a/apps/testing/composer/composer/autoload_static.php b/apps/testing/composer/composer/autoload_static.php index f87a822aaf2fc..bd557c37f6bd1 100644 --- a/apps/testing/composer/composer/autoload_static.php +++ b/apps/testing/composer/composer/autoload_static.php @@ -28,6 +28,7 @@ class ComposerStaticInitTesting 'OCA\\Testing\\Controller\\LockingController' => __DIR__ . '/..' . '/../lib/Controller/LockingController.php', 'OCA\\Testing\\Controller\\RateLimitTestController' => __DIR__ . '/..' . '/../lib/Controller/RateLimitTestController.php', 'OCA\\Testing\\Conversion\\ConversionProvider' => __DIR__ . '/..' . '/../lib/Conversion/ConversionProvider.php', + 'OCA\\Testing\\HiddenGroupBackend' => __DIR__ . '/..' . '/../lib/HiddenGroupBackend.php', 'OCA\\Testing\\Listener\\GetDeclarativeSettingsValueListener' => __DIR__ . '/..' . '/../lib/Listener/GetDeclarativeSettingsValueListener.php', 'OCA\\Testing\\Listener\\RegisterDeclarativeSettingsListener' => __DIR__ . '/..' . '/../lib/Listener/RegisterDeclarativeSettingsListener.php', 'OCA\\Testing\\Listener\\SetDeclarativeSettingsValueListener' => __DIR__ . '/..' . '/../lib/Listener/SetDeclarativeSettingsValueListener.php', diff --git a/apps/testing/lib/AppInfo/Application.php b/apps/testing/lib/AppInfo/Application.php index bbd9e288cc19e..0b86c2be78e6c 100644 --- a/apps/testing/lib/AppInfo/Application.php +++ b/apps/testing/lib/AppInfo/Application.php @@ -8,6 +8,7 @@ use OCA\Testing\AlternativeHomeUserBackend; use OCA\Testing\Conversion\ConversionProvider; +use OCA\Testing\HiddenGroupBackend; use OCA\Testing\Listener\GetDeclarativeSettingsValueListener; use OCA\Testing\Listener\RegisterDeclarativeSettingsListener; use OCA\Testing\Listener\SetDeclarativeSettingsValueListener; @@ -26,6 +27,7 @@ use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; +use OCP\IGroupManager; use OCP\Settings\Events\DeclarativeSettingsGetValueEvent; use OCP\Settings\Events\DeclarativeSettingsRegisterFormEvent; use OCP\Settings\Events\DeclarativeSettingsSetValueEvent; @@ -68,5 +70,8 @@ public function boot(IBootContext $context): void { $userManager->clearBackends(); $userManager->registerBackend($context->getAppContainer()->get(AlternativeHomeUserBackend::class)); } + + $groupManager = $server->get(IGroupManager::class); + $groupManager->addBackend($server->get(HiddenGroupBackend::class)); } } diff --git a/apps/testing/lib/HiddenGroupBackend.php b/apps/testing/lib/HiddenGroupBackend.php new file mode 100644 index 0000000000000..4f7004aae0a5a --- /dev/null +++ b/apps/testing/lib/HiddenGroupBackend.php @@ -0,0 +1,47 @@ +groupName = $groupName; + } + + public function inGroup($uid, $gid): bool { + return false; + } + + public function getUserGroups($uid): array { + return []; + } + + public function getGroups($search = '', $limit = -1, $offset = 0): array { + return $offset === 0 ? [$this->groupName] : []; + } + + public function groupExists($gid): bool { + return $gid === $this->groupName; + } + + public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0): array { + return []; + } + + public function hideGroup(string $groupId): bool { + return true; + } +} diff --git a/build/integration/config/behat.yml b/build/integration/config/behat.yml index f619216e6f88c..3007f72ea090c 100644 --- a/build/integration/config/behat.yml +++ b/build/integration/config/behat.yml @@ -80,6 +80,8 @@ default: - CommandLineContext: baseUrl: http://localhost:8080 ocPath: ../../ + - PrincipalPropertySearchContext: + baseUrl: http://localhost:8080 federation: paths: - "%paths.base%/../federation_features" diff --git a/build/integration/dav_features/principal-property-search.feature b/build/integration/dav_features/principal-property-search.feature new file mode 100644 index 0000000000000..b2195489263eb --- /dev/null +++ b/build/integration/dav_features/principal-property-search.feature @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +Feature: principal-property-search + Background: + Given user "user0" exists + Given As an "admin" + Given invoking occ with "app:enable --force testing" + + Scenario: Find a principal by a given displayname + When searching for a principal matching "user0" + Then The search HTTP status code should be "207" + And The search response should contain "/remote.php/dav/principals/users/user0/" diff --git a/build/integration/features/bootstrap/PrincipalPropertySearchContext.php b/build/integration/features/bootstrap/PrincipalPropertySearchContext.php new file mode 100644 index 0000000000000..27a4c31490044 --- /dev/null +++ b/build/integration/features/bootstrap/PrincipalPropertySearchContext.php @@ -0,0 +1,140 @@ +baseUrl = $baseUrl; + + // in case of ci deployment we take the server url from the environment + $testServerUrl = getenv('TEST_SERVER_URL'); + if ($testServerUrl !== false) { + $this->baseUrl = substr($testServerUrl, 0, -5); + } + } + + /** @BeforeScenario */ + public function setUpScenario(): void { + $this->client = $this->createGuzzleInstance(); + } + + /** + * Create a Guzzle client with a higher truncateAt value to read full error responses. + */ + private function createGuzzleInstance(): Client { + $bodySummarizer = new BodySummarizer(2048); + + $stack = new HandlerStack(Utils::chooseHandler()); + $stack->push(Middleware::httpErrors($bodySummarizer), 'http_errors'); + $stack->push(Middleware::redirect(), 'allow_redirects'); + $stack->push(Middleware::cookies(), 'cookies'); + $stack->push(Middleware::prepareBody(), 'prepare_body'); + + return new Client(['handler' => $stack]); + } + + /** + * @When searching for a principal matching :match + * @param string $match + * @throws \Exception + */ + public function principalPropertySearch(string $match) { + $davUrl = $this->baseUrl . '/remote.php/dav/'; + $user = 'admin'; + $password = 'admin'; + + $this->response = $this->client->request( + 'REPORT', + $davUrl, + [ + 'body' => ' + + + + + + ' . $match . ' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +', + 'auth' => [ + $user, + $password, + ], + 'headers' => [ + 'Content-Type' => 'application/xml; charset=UTF-8', + 'Depth' => '0', + ], + ] + ); + } + + /** + * @Then The search HTTP status code should be :code + * @param string $code + * @throws \Exception + */ + public function theHttpStatusCodeShouldBe(string $code): void { + if ((int)$code !== $this->response->getStatusCode()) { + throw new \Exception('Expected ' . (int)$code . ' got ' . $this->response->getStatusCode()); + } + } + + /** + * @Then The search response should contain :needle + * @param string $needle + * @throws \Exception + */ + public function theResponseShouldContain(string $needle): void { + $body = $this->response->getBody()->getContents(); + + if (str_contains($body, $needle) === false) { + throw new \Exception('Response does not contain "' . $needle . '"'); + } + } +} diff --git a/build/integration/features/provisioning-v1.feature b/build/integration/features/provisioning-v1.feature index bffe7a5887fb1..6ace70a25beed 100644 --- a/build/integration/features/provisioning-v1.feature +++ b/build/integration/features/provisioning-v1.feature @@ -452,6 +452,7 @@ Feature: provisioning Then groups returned are | EspaƱa | | admin | + | hidden_group | | new-group | Scenario: create a subadmin