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
18 changes: 18 additions & 0 deletions app/config/collections/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,17 @@
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('impersonator'),
'type' => Database::VAR_BOOLEAN,
'signed' => true,
'size' => 0,
'format' => '',
'filters' => [],
'required' => false,
'default' => false,
'array' => false,
],
],
'indexes' => [
[
Expand Down Expand Up @@ -491,6 +502,13 @@
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('impersonator'),
'type' => Database::INDEX_KEY,
'attributes' => [ID::custom('impersonator')],
'lengths' => [],
Comment thread
eldadfux marked this conversation as resolved.
'orders' => [],
],
],
],

Expand Down
3 changes: 3 additions & 0 deletions app/config/cors.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
'X-Appwrite-Timestamp',
'X-Appwrite-Session',
'X-Appwrite-Platform',
'X-Appwrite-Impersonate-User-Id',
'X-Appwrite-Impersonate-User-Email',
'X-Appwrite-Impersonate-User-Phone',
// SDK generator
'X-SDK-Version',
'X-SDK-Name',
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/api/account.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
use Appwrite\Event\Messaging;
use Appwrite\Extend\Exception;
use Appwrite\Hooks\Hooks;
use Appwrite\Network\Validator\Email as EmailValidator;
use Appwrite\Network\Validator\Redirect;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\SDK\AuthType;
Expand Down Expand Up @@ -60,6 +59,7 @@
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Emails\Email;
use Utopia\Emails\Validator\Email as EmailValidator;
use Utopia\Http\Http;
use Utopia\Locale\Locale;
use Utopia\Storage\Validator\FileName;
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/api/messaging.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
use Appwrite\Event\Messaging;
use Appwrite\Extend\Exception;
use Appwrite\Messaging\Status as MessageStatus;
use Appwrite\Network\Validator\Email;
use Appwrite\Permission;
use Appwrite\Role;
use Appwrite\SDK\AuthType;
Expand Down Expand Up @@ -43,6 +42,7 @@
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\Roles;
use Utopia\Database\Validator\UID;
use Utopia\Emails\Validator\Email;
use Utopia\Http\Http;
use Utopia\Locale\Locale;
use Utopia\System\System;
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/api/projects.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
use Appwrite\Event\Mail;
use Appwrite\Extend\Exception;
use Appwrite\Network\Platform;
use Appwrite\Network\Validator\Email;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Deprecated;
Expand All @@ -29,6 +28,7 @@
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Emails\Validator\Email;
use Utopia\Http\Http;
use Utopia\Locale\Locale;
use Utopia\System\System;
Expand Down
43 changes: 42 additions & 1 deletion app/controllers/api/users.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Hooks\Hooks;
use Appwrite\Network\Validator\Email as EmailValidator;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Deprecated;
Expand Down Expand Up @@ -60,6 +59,7 @@
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Emails\Email;
use Utopia\Emails\Validator\Email as EmailValidator;
use Utopia\Http\Http;
use Utopia\Locale\Locale;
use Utopia\System\System;
Expand Down Expand Up @@ -1212,6 +1212,47 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
$response->dynamic($user, Response::MODEL_USER);
});

Http::patch('/v1/users/:userId/impersonator')
->desc('Update user impersonator capability')
->groups(['api', 'users'])
->label('event', 'users.[userId].update.impersonator')
->label('scope', 'users.write')
->label('audits.event', 'user.update')
->label('audits.resource', 'user/{response.$id}')
->label('sdk', new Method(
namespace: 'users',
group: 'users',
name: 'updateImpersonator',
description: '/docs/references/users/update-user-impersonator.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_USER,
)
]
))
->param('userId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'User ID.', false, ['dbForProject'])
->param('impersonator', false, new Boolean(true), 'Whether the user can impersonate other users. When true, the user can browse project users to choose a target and can pass impersonation headers to act as that user. Internal audit logs still attribute impersonated actions to the original impersonator and store the target user details only in internal audit payload data.')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $userId, bool $impersonator, Response $response, Database $dbForProject, Event $queueForEvents) {

$user = $dbForProject->getDocument('users', $userId);

if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}

$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['impersonator' => $impersonator]));

$queueForEvents
->setParam('userId', $user->getId());

$response->dynamic($user, Response::MODEL_USER);
});

Http::patch('/v1/users/:userId/verification/phone')
->desc('Update phone verification')
->groups(['api', 'users'])
Expand Down
18 changes: 17 additions & 1 deletion app/controllers/shared/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,19 @@

$scopes = \array_unique($scopes);

// Intentional: impersonators get users.read so they can discover a target user
// before impersonation starts, and keep that access while impersonating.
if (
!$user->isEmpty()
&& (
$user->getAttribute('impersonator', false)
|| $user->getAttribute('impersonatorUserId')
)
) {
$scopes[] = 'users.read';
$scopes = \array_unique($scopes);
}

$authorization->addRole($role);
foreach ($user->getRoles($authorization) as $authRole) {
$authorization->addRole($authRole);
Expand Down Expand Up @@ -369,8 +382,11 @@
}

