Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 56 additions & 23 deletions build/integration/features/bootstrap/Provisioning.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

use Behat\Gherkin\Node\TableNode;
use GuzzleHttp\Client;
use GuzzleHttp\Message\ResponseInterface;
use PHPUnit\Framework\Assert;
Expand Down Expand Up @@ -124,7 +126,7 @@ public function creatingTheUser($user, $displayname = '') {
* @Then /^user "([^"]*)" has$/
*
* @param string $user
* @param \Behat\Gherkin\Node\TableNode|null $settings
* @param TableNode|null $settings
*/
public function userHasSetting($user, $settings) {
$fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user";
Expand All @@ -145,12 +147,43 @@ public function userHasSetting($user, $settings) {
if (isset($value['element']) && in_array($setting[0], ['additional_mail', 'additional_mailScope'], true)) {
$expectedValues = explode(';', $setting[1]);
foreach ($expectedValues as $expected) {
Assert::assertTrue(in_array($expected, $value['element'], true));
Assert::assertTrue(in_array($expected, $value['element'], true), 'Data wrong for field: ' . $setting[0]);
}
} elseif (isset($value[0])) {
Assert::assertEqualsCanonicalizing($setting[1], $value[0]);
Assert::assertEqualsCanonicalizing($setting[1], $value[0], 'Data wrong for field: ' . $setting[0]);
} else {
Assert::assertEquals('', $setting[1]);
Assert::assertEquals('', $setting[1], 'Data wrong for field: ' . $setting[0]);
}
}
}

/**
* @Then /^user "([^"]*)" has the following profile data$/
*/
public function userHasProfileData(string $user, ?TableNode $settings): void {
$fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/profile/$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',
'Accept' => 'application/json',
];

