diff --git a/apps/provisioning_api/appinfo/routes.php b/apps/provisioning_api/appinfo/routes.php index 8cf57487d3aba..01b6260c1d58f 100644 --- a/apps/provisioning_api/appinfo/routes.php +++ b/apps/provisioning_api/appinfo/routes.php @@ -54,6 +54,7 @@ ['root' => '/cloud', 'name' => 'Users#getEditableFields', 'url' => '/user/fields', 'verb' => 'GET'], ['root' => '/cloud', 'name' => 'Users#getEditableFieldsForUser', 'url' => '/user/fields/{userId}', 'verb' => 'GET'], ['root' => '/cloud', 'name' => 'Users#editUser', 'url' => '/users/{userId}', 'verb' => 'PUT'], + ['root' => '/cloud', 'name' => 'Users#editUserMultiValue', 'url' => '/users/{userId}/{collectionName}', 'verb' => 'PUT', 'requirements' => ['collectionName' => '[^(enable|disable)]']], ['root' => '/cloud', 'name' => 'Users#wipeUserDevices', 'url' => '/users/{userId}/wipe', 'verb' => 'POST'], ['root' => '/cloud', 'name' => 'Users#deleteUser', 'url' => '/users/{userId}', 'verb' => 'DELETE'], ['root' => '/cloud', 'name' => 'Users#enableUser', 'url' => '/users/{userId}/enable', 'verb' => 'PUT'], diff --git a/apps/provisioning_api/lib/Controller/UsersController.php b/apps/provisioning_api/lib/Controller/UsersController.php index 6c17648b9ea3f..709f139d83068 100644 --- a/apps/provisioning_api/lib/Controller/UsersController.php +++ b/apps/provisioning_api/lib/Controller/UsersController.php @@ -52,6 +52,7 @@ use OC\User\Backend; use OCA\Settings\Mailer\NewUserMailHelper; use OCP\Accounts\IAccountManager; +use OCP\Accounts\IAccountProperty; use OCP\App\IAppManager; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; @@ -601,6 +602,85 @@ public function getEditableFieldsForUser(string $userId): DataResponse { return new DataResponse($permittedFields); } + /** + * @throws OCSException + */ + public function editUserMultiValue( + string $userId, + string $collectionName, + string $key, + string $value + ): DataResponse { + $currentLoggedInUser = $this->userSession->getUser(); + if ($currentLoggedInUser === null) { + throw new OCSException('', OCSController::RESPOND_UNAUTHORISED); + } + + $targetUser = $this->userManager->get($userId); + if ($targetUser === null) { + throw new OCSException('', OCSController::RESPOND_NOT_FOUND); + } + + $permittedFields = []; + if ($targetUser->getUID() === $currentLoggedInUser->getUID()) { + // Editing self (display, email) + $permittedFields[] = IAccountManager::COLLECTION_EMAIL; + $permittedFields[] = IAccountManager::COLLECTION_EMAIL . self::SCOPE_SUFFIX; + } else { + // Check if admin / subadmin + $subAdminManager = $this->groupManager->getSubAdmin(); + if ($this->groupManager->isAdmin($currentLoggedInUser->getUID()) + || $subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) { + // They have permissions over the user + + $permittedFields[] = IAccountManager::COLLECTION_EMAIL; + } else { + // No rights + throw new OCSException('', OCSController::RESPOND_NOT_FOUND); + } + } + + // Check if permitted to edit this field + if (!in_array($collectionName, $permittedFields)) { + throw new OCSException('', 103); + } + + switch ($collectionName) { + case IAccountManager::COLLECTION_EMAIL: + $userAccount = $this->accountManager->getAccount($targetUser); + $mailCollection = $userAccount->getPropertyCollection(IAccountManager::COLLECTION_EMAIL); + $mailCollection->removePropertyByValue($key); + if ($value !== '') { + // "replace on" + $mailCollection->addPropertyWithDefaults($value); + } + $this->accountManager->updateAccount($userAccount); + break; + + case IAccountManager::COLLECTION_EMAIL . self::SCOPE_SUFFIX: + $userAccount = $this->accountManager->getAccount($targetUser); + $mailCollection = $userAccount->getPropertyCollection(IAccountManager::COLLECTION_EMAIL); + $targetProperty = null; + foreach ($mailCollection->getProperties() as $property) { + if ($property->getValue() === $value) { + $targetProperty = $property; + break; + } + } + if ($targetProperty instanceof IAccountProperty) { + $targetProperty->setScope($value); + $this->accountManager->updateAccount($userAccount); + } else { + throw new OCSException('', 102); + } + break; + + default: + throw new OCSException('', 103); + } + return new DataResponse(); + } + /** * @NoAdminRequired * @NoSubAdminRequired @@ -637,6 +717,8 @@ public function editUser(string $userId, string $key, string $value): DataRespon $permittedFields[] = IAccountManager::PROPERTY_DISPLAYNAME . self::SCOPE_SUFFIX; $permittedFields[] = IAccountManager::PROPERTY_EMAIL . self::SCOPE_SUFFIX; + $permittedFields[] = IAccountManager::COLLECTION_EMAIL; + $permittedFields[] = 'password'; if ($this->config->getSystemValue('force_language', false) === false || $this->groupManager->isAdmin($currentLoggedInUser->getUID())) { @@ -675,6 +757,7 @@ public function editUser(string $userId, string $key, string $value): DataRespon $permittedFields[] = IAccountManager::PROPERTY_DISPLAYNAME; } $permittedFields[] = IAccountManager::PROPERTY_EMAIL; + $permittedFields[] = IAccountManager::COLLECTION_EMAIL; $permittedFields[] = 'password'; $permittedFields[] = 'language'; $permittedFields[] = 'locale'; @@ -747,6 +830,21 @@ public function editUser(string $userId, string $key, string $value): DataRespon throw new OCSException('', 102); } break; + case IAccountManager::COLLECTION_EMAIL: + if (filter_var($value, FILTER_VALIDATE_EMAIL) && $value !== $targetUser->getEMailAddress()) { + $userAccount = $this->accountManager->getAccount($targetUser); + $mailCollection = $userAccount->getPropertyCollection(IAccountManager::COLLECTION_EMAIL); + foreach ($mailCollection->getProperties() as $property) { + if ($property->getValue() === $value) { + break; + } + } + $mailCollection->addPropertyWithDefaults($value); + $this->accountManager->updateAccount($userAccount); + } else { + throw new OCSException('', 102); + } + break; case IAccountManager::PROPERTY_PHONE: case IAccountManager::PROPERTY_ADDRESS: case IAccountManager::PROPERTY_WEBSITE: diff --git a/build/integration/features/bootstrap/Provisioning.php b/build/integration/features/bootstrap/Provisioning.php index 8eab793d66bb1..3cc1366c52c77 100644 --- a/build/integration/features/bootstrap/Provisioning.php +++ b/build/integration/features/bootstrap/Provisioning.php @@ -169,13 +169,20 @@ public function userHasSetting($user, $settings) { foreach ($settings->getRows() as $setting) { $value = json_decode(json_encode(simplexml_load_string($response->getBody())->data->{$setting[0]}), 1); if (isset($value[0])) { - Assert::assertEquals($setting[1], $value[0], "", 0.0, 10, true); + if (in_array($setting[0], ['additional_mail', 'additional_mailScope'], true)) { + $expectedValues = explode(';', $setting[1]); + foreach ($expectedValues as $expected) { + Assert::assertTrue(in_array($expected, $value, true)); + } + } else { + Assert::assertEquals($setting[1], $value[0], "", 0.0, 10, true); + } } else { Assert::assertEquals('', $setting[1]); } } } - + /** * @Then /^group "([^"]*)" has$/ * @@ -194,7 +201,7 @@ public function groupHasSetting($group, $settings) { $options['headers'] = [ 'OCS-APIREQUEST' => 'true', ]; - + $response = $client->get($fullUrl, $options); $groupDetails = simplexml_load_string($response->getBody())->data[0]->groups[0]->element; foreach ($settings->getRows() as $setting) { @@ -206,7 +213,7 @@ public function groupHasSetting($group, $settings) { } } } - + /** * @Then /^user "([^"]*)" has editable fields$/ @@ -967,4 +974,38 @@ public function cleanupGroups() { } $this->usingServer($previousServer); } + + /** + * @Then /^user "([^"]*)" has not$/ + */ + public function userHasNotSetting($user, \Behat\Gherkin\Node\TableNode $settings) { + $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user"; + $client = new Client(); + $options = []; + if ($this->currentUser === 'admin') { + $options['auth'] = $this->adminUser; + } else { + $options['auth'] = [$this->currentUser, $this->regularUser]; + } + $options['headers'] = [ + 'OCS-APIREQUEST' => 'true', + ]; + + $response = $client->get($fullUrl, $options); + foreach ($settings->getRows() as $setting) { + $value = json_decode(json_encode(simplexml_load_string($response->getBody())->data->{$setting[0]}), 1); + if (isset($value[0])) { + if (in_array($setting[0], ['additional_mail', 'additional_mailScope'], true)) { + $expectedValues = explode(';', $setting[1]); + foreach ($expectedValues as $expected) { + Assert::assertFalse(in_array($expected, $value, true)); + } + } else { + Assert::assertNotEquals($setting[1], $value[0], "", 0.0, 10, true); + } + } else { + Assert::assertNotEquals('', $setting[1]); + } + } + } } diff --git a/build/integration/features/provisioning-v1.feature b/build/integration/features/provisioning-v1.feature index d2870e34d4841..dc55f48dc6ae6 100644 --- a/build/integration/features/provisioning-v1.feature +++ b/build/integration/features/provisioning-v1.feature @@ -103,6 +103,16 @@ Feature: provisioning | value | no-reply@nextcloud.com | And the OCS status code should be "100" And the HTTP status code should be "200" + And sending "PUT" to "/cloud/users/brand-new-user" with + | key | additional_email | + | value | no.reply@nextcloud.com | + And the OCS status code should be "100" + And the HTTP status code should be "200" + And sending "PUT" to "/cloud/users/brand-new-user" with + | key | additional_email | + | value | noreply@nextcloud.com | + And the OCS status code should be "100" + And the HTTP status code should be "200" And sending "PUT" to "/cloud/users/brand-new-user" with | key | phone | | value | +49 711 / 25 24 28-90 | @@ -127,6 +137,7 @@ Feature: provisioning | id | brand-new-user | | displayname | Brand New User | | email | no-reply@nextcloud.com | + | additional_mail | no.reply@nextcloud.com;noreply@nextcloud.com | | phone | +4971125242890 | | address | Foo Bar Town | | website | https://nextcloud.com | @@ -180,6 +191,33 @@ Feature: provisioning | displaynameScope | v2-federated | | avatarScope | v2-local | + Scenario: Edit a user account multivalue property scopes + Given user "brand-new-user" exists + And As an "brand-new-user" + When sending "PUT" to "/cloud/users/brand-new-user" with + | key | additional_email | + | value | no.reply@nextcloud.com | + And the OCS status code should be "100" + And the HTTP status code should be "200" + And sending "PUT" to "/cloud/users/brand-new-user" with + | key | additional_email | + | value | noreply@nextcloud.com | + And the OCS status code should be "100" + And the HTTP status code should be "200" + When sending "PUT" to "/cloud/users/brand-new-user/additional_emailScope" with + | key | no.reply@nextcloud.com | + | value | v2-federated | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + When sending "PUT" to "/cloud/users/brand-new-user/additional_emailScope" with + | key | noreply@nextcloud.com | + | value | v2-published | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + Then user "brand-new-user" has + | id | brand-new-user | + | additional_emailScope | v2-federated;v2-published | + Scenario: Edit a user account properties scopes with invalid or unsupported value Given user "brand-new-user" exists And As an "brand-new-user" @@ -199,6 +237,43 @@ Feature: provisioning Then the OCS status code should be "102" And the HTTP status code should be "200" + Scenario: Edit a user account multi-value property scopes with invalid or unsupported value + Given user "brand-new-user" exists + And As an "brand-new-user" + When sending "PUT" to "/cloud/users/brand-new-user" with + | key | additional_email | + | value | no.reply@nextcloud.com | + And the OCS status code should be "100" + And the HTTP status code should be "200" + When sending "PUT" to "/cloud/users/brand-new-user/additional_emailScope" with + | key | no.reply@nextcloud.com | + | value | invalid | + Then the OCS status code should be "102" + And the HTTP status code should be "200" + + Scenario: Delete a user account multi-value property value + Given user "brand-new-user" exists + And As an "brand-new-user" + When sending "PUT" to "/cloud/users/brand-new-user" with + | key | additional_email | + | value | no.reply@nextcloud.com | + And the OCS status code should be "100" + And the HTTP status code should be "200" + And sending "PUT" to "/cloud/users/brand-new-user" with + | key | additional_email | + | value | noreply@nextcloud.com | + And the OCS status code should be "100" + And the HTTP status code should be "200" + When sending "PUT" to "/cloud/users/brand-new-user/additional_email" with + | key | no.reply@nextcloud.com | + | value | | + And the OCS status code should be "100" + And the HTTP status code should be "200" + Then user "brand-new-user" has + | additional_email | noreply@nextcloud.com | + Then user "brand-new-user" has not + | additional_email | no.reply@nextcloud.com | + Scenario: An admin cannot edit user account property scopes Given As an "admin" And user "brand-new-user" exists diff --git a/lib/private/Accounts/AccountPropertyCollection.php b/lib/private/Accounts/AccountPropertyCollection.php index 84e10e6a507af..eb92536a6a0a5 100644 --- a/lib/private/Accounts/AccountPropertyCollection.php +++ b/lib/private/Accounts/AccountPropertyCollection.php @@ -27,6 +27,7 @@ namespace OC\Accounts; use InvalidArgumentException; +use OCP\Accounts\IAccountManager; use OCP\Accounts\IAccountProperty; use OCP\Accounts\IAccountPropertyCollection; @@ -63,6 +64,18 @@ public function addProperty(IAccountProperty $property): IAccountPropertyCollect return $this; } + public function addPropertyWithDefaults(string $value): IAccountPropertyCollection { + $property = new AccountProperty( + $this->collectionName, + $value, + IAccountManager::SCOPE_LOCAL, + IAccountManager::NOT_VERIFIED, + '' + ); + $this->addProperty($property); + return $this; + } + public function removeProperty(IAccountProperty $property): IAccountPropertyCollection { $ref = array_search($property, $this->properties, true); if ($ref !== false) { diff --git a/lib/public/Accounts/IAccountPropertyCollection.php b/lib/public/Accounts/IAccountPropertyCollection.php index 9e026f4ce5b2b..779fb1299b4cd 100644 --- a/lib/public/Accounts/IAccountPropertyCollection.php +++ b/lib/public/Accounts/IAccountPropertyCollection.php @@ -68,6 +68,14 @@ public function getProperties(): array; */ public function addProperty(IAccountProperty $property): IAccountPropertyCollection; + /** + * adds a property to this collection with only specifying the value + * + * @throws InvalidArgumentException + * @since 22.0.0 + */ + public function addPropertyWithDefaults(string $value): IAccountPropertyCollection; + /** * removes a property of this collection *