if (! empty($user->getId())) {
$impersonatorUserId = $user->getAttribute('impersonatorUserId');
$accessedAt = $user->getAttribute('accessedAt', 0);
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCESS)) > $accessedAt) {

// Skip updating accessedAt for impersonated requests so we don't attribute activity to the target user.
if (! $impersonatorUserId && DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCESS)) > $accessedAt) {
$user->setAttribute('accessedAt', DateTime::now());

if ($project->getId() !== 'console' && $mode !== APP_MODE_ADMIN) {
Expand Down
2 changes: 1 addition & 1 deletion app/init/database/formats.php
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<?php

use Appwrite\Network\Validator\Email;
use Utopia\Database\Database;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\Structure;
use Utopia\Emails\Validator\Email;
use Utopia\Validator\IP;
use Utopia\Validator\Range;
use Utopia\Validator\URL;
Expand Down
25 changes: 25 additions & 0 deletions app/init/resources.php
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,31 @@
}
}

// Impersonation: if current user has impersonator capability and headers are set, act as another user
$impersonateUserId = $request->getHeader('x-appwrite-impersonate-user-id', '');
$impersonateEmail = $request->getHeader('x-appwrite-impersonate-user-email', '');
$impersonatePhone = $request->getHeader('x-appwrite-impersonate-user-phone', '');
if (!$user->isEmpty() && $user->getAttribute('impersonator', false)) {
$userDb = (APP_MODE_ADMIN === $mode || $project->getId() === 'console') ? $dbForPlatform : $dbForProject;
$targetUser = null;
if (!empty($impersonateUserId)) {
$targetUser = $userDb->getAuthorization()->skip(fn () => $userDb->getDocument('users', $impersonateUserId));
} elseif (!empty($impersonateEmail)) {
$targetUser = $userDb->getAuthorization()->skip(fn () => $userDb->findOne('users', [Query::equal('email', [\strtolower($impersonateEmail)])]));
} elseif (!empty($impersonatePhone)) {
$targetUser = $userDb->getAuthorization()->skip(fn () => $userDb->findOne('users', [Query::equal('phone', [$impersonatePhone])]));
Comment thread
eldadfux marked this conversation as resolved.
Comment thread
Meldiron marked this conversation as resolved.
}
if ($targetUser !== null && !$targetUser->isEmpty()) {
$impersonator = clone $user;
$user = clone $targetUser;
$user->setAttribute('impersonatorUserId', $impersonator->getId());
$user->setAttribute('impersonatorUserInternalId', $impersonator->getSequence());
$user->setAttribute('impersonatorUserName', $impersonator->getAttribute('name', ''));
$user->setAttribute('impersonatorUserEmail', $impersonator->getAttribute('email', ''));
$user->setAttribute('impersonatorAccessedAt', $impersonator->getAttribute('accessedAt', 0));
}
Comment thread
Meldiron marked this conversation as resolved.
Comment thread
Meldiron marked this conversation as resolved.
}
Comment thread
eldadfux marked this conversation as resolved.

$dbForProject->setMetadata('user', $user->getId());
$dbForPlatform->setMetadata('user', $user->getId());
Comment thread
eldadfux marked this conversation as resolved.

Expand Down
1 change: 1 addition & 0 deletions docs/references/users/update-user-impersonator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Enable or disable whether a user can impersonate other users. When impersonation headers are used, the request runs as the target user for API behavior, while internal audit logs still attribute the action to the original impersonator and store the impersonated target details only in internal audit payload data.
12 changes: 0 additions & 12 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1200,18 +1200,6 @@ parameters:
count: 1
path: src/Appwrite/Utopia/Database/Documents/User.php

-
message: '#^Call to method isValid\(\) on an unknown class Utopia\\Validator\\Email\.$#'
identifier: class.notFound
count: 1
path: src/Appwrite/Utopia/Database/Validator/Attributes.php

-
message: '#^Instantiated class Utopia\\Validator\\Email not found\.$#'
identifier: class.notFound
count: 1
path: src/Appwrite/Utopia/Database/Validator/Attributes.php

-
message: '#^Unsafe call to private method Appwrite\\Utopia\\Request\\Filters\\V17\:\:appendSymbol\(\) through static\:\:\.$#'
identifier: staticClassAccess.privateMethod
Expand Down
2 changes: 1 addition & 1 deletion src/Appwrite/GraphQL/Types/Mapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ public static function param(
case \Appwrite\Event\Validator\Event::class:
case \Appwrite\Event\Validator\FunctionEvent::class:
case \Appwrite\Network\Validator\CNAME::class:
case \Appwrite\Network\Validator\Email::class:
case \Utopia\Emails\Validator\Email::class:
case \Appwrite\Network\Validator\Redirect::class:
case \Appwrite\Network\Validator\DNS::class:
case \Appwrite\Network\Validator\Origin::class:
Expand Down
79 changes: 0 additions & 79 deletions src/Appwrite/Network/Validator/Email.php

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Network\Validator\Email;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Deprecated;
Expand All @@ -16,6 +15,7 @@
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Emails\Validator\Email;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Email;

use Appwrite\Event\Event;
use Appwrite\Network\Validator\Email;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
Expand All @@ -15,6 +14,7 @@
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Emails\Validator\Email;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Email;

use Appwrite\Network\Validator\Email;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Email\Create as EmailCreate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
Expand All @@ -11,6 +10,7 @@
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Emails\Validator\Email;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
Expand Down
Loading
Loading