$response = $client->get($fullUrl, $options);
$body = $response->getBody()->getContents();
$data = json_decode($body, true);
$data = $data['ocs']['data'];
foreach ($settings->getRows() as $setting) {
Assert::assertArrayHasKey($setting[0], $data, 'Profile data field missing: ' . $setting[0]);
if ($setting[1] === 'NULL') {
Assert::assertNull($data[$setting[0]], 'Profile data wrong for field: ' . $setting[0]);
} else {
Assert::assertEquals($setting[1], $data[$setting[0]], 'Profile data wrong for field: ' . $setting[0]);
}
}
}
Expand All @@ -159,7 +192,7 @@ public function userHasSetting($user, $settings) {
* @Then /^group "([^"]*)" has$/
*
* @param string $user
* @param \Behat\Gherkin\Node\TableNode|null $settings
* @param TableNode|null $settings
*/
public function groupHasSetting($group, $settings) {
$fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/groups/details?search=$group";
Expand Down Expand Up @@ -191,7 +224,7 @@ public function groupHasSetting($group, $settings) {
* @Then /^user "([^"]*)" has editable fields$/
*
* @param string $user
* @param \Behat\Gherkin\Node\TableNode|null $fields
* @param TableNode|null $fields
*/
public function userHasEditableFields($user, $fields) {
$fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/user/fields";
Expand Down Expand Up @@ -221,9 +254,9 @@ public function userHasEditableFields($user, $fields) {
* @Then /^search users by phone for region "([^"]*)" with$/
*
* @param string $user
* @param \Behat\Gherkin\Node\TableNode|null $settings
* @param TableNode|null $settings
*/
public function searchUserByPhone($region, \Behat\Gherkin\Node\TableNode $searchTable) {
public function searchUserByPhone($region, TableNode $searchTable) {
$fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/search/by-phone";
$client = new Client();
$options = [];
Expand Down Expand Up @@ -624,10 +657,10 @@ public function userIsNotSubadminOfGroup($user, $group) {

/**
* @Then /^users returned are$/
* @param \Behat\Gherkin\Node\TableNode|null $usersList
* @param TableNode|null $usersList
*/
public function theUsersShouldBe($usersList) {
if ($usersList instanceof \Behat\Gherkin\Node\TableNode) {
if ($usersList instanceof TableNode) {
$users = $usersList->getRows();
$usersSimplified = $this->simplifyArray($users);
$respondedArray = $this->getArrayOfUsersResponded($this->response);
Expand All @@ -637,10 +670,10 @@ public function theUsersShouldBe($usersList) {

/**
* @Then /^phone matches returned are$/
* @param \Behat\Gherkin\Node\TableNode|null $usersList
* @param TableNode|null $usersList
*/
public function thePhoneUsersShouldBe($usersList) {
if ($usersList instanceof \Behat\Gherkin\Node\TableNode) {
if ($usersList instanceof TableNode) {
$users = $usersList->getRowsHash();
$listCheckedElements = simplexml_load_string($this->response->getBody())->data;
$respondedArray = json_decode(json_encode($listCheckedElements), true);
Expand All @@ -650,10 +683,10 @@ public function thePhoneUsersShouldBe($usersList) {

/**
* @Then /^detailed users returned are$/
* @param \Behat\Gherkin\Node\TableNode|null $usersList
* @param TableNode|null $usersList
*/
public function theDetailedUsersShouldBe($usersList) {
if ($usersList instanceof \Behat\Gherkin\Node\TableNode) {
if ($usersList instanceof TableNode) {
$users = $usersList->getRows();
$usersSimplified = $this->simplifyArray($users);
$respondedArray = $this->getArrayOfDetailedUsersResponded($this->response);
Expand All @@ -664,10 +697,10 @@ public function theDetailedUsersShouldBe($usersList) {

/**
* @Then /^groups returned are$/
* @param \Behat\Gherkin\Node\TableNode|null $groupsList
* @param TableNode|null $groupsList
*/
public function theGroupsShouldBe($groupsList) {
if ($groupsList instanceof \Behat\Gherkin\Node\TableNode) {
if ($groupsList instanceof TableNode) {
$groups = $groupsList->getRows();
$groupsSimplified = $this->simplifyArray($groups);
$respondedArray = $this->getArrayOfGroupsResponded($this->response);
Expand All @@ -677,10 +710,10 @@ public function theGroupsShouldBe($groupsList) {

/**
* @Then /^subadmin groups returned are$/
* @param \Behat\Gherkin\Node\TableNode|null $groupsList
* @param TableNode|null $groupsList
*/
public function theSubadminGroupsShouldBe($groupsList) {
if ($groupsList instanceof \Behat\Gherkin\Node\TableNode) {
if ($groupsList instanceof TableNode) {
$groups = $groupsList->getRows();
$groupsSimplified = $this->simplifyArray($groups);
$respondedArray = $this->getArrayOfSubadminsResponded($this->response);
Expand All @@ -690,10 +723,10 @@ public function theSubadminGroupsShouldBe($groupsList) {

/**
* @Then /^apps returned are$/
* @param \Behat\Gherkin\Node\TableNode|null $appList
* @param TableNode|null $appList
*/
public function theAppsShouldBe($appList) {
if ($appList instanceof \Behat\Gherkin\Node\TableNode) {
if ($appList instanceof TableNode) {
$apps = $appList->getRows();
$appsSimplified = $this->simplifyArray($apps);
$respondedArray = $this->getArrayOfAppsResponded($this->response);
Expand All @@ -703,7 +736,7 @@ public function theAppsShouldBe($appList) {

/**
* @Then /^subadmin users returned are$/
* @param \Behat\Gherkin\Node\TableNode|null $groupsList
* @param TableNode|null $groupsList
*/
public function theSubadminUsersShouldBe($groupsList) {
$this->theSubadminGroupsShouldBe($groupsList);
Expand Down Expand Up @@ -882,7 +915,7 @@ public function userIsEnabled($user) {
* @param string $quota
*/
public function userHasAQuotaOf($user, $quota) {
$body = new \Behat\Gherkin\Node\TableNode([
$body = new TableNode([
0 => ['key', 'quota'],
1 => ['value', $quota],
]);
Expand Down Expand Up @@ -950,7 +983,7 @@ public function cleanupGroups() {
/**
* @Then /^user "([^"]*)" has not$/
*/
public function userHasNotSetting($user, \Behat\Gherkin\Node\TableNode $settings) {
public function userHasNotSetting($user, TableNode $settings) {
$fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user";
$client = new Client();
$options = [];
Expand Down
17 changes: 17 additions & 0 deletions build/integration/features/provisioning-v1.feature
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,23 @@ Feature: provisioning
| address | Foo Bar Town |
| website | https://nextcloud.com |
| twitter | Nextcloud |
And sending "PUT" to "/cloud/users/brand-new-user" with
| key | organisation |
| value | Nextcloud GmbH |
And sending "PUT" to "/cloud/users/brand-new-user" with
| key | role |
| value | Engineer |
And the OCS status code should be "100"
And the HTTP status code should be "200"
Then user "brand-new-user" has the following profile data
| userId | brand-new-user |
| displayname | Brand New User |
| organisation | Nextcloud GmbH |
| role | Engineer |
| address | Foo Bar Town |
| timezone | UTC |
| timezoneOffset | 0 |
| pronouns | NULL |

Scenario: Edit a user account properties scopes
Given user "brand-new-user" exists
Expand Down
74 changes: 68 additions & 6 deletions core/Controller/ProfileApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
namespace OC\Core\Controller;

use OC\Core\Db\ProfileConfigMapper;
use OC\Core\ResponseDefinitions;
use OC\Profile\ProfileManager;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\ApiRoute;
use OCP\AppFramework\Http\Attribute\BruteForceProtection;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired;
use OCP\AppFramework\Http\Attribute\UserRateLimit;
Expand All @@ -21,17 +23,27 @@
use OCP\AppFramework\OCS\OCSForbiddenException;
use OCP\AppFramework\OCS\OCSNotFoundException;
use OCP\AppFramework\OCSController;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
use OCP\IRequest;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\Share\IManager;

/**
* @psalm-import-type CoreProfileData from ResponseDefinitions
*/
class ProfileApiController extends OCSController {
public function __construct(
IRequest $request,
private IConfig $config,
private ITimeFactory $timeFactory,
private ProfileConfigMapper $configMapper,
private ProfileManager $profileManager,
private IUserManager $userManager,
private IUserSession $userSession,
private IManager $shareManager,
) {
parent::__construct('core', $request);
}
Expand All @@ -57,14 +69,13 @@ public function __construct(
#[ApiRoute(verb: 'PUT', url: '/{targetUserId}', root: '/profile')]
public function setVisibility(string $targetUserId, string $paramId, string $visibility): DataResponse {
$requestingUser = $this->userSession->getUser();
$targetUser = $this->userManager->get($targetUserId);

if (!$this->userManager->userExists($targetUserId)) {
throw new OCSNotFoundException('Account does not exist');
if ($requestingUser->getUID() !== $targetUserId) {
throw new OCSForbiddenException('People can only edit their own visibility settings');
}

if ($requestingUser !== $targetUser) {
throw new OCSForbiddenException('People can only edit their own visibility settings');
$targetUser = $this->userManager->get($targetUserId);
if (!$targetUser instanceof IUser) {
throw new OCSNotFoundException('Account does not exist');
}

// Ensure that a profile config is created in the database
Expand All @@ -80,4 +91,55 @@ public function setVisibility(string $targetUserId, string $paramId, string $vis

return new DataResponse();
}

/**
* Get profile fields for another user
*
* @param string $targetUserId ID of the user
* @return DataResponse<Http::STATUS_OK, CoreProfileData, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND, null, array{}>
*
* 200: Profile data returned successfully
* 400: Profile is disabled
* 404: Account not found or disabled
*/
#[NoAdminRequired]
#[ApiRoute(verb: 'GET', url: '/{targetUserId}', root: '/profile')]
#[BruteForceProtection(action: 'user')]
#[UserRateLimit(limit: 30, period: 120)]
public function getProfileFields(string $targetUserId): DataResponse {
$targetUser = $this->userManager->get($targetUserId);
if (!$targetUser instanceof IUser) {
$response = new DataResponse(null, Http::STATUS_NOT_FOUND);
$response->throttle();
return $response;
}
if (!$targetUser->isEnabled()) {
return new DataResponse(null, Http::STATUS_NOT_FOUND);
}

if (!$this->profileManager->isProfileEnabled($targetUser)) {
return new DataResponse(null, Http::STATUS_BAD_REQUEST);
}

$requestingUser = $this->userSession->getUser();
if ($targetUser !== $requestingUser) {
if (!$this->shareManager->currentUserCanEnumerateTargetUser($requestingUser, $targetUser)) {
return new DataResponse(null, Http::STATUS_NOT_FOUND);
}
}

$profileFields = $this->profileManager->getProfileFields($targetUser, $requestingUser);

// Extend the profile information with timezone of the user
$timezoneStringTarget = $this->config->getUserValue($targetUser->getUID(), 'core', 'timezone') ?: $this->config->getSystemValueString('default_timezone', 'UTC');
try {
$timezoneTarget = new \DateTimeZone($timezoneStringTarget);
} catch (\Throwable) {
$timezoneTarget = new \DateTimeZone('UTC');
}
$profileFields['timezone'] = $timezoneTarget->getName(); // E.g. Europe/Berlin
$profileFields['timezoneOffset'] = $timezoneTarget->getOffset($this->timeFactory->now()); // In seconds E.g. 7200

return new DataResponse($profileFields);
}
}
25 changes: 25 additions & 0 deletions core/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,31 @@
* endedAt: ?int,
* }
*
* @psalm-type CoreProfileAction = array{
* id: string,
* icon: string,
* title: string,
* target: ?string,
* }
*
* @psalm-type CoreProfileFields = array{
* userId: string,
* address?: string|null,
* biography?: string|null,
* displayname?: string|null,
* headline?: string|null,
* isUserAvatarVisible?: bool,
* organisation?: string|null,
* pronouns?: string|null,
* role?: string|null,
* actions: list<CoreProfileAction>,
* }
*
* @psalm-type CoreProfileData = CoreProfileFields&array{
* timezone: string,
* timezoneOffset: int,
* }
*
*/
class ResponseDefinitions {
}
Loading
Loading