mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge pull request #10758 from appwrite/feat-appwrite-auth
Feat: utopia auth
This commit is contained in:
@@ -9,6 +9,7 @@ use Appwrite\Event\StatsResources;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Platform\Appwrite;
|
||||
use Appwrite\Runtimes\Runtimes;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Executor\Executor;
|
||||
use Swoole\Runtime;
|
||||
use Swoole\Timer;
|
||||
@@ -76,6 +77,7 @@ CLI::setResource('dbForPlatform', function ($pools, $cache) {
|
||||
->setNamespace('_console')
|
||||
->setMetadata('host', \gethostname())
|
||||
->setMetadata('project', 'console');
|
||||
$dbForPlatform->setDocumentType('users', User::class);
|
||||
|
||||
// Ensure tables exist
|
||||
$collections = Config::getParam('collections', [])['console'];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Utopia\Auth\Hashes\Argon2;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
|
||||
@@ -173,7 +173,7 @@ return [
|
||||
'size' => 256,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => Auth::DEFAULT_ALGO,
|
||||
'default' => (new Argon2())->getName(),
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
@@ -184,7 +184,7 @@ return [
|
||||
'size' => 65535,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => Auth::DEFAULT_ALGO_OPTIONS,
|
||||
'default' => (new Argon2())->getOptions(),
|
||||
'array' => false,
|
||||
'filters' => ['json'],
|
||||
],
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* Initializes console project document.
|
||||
*/
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Network\Platform;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\System\System;
|
||||
@@ -38,7 +37,7 @@ $console = [
|
||||
'mockNumbers' => [],
|
||||
'invites' => System::getEnv('_APP_CONSOLE_INVITES', 'enabled') === 'enabled',
|
||||
'limit' => (System::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled') === 'enabled') ? 1 : 0, // limit signup to 1 user
|
||||
'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds
|
||||
'duration' => TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds
|
||||
'sessionAlerts' => System::getEnv('_APP_CONSOLE_SESSION_ALERTS', 'disabled') === 'enabled',
|
||||
'invalidateSessions' => true
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
|
||||
$member = [
|
||||
'global',
|
||||
@@ -92,7 +92,7 @@ $admins = [
|
||||
];
|
||||
|
||||
return [
|
||||
Auth::USER_ROLE_GUESTS => [
|
||||
User::ROLE_GUESTS => [
|
||||
'label' => 'Guests',
|
||||
'scopes' => [
|
||||
'global',
|
||||
@@ -112,23 +112,23 @@ return [
|
||||
'execution.write',
|
||||
],
|
||||
],
|
||||
Auth::USER_ROLE_USERS => [
|
||||
User::ROLE_USERS => [
|
||||
'label' => 'Users',
|
||||
'scopes' => \array_merge($member),
|
||||
],
|
||||
Auth::USER_ROLE_ADMIN => [
|
||||
User::ROLE_ADMIN => [
|
||||
'label' => 'Admin',
|
||||
'scopes' => \array_merge($admins),
|
||||
],
|
||||
Auth::USER_ROLE_DEVELOPER => [
|
||||
User::ROLE_DEVELOPER => [
|
||||
'label' => 'Developer',
|
||||
'scopes' => \array_merge($admins),
|
||||
],
|
||||
Auth::USER_ROLE_OWNER => [
|
||||
User::ROLE_OWNER => [
|
||||
'label' => 'Owner',
|
||||
'scopes' => \array_merge($member, $admins),
|
||||
],
|
||||
Auth::USER_ROLE_APPS => [
|
||||
User::ROLE_APPS => [
|
||||
'label' => 'Applications',
|
||||
'scopes' => ['global', 'health.read', 'graphql'],
|
||||
],
|
||||
|
||||
+277
-188
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Extend\Exception as AppwriteException;
|
||||
use Appwrite\GraphQL\Promises\Adapter;
|
||||
@@ -9,6 +8,7 @@ use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\MethodType;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use GraphQL\Error\DebugFlag;
|
||||
@@ -32,7 +32,7 @@ App::init()
|
||||
if (
|
||||
array_key_exists('graphql', $project->getAttribute('apis', []))
|
||||
&& !$project->getAttribute('apis', [])['graphql']
|
||||
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
|
||||
&& !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles()))
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?php
|
||||
|
||||
use Ahc\Jwt\JWT;
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Auth\Validator\MockNumber;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Mail;
|
||||
@@ -119,7 +118,7 @@ App::post('/v1/projects')
|
||||
'maxSessions' => APP_LIMIT_USER_SESSIONS_DEFAULT,
|
||||
'passwordHistory' => 0,
|
||||
'passwordDictionary' => false,
|
||||
'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG,
|
||||
'duration' => TOKEN_EXPIRATION_LOGIN_LONG,
|
||||
'personalDataCheck' => false,
|
||||
'mockNumbers' => [],
|
||||
'sessionAlerts' => false,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
use Ahc\Jwt\JWT;
|
||||
use Ahc\Jwt\JWTException;
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\ClamAV\Network;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
@@ -13,6 +12,7 @@ use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\MethodType;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Buckets;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Files;
|
||||
@@ -437,8 +437,8 @@ App::post('/v1/storage/buckets/:bucketId/files')
|
||||
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAPIKey = User::isApp(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
@@ -470,7 +470,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
|
||||
|
||||
// Users can only manage their own roles, API keys and Admin users can manage any
|
||||
$roles = Authorization::getRoles();
|
||||
if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) {
|
||||
if (!User::isApp($roles) && !User::isPrivileged($roles)) {
|
||||
foreach (Database::PERMISSIONS as $type) {
|
||||
foreach ($permissions as $permission) {
|
||||
$permission = Permission::parse($permission);
|
||||
@@ -799,8 +799,8 @@ App::get('/v1/storage/buckets/:bucketId/files')
|
||||
->action(function (string $bucketId, array $queries, string $search, bool $includeTotal, Response $response, Database $dbForProject, string $mode) {
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAPIKey = User::isApp(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
@@ -900,8 +900,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId')
|
||||
->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, string $mode) {
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAPIKey = User::isApp(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
@@ -982,8 +982,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
|
||||
/* @type Document $bucket */
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAPIKey = User::isApp(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
@@ -1127,7 +1127,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
|
||||
$contentType = (\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg'];
|
||||
|
||||
//Do not update transformedAt if it's a console user
|
||||
if (!Auth::isPrivilegedUser(Authorization::getRoles())) {
|
||||
if (!User::isPrivileged(Authorization::getRoles())) {
|
||||
$transformedAt = $file->getAttribute('transformedAt', '');
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) {
|
||||
$file->setAttribute('transformedAt', DateTime::now());
|
||||
@@ -1178,8 +1178,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download')
|
||||
/* @type Document $bucket */
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAPIKey = User::isApp(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
@@ -1339,8 +1339,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view')
|
||||
/* @type Document $bucket */
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAPIKey = User::isApp(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
@@ -1512,8 +1512,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push')
|
||||
$disposition = $decoded['disposition'] ?? 'inline';
|
||||
$dbForProject = $isInternal ? $dbForPlatform : $dbForProject;
|
||||
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAPIKey = User::isApp(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
@@ -1669,8 +1669,8 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
|
||||
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAPIKey = User::isApp(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
@@ -1699,7 +1699,7 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
|
||||
|
||||
// Users can only manage their own roles, API keys and Admin users can manage any
|
||||
$roles = Authorization::getRoles();
|
||||
if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles) && !\is_null($permissions)) {
|
||||
if (!User::isApp($roles) && !User::isPrivileged($roles) && !\is_null($permissions)) {
|
||||
foreach (Database::PERMISSIONS as $type) {
|
||||
foreach ($permissions as $permission) {
|
||||
$permission = Permission::parse($permission);
|
||||
@@ -1783,8 +1783,8 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
|
||||
->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Event $queueForEvents, string $mode, Device $deviceForFiles, Delete $queueForDeletes) {
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAPIKey = User::isApp(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Auth\MFA\Type\TOTP;
|
||||
use Appwrite\Auth\Validator\Phone;
|
||||
use Appwrite\Detector\Detector;
|
||||
@@ -18,6 +17,7 @@ use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Template\Template;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Memberships;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Teams;
|
||||
@@ -28,6 +28,9 @@ use MaxMind\Db\Reader;
|
||||
use Utopia\Abuse\Abuse;
|
||||
use Utopia\App;
|
||||
use Utopia\Audit\Audit;
|
||||
use Utopia\Auth\Proofs\Password;
|
||||
use Utopia\Auth\Proofs\Token;
|
||||
use Utopia\Auth\Store;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
@@ -87,8 +90,8 @@ App::post('/v1/teams')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) {
|
||||
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAppUser = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
$isAppUser = User::isApp(Authorization::getRoles());
|
||||
|
||||
$teamId = $teamId == 'unique()' ? ID::unique() : $teamId;
|
||||
|
||||
@@ -176,6 +179,7 @@ App::get('/v1/teams')
|
||||
->inject('dbForProject')
|
||||
->action(function (array $queries, string $search, bool $includeTotal, Response $response, Database $dbForProject) {
|
||||
|
||||
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
@@ -474,10 +478,9 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
->param('phone', '', new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
|
||||
->param('roles', [], function (Document $project) {
|
||||
if ($project->getId() === 'console') {
|
||||
;
|
||||
$roles = array_keys(Config::getParam('roles', []));
|
||||
array_filter($roles, function ($role) {
|
||||
return !in_array($role, [Auth::USER_ROLE_APPS, Auth::USER_ROLE_GUESTS, Auth::USER_ROLE_USERS]);
|
||||
$roles = array_filter($roles, function ($role) {
|
||||
return !in_array($role, [User::ROLE_APPS, User::ROLE_GUESTS, User::ROLE_USERS]);
|
||||
});
|
||||
return new ArrayList(new WhiteList($roles), APP_LIMIT_ARRAY_PARAMS_SIZE);
|
||||
}
|
||||
@@ -496,9 +499,11 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
->inject('timelimit')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('plan')
|
||||
->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan) {
|
||||
$isAppUser = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
->inject('proofForPassword')
|
||||
->inject('proofForToken')
|
||||
->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, Password $proofForPassword, Token $proofForToken) {
|
||||
$isAppUser = User::isApp(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
|
||||
$url = htmlentities($url);
|
||||
if (empty($url)) {
|
||||
@@ -568,6 +573,8 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
}
|
||||
|
||||
try {
|
||||
$userId = ID::unique();
|
||||
$hash = $proofForPassword->hash($proofForPassword->generate());
|
||||
$emailCanonical = new Email($email);
|
||||
} catch (Throwable) {
|
||||
$emailCanonical = null;
|
||||
@@ -588,9 +595,9 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
'emailVerification' => false,
|
||||
'status' => true,
|
||||
// TODO: Set password empty?
|
||||
'password' => Auth::passwordHash(Auth::passwordGenerator(), Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS),
|
||||
'hash' => Auth::DEFAULT_ALGO,
|
||||
'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,
|
||||
'password' => $hash,
|
||||
'hash' => $proofForPassword->getHash()->getName(),
|
||||
'hashOptions' => $proofForPassword->getHash()->getOptions(),
|
||||
/**
|
||||
* Set the password update time to 0 for users created using
|
||||
* team invite and OAuth to allow password updates without an
|
||||
@@ -630,7 +637,7 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
Query::equal('teamInternalId', [$team->getSequence()]),
|
||||
]);
|
||||
|
||||
$secret = Auth::tokenGenerator();
|
||||
$secret = $proofForToken->generate();
|
||||
if ($membership->isEmpty()) {
|
||||
$membershipId = ID::unique();
|
||||
$membership = new Document([
|
||||
@@ -650,7 +657,7 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
'invited' => DateTime::now(),
|
||||
'joined' => ($isPrivilegedUser || $isAppUser) ? DateTime::now() : null,
|
||||
'confirm' => ($isPrivilegedUser || $isAppUser),
|
||||
'secret' => Auth::hash($secret),
|
||||
'secret' => $proofForToken->hash($secret),
|
||||
'search' => implode(' ', [$membershipId, $invitee->getId()])
|
||||
]);
|
||||
|
||||
@@ -661,9 +668,8 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
if ($isPrivilegedUser || $isAppUser) {
|
||||
Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1));
|
||||
}
|
||||
|
||||
} elseif ($membership->getAttribute('confirm') === false) {
|
||||
$membership->setAttribute('secret', Auth::hash($secret));
|
||||
$membership->setAttribute('secret', $proofForToken->hash($secret));
|
||||
$membership->setAttribute('invited', DateTime::now());
|
||||
|
||||
if ($isPrivilegedUser || $isAppUser) {
|
||||
@@ -766,7 +772,6 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
->setName($invitee->getAttribute('name', ''))
|
||||
->setVariables($emailVariables)
|
||||
->trigger();
|
||||
|
||||
} elseif (!empty($phone)) {
|
||||
if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
|
||||
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
|
||||
@@ -930,8 +935,8 @@ App::get('/v1/teams/:teamId/memberships')
|
||||
];
|
||||
|
||||
$roles = Authorization::getRoles();
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
|
||||
$isAppUser = Auth::isAppUser($roles);
|
||||
$isPrivilegedUser = User::isPrivileged($roles);
|
||||
$isAppUser = User::isApp($roles);
|
||||
|
||||
$membershipsPrivacy = array_map(function ($privacy) use ($isPrivilegedUser, $isAppUser) {
|
||||
return $privacy || $isPrivilegedUser || $isAppUser;
|
||||
@@ -1021,8 +1026,8 @@ App::get('/v1/teams/:teamId/memberships/:membershipId')
|
||||
];
|
||||
|
||||
$roles = Authorization::getRoles();
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
|
||||
$isAppUser = Auth::isAppUser($roles);
|
||||
$isPrivilegedUser = User::isPrivileged($roles);
|
||||
$isAppUser = User::isApp($roles);
|
||||
|
||||
$membershipsPrivacy = array_map(function ($privacy) use ($isPrivilegedUser, $isAppUser) {
|
||||
return $privacy || $isPrivilegedUser || $isAppUser;
|
||||
@@ -1087,8 +1092,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
|
||||
->param('roles', [], function (Document $project) {
|
||||
if ($project->getId() === 'console') {
|
||||
$roles = array_keys(Config::getParam('roles', []));
|
||||
array_filter($roles, function ($role) {
|
||||
return !in_array($role, [Auth::USER_ROLE_APPS, Auth::USER_ROLE_GUESTS, Auth::USER_ROLE_USERS]);
|
||||
$roles = array_filter($roles, function ($role) {
|
||||
return !in_array($role, [User::ROLE_APPS, User::ROLE_GUESTS, User::ROLE_USERS]);
|
||||
});
|
||||
return new ArrayList(new WhiteList($roles), APP_LIMIT_ARRAY_PARAMS_SIZE);
|
||||
}
|
||||
@@ -1117,8 +1122,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAppUser = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
$isAppUser = User::isApp(Authorization::getRoles());
|
||||
$isOwner = Authorization::isRole('team:' . $team->getId() . '/owner');
|
||||
|
||||
if ($project->getId() === 'console') {
|
||||
@@ -1203,7 +1208,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
|
||||
->inject('project')
|
||||
->inject('geodb')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Reader $geodb, Event $queueForEvents) {
|
||||
->inject('store')
|
||||
->inject('proofForToken')
|
||||
->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken) {
|
||||
$protocol = $request->getProtocol();
|
||||
|
||||
$membership = $dbForProject->getDocument('memberships', $membershipId);
|
||||
@@ -1222,7 +1229,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
|
||||
throw new Exception(Exception::TEAM_MEMBERSHIP_MISMATCH);
|
||||
}
|
||||
|
||||
if (Auth::hash($secret) !== $membership->getAttribute('secret')) {
|
||||
if (!$proofForToken->verify($secret, $membership->getAttribute('secret'))) {
|
||||
throw new Exception(Exception::TEAM_INVALID_SECRET);
|
||||
}
|
||||
|
||||
@@ -1256,9 +1263,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
|
||||
|
||||
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
||||
$record = $geodb->get($request->getIP());
|
||||
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
$authDuration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
$expire = DateTime::addSeconds(new \DateTime(), $authDuration);
|
||||
$secret = Auth::tokenGenerator();
|
||||
$secret = $proofForToken->generate();
|
||||
$session = new Document(array_merge([
|
||||
'$id' => ID::unique(),
|
||||
'$permissions' => [
|
||||
@@ -1268,9 +1275,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
|
||||
],
|
||||
'userId' => $user->getId(),
|
||||
'userInternalId' => $user->getSequence(),
|
||||
'provider' => Auth::SESSION_PROVIDER_EMAIL,
|
||||
'provider' => SESSION_PROVIDER_EMAIL,
|
||||
'providerUid' => $user->getAttribute('email'),
|
||||
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
|
||||
'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
'ip' => $request->getIP(),
|
||||
'factors' => ['email'],
|
||||
@@ -1282,14 +1289,19 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
|
||||
|
||||
Authorization::setRole(Role::user($userId)->toString());
|
||||
|
||||
$encoded = $store
|
||||
->setProperty('id', $user->getId())
|
||||
->setProperty('secret', $secret)
|
||||
->encode();
|
||||
|
||||
if (!Config::getParam('domainVerification')) {
|
||||
$response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]));
|
||||
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
|
||||
}
|
||||
|
||||
$response
|
||||
->addCookie(
|
||||
name: Auth::$cookieName . '_legacy',
|
||||
value: Auth::encodeSession($user->getId(), $secret),
|
||||
name: $store->getKey() . '_legacy',
|
||||
value: $encoded,
|
||||
expire: (new \DateTime($expire))->getTimestamp(),
|
||||
path: '/',
|
||||
domain: Config::getParam('cookieDomain'),
|
||||
@@ -1297,8 +1309,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
|
||||
httponly: true
|
||||
)
|
||||
->addCookie(
|
||||
name: Auth::$cookieName,
|
||||
value: Auth::encodeSession($user->getId(), $secret),
|
||||
name: $store->getKey(),
|
||||
value: $encoded,
|
||||
expire: (new \DateTime($expire))->getTimestamp(),
|
||||
path: '/',
|
||||
domain: Config::getParam('cookieDomain'),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?php
|
||||
|
||||
use Ahc\Jwt\JWT;
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Auth\MFA\Type;
|
||||
use Appwrite\Auth\MFA\Type\TOTP;
|
||||
use Appwrite\Auth\Validator\Password;
|
||||
@@ -32,6 +31,18 @@ use Appwrite\Utopia\Response;
|
||||
use MaxMind\Db\Reader;
|
||||
use Utopia\App;
|
||||
use Utopia\Audit\Audit;
|
||||
use Utopia\Auth\Hash;
|
||||
use Utopia\Auth\Hashes\Argon2;
|
||||
use Utopia\Auth\Hashes\Bcrypt;
|
||||
use Utopia\Auth\Hashes\MD5;
|
||||
use Utopia\Auth\Hashes\PHPass;
|
||||
use Utopia\Auth\Hashes\Plaintext;
|
||||
use Utopia\Auth\Hashes\Scrypt;
|
||||
use Utopia\Auth\Hashes\ScryptModified;
|
||||
use Utopia\Auth\Hashes\Sha;
|
||||
use Utopia\Auth\Proofs\Password as ProofsPassword;
|
||||
use Utopia\Auth\Proofs\Token;
|
||||
use Utopia\Auth\Store;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
@@ -62,10 +73,9 @@ use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
/** TODO: Remove function when we move to using utopia/platform */
|
||||
function createUser(string $hash, mixed $hashOptions, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Document $project, Database $dbForProject, Hooks $hooks): Document
|
||||
function createUser(Hash $hash, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Document $project, Database $dbForProject, Hooks $hooks): Document
|
||||
{
|
||||
$plaintextPassword = $password;
|
||||
$hashOptionsObject = (\is_string($hashOptions)) ? \json_decode($hashOptions, true) : $hashOptions; // Cast to JSON array
|
||||
$passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
|
||||
|
||||
if (!empty($email)) {
|
||||
@@ -104,8 +114,19 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
|
||||
} catch (Throwable) {
|
||||
$emailCanonical = null;
|
||||
}
|
||||
$hashedPassword = null;
|
||||
|
||||
$isHashed = !$hash instanceof Plaintext;
|
||||
if (!empty($password)) {
|
||||
if (!$isHashed) { // Password was never hashed, hash it with the default hash
|
||||
$defaultHash = new ProofsPassword();
|
||||
$hashedPassword = $defaultHash->hash($password);
|
||||
$hash = $defaultHash->getHash();
|
||||
} else {
|
||||
$hashedPassword = $password;
|
||||
}
|
||||
}
|
||||
|
||||
$password = (!empty($password)) ? ($hash === 'plaintext' ? Auth::passwordHash($password, $hash, $hashOptionsObject) : $password) : null;
|
||||
$user = new Document([
|
||||
'$id' => $userId,
|
||||
'$permissions' => [
|
||||
@@ -119,11 +140,11 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
|
||||
'phoneVerification' => false,
|
||||
'status' => true,
|
||||
'labels' => [],
|
||||
'password' => $password,
|
||||
'passwordHistory' => is_null($password) || $passwordHistory === 0 ? [] : [$password],
|
||||
'passwordUpdate' => (!empty($password)) ? DateTime::now() : null,
|
||||
'hash' => $hash === 'plaintext' ? Auth::DEFAULT_ALGO : $hash,
|
||||
'hashOptions' => $hash === 'plaintext' ? Auth::DEFAULT_ALGO_OPTIONS : $hashOptionsObject + ['type' => $hash],
|
||||
'password' => $hashedPassword,
|
||||
'passwordHistory' => is_null($hashedPassword) || $passwordHistory === 0 ? [] : [$hashedPassword],
|
||||
'passwordUpdate' => (!empty($hashedPassword)) ? DateTime::now() : null,
|
||||
'hash' => $hash->getName(),
|
||||
'hashOptions' => $hash->getOptions(),
|
||||
'registration' => DateTime::now(),
|
||||
'reset' => false,
|
||||
'name' => $name,
|
||||
@@ -139,7 +160,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
|
||||
'emailIsFree' => $emailCanonical?->isFree(),
|
||||
]);
|
||||
|
||||
if ($hash === 'plaintext') {
|
||||
if (!$isHashed) {
|
||||
$hooks->trigger('passwordValidator', [$dbForProject, $project, $plaintextPassword, &$user, true]);
|
||||
}
|
||||
|
||||
@@ -230,7 +251,9 @@ App::post('/v1/users')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$user = createUser('plaintext', '{}', $userId, $email, $password, $phone, $name, $project, $dbForProject, $hooks);
|
||||
$plaintext = new Plaintext();
|
||||
|
||||
$user = createUser($plaintext, $userId, $email, $password, $phone, $name, $project, $dbForProject, $hooks);
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($user, Response::MODEL_USER);
|
||||
@@ -264,7 +287,10 @@ App::post('/v1/users/bcrypt')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$user = createUser('bcrypt', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
$bcrypt = new Bcrypt();
|
||||
$bcrypt->setCost(8); // Default cost
|
||||
|
||||
$user = createUser($bcrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -299,7 +325,9 @@ App::post('/v1/users/md5')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$user = createUser('md5', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
$md5 = new MD5();
|
||||
|
||||
$user = createUser($md5, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -334,7 +362,9 @@ App::post('/v1/users/argon2')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$user = createUser('argon2', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
$argon2 = new Argon2();
|
||||
|
||||
$user = createUser($argon2, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -370,13 +400,12 @@ App::post('/v1/users/sha')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordVersion, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$options = '{}';
|
||||
|
||||
$sha = new Sha();
|
||||
if (!empty($passwordVersion)) {
|
||||
$options = '{"version":"' . $passwordVersion . '"}';
|
||||
$sha->setVersion($passwordVersion);
|
||||
}
|
||||
|
||||
$user = createUser('sha', $options, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
$user = createUser($sha, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -411,7 +440,9 @@ App::post('/v1/users/phpass')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$user = createUser('phpass', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
$phpass = new PHPass();
|
||||
|
||||
$user = createUser($phpass, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -451,15 +482,15 @@ App::post('/v1/users/scrypt')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$options = [
|
||||
'salt' => $passwordSalt,
|
||||
'costCpu' => $passwordCpu,
|
||||
'costMemory' => $passwordMemory,
|
||||
'costParallel' => $passwordParallel,
|
||||
'length' => $passwordLength
|
||||
];
|
||||
$scrypt = new Scrypt();
|
||||
$scrypt
|
||||
->setSalt($passwordSalt)
|
||||
->setCpuCost($passwordCpu)
|
||||
->setMemoryCost($passwordMemory)
|
||||
->setParallelCost($passwordParallel)
|
||||
->setLength($passwordLength);
|
||||
|
||||
$user = createUser('scrypt', \json_encode($options), $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
$user = createUser($scrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -497,7 +528,13 @@ App::post('/v1/users/scrypt-modified')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$user = createUser('scryptMod', '{"signerKey":"' . $passwordSignerKey . '","saltSeparator":"' . $passwordSaltSeparator . '","salt":"' . $passwordSalt . '"}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
$scryptModified = new ScryptModified();
|
||||
$scryptModified
|
||||
->setSalt($passwordSalt)
|
||||
->setSaltSeparator($passwordSaltSeparator)
|
||||
->setSignerKey($passwordSignerKey);
|
||||
|
||||
$user = createUser($scryptModified, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -809,16 +846,12 @@ App::get('/v1/users/:userId/sessions')
|
||||
if ($user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$sessions = $user->getAttribute('sessions', []);
|
||||
|
||||
foreach ($sessions as $key => $session) {
|
||||
/** @var Document $session */
|
||||
|
||||
$countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'));
|
||||
$session->setAttribute('countryName', $countryName);
|
||||
$session->setAttribute('current', false);
|
||||
|
||||
$sessions[$key] = $session;
|
||||
}
|
||||
|
||||
@@ -858,28 +891,22 @@ App::get('/v1/users/:userId/memberships')
|
||||
if ($user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
// Set internal queries
|
||||
$queries[] = Query::equal('userInternalId', [$user->getSequence()]);
|
||||
|
||||
$memberships = array_map(function ($membership) use ($dbForProject, $user) {
|
||||
$team = $dbForProject->getDocument('teams', $membership->getAttribute('teamId'));
|
||||
|
||||
$membership
|
||||
->setAttribute('teamName', $team->getAttribute('name'))
|
||||
->setAttribute('userName', $user->getAttribute('name'))
|
||||
->setAttribute('userEmail', $user->getAttribute('email'));
|
||||
|
||||
return $membership;
|
||||
}, $dbForProject->find('memberships', $queries));
|
||||
|
||||
@@ -920,35 +947,26 @@ App::get('/v1/users/:userId/logs')
|
||||
if ($user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
// Temp fix for logs
|
||||
$queries[] = Query::or([
|
||||
Query::greaterThan('$createdAt', DateTime::format(new \DateTime('2025-02-26T01:30+00:00'))),
|
||||
Query::lessThan('$createdAt', DateTime::format(new \DateTime('2025-02-13T00:00+00:00'))),
|
||||
]);
|
||||
|
||||
$audit = new Audit($dbForProject);
|
||||
|
||||
$logs = $audit->getLogsByUser($user->getSequence(), $queries);
|
||||
|
||||
$output = [];
|
||||
|
||||
foreach ($logs as $i => &$log) {
|
||||
$log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN';
|
||||
|
||||
$detector = new Detector($log['userAgent']);
|
||||
$detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then)
|
||||
|
||||
$os = $detector->getOS();
|
||||
$client = $detector->getClient();
|
||||
$device = $detector->getDevice();
|
||||
|
||||
$output[$i] = new Document([
|
||||
'event' => $log['event'],
|
||||
'userId' => ID::custom($log['data']['userId']),
|
||||
@@ -969,9 +987,7 @@ App::get('/v1/users/:userId/logs')
|
||||
'deviceBrand' => $device['deviceBrand'],
|
||||
'deviceModel' => $device['deviceModel']
|
||||
]);
|
||||
|
||||
$record = $geodb->get($log['ip']);
|
||||
|
||||
if ($record) {
|
||||
$output[$i]['countryCode'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), false) ? \strtolower($record['country']['iso_code']) : '--';
|
||||
$output[$i]['countryName'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), $locale->getText('locale.country.unknown'));
|
||||
@@ -1015,15 +1031,12 @@ App::get('/v1/users/:userId/targets')
|
||||
if ($user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
$queries[] = Query::equal('userId', [$userId]);
|
||||
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
@@ -1031,20 +1044,16 @@ App::get('/v1/users/:userId/targets')
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
$cursor = reset($cursor);
|
||||
|
||||
if ($cursor) {
|
||||
$validator = new Cursor();
|
||||
if (!$validator->isValid($cursor)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
$targetId = $cursor->getValue();
|
||||
$cursorDocument = $dbForProject->getDocument('targets', $targetId);
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Target '{$targetId}' for the 'cursor' value not found.");
|
||||
}
|
||||
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
try {
|
||||
@@ -1092,7 +1101,6 @@ App::get('/v1/users/identities')
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
@@ -1102,19 +1110,15 @@ App::get('/v1/users/identities')
|
||||
$cursor = reset($cursor);
|
||||
if ($cursor) {
|
||||
/** @var Query $cursor */
|
||||
|
||||
$validator = new Cursor();
|
||||
if (!$validator->isValid($cursor)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
$identityId = $cursor->getValue();
|
||||
$cursorDocument = $dbForProject->getDocument('identities', $identityId);
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "User '{$identityId}' for the 'cursor' value not found.");
|
||||
}
|
||||
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
@@ -1354,12 +1358,17 @@ App::patch('/v1/users/:userId/password')
|
||||
|
||||
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]);
|
||||
|
||||
$newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
|
||||
// Create Argon2 hasher with default settings
|
||||
$hasher = new Argon2();
|
||||
|
||||
$newPassword = $hasher->hash($password);
|
||||
|
||||
$hash = ProofsPassword::createHash($user->getAttribute('hash'), $user->getAttribute('hashOptions'));
|
||||
$historyLimit = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
|
||||
$history = $user->getAttribute('passwordHistory', []);
|
||||
|
||||
if ($historyLimit > 0) {
|
||||
$validator = new PasswordHistory($history, $user->getAttribute('hash'), $user->getAttribute('hashOptions'));
|
||||
$validator = new PasswordHistory($history, $hash);
|
||||
if (!$validator->isValid($password)) {
|
||||
throw new Exception(Exception::USER_PASSWORD_RECENTLY_USED);
|
||||
}
|
||||
@@ -1372,8 +1381,8 @@ App::patch('/v1/users/:userId/password')
|
||||
->setAttribute('password', $newPassword)
|
||||
->setAttribute('passwordHistory', $history)
|
||||
->setAttribute('passwordUpdate', DateTime::now())
|
||||
->setAttribute('hash', Auth::DEFAULT_ALGO)
|
||||
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS);
|
||||
->setAttribute('hash', $hasher->getName())
|
||||
->setAttribute('hashOptions', $hasher->getOptions());
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
|
||||
@@ -2197,17 +2206,19 @@ App::post('/v1/users/:userId/sessions')
|
||||
->inject('locale')
|
||||
->inject('geodb')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $userId, Request $request, Response $response, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents) {
|
||||
->inject('store')
|
||||
->inject('proofForToken')
|
||||
->action(function (string $userId, Request $request, Response $response, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken) {
|
||||
$user = $dbForProject->getDocument('users', $userId);
|
||||
if ($user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$secret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_SESSION);
|
||||
$secret = $proofForToken->generate();
|
||||
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
||||
$record = $geodb->get($request->getIP());
|
||||
|
||||
$duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
$duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
|
||||
|
||||
$session = new Document(array_merge(
|
||||
@@ -2215,8 +2226,8 @@ App::post('/v1/users/:userId/sessions')
|
||||
'$id' => ID::unique(),
|
||||
'userId' => $user->getId(),
|
||||
'userInternalId' => $user->getSequence(),
|
||||
'provider' => Auth::SESSION_PROVIDER_SERVER,
|
||||
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
|
||||
'provider' => SESSION_PROVIDER_SERVER,
|
||||
'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
'factors' => ['server'],
|
||||
'ip' => $request->getIP(),
|
||||
@@ -2240,8 +2251,13 @@ App::post('/v1/users/:userId/sessions')
|
||||
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$encoded = $store
|
||||
->setProperty('id', $user->getId())
|
||||
->setProperty('secret', $secret)
|
||||
->encode();
|
||||
|
||||
$session
|
||||
->setAttribute('secret', Auth::encodeSession($user->getId(), $secret))
|
||||
->setAttribute('secret', $encoded)
|
||||
->setAttribute('countryName', $countryName);
|
||||
|
||||
$queueForEvents
|
||||
@@ -2276,7 +2292,7 @@ App::post('/v1/users/:userId/tokens')
|
||||
))
|
||||
->param('userId', '', new UID(), 'User ID.')
|
||||
->param('length', 6, new Range(4, 128), 'Token length in characters. The default length is 6 characters', true)
|
||||
->param('expire', Auth::TOKEN_EXPIRATION_GENERIC, new Range(60, Auth::TOKEN_EXPIRATION_LOGIN_LONG), 'Token expiration period in seconds. The default expiration is 15 minutes.', true)
|
||||
->param('expire', TOKEN_EXPIRATION_GENERIC, new Range(60, TOKEN_EXPIRATION_LOGIN_LONG), 'Token expiration period in seconds. The default expiration is 15 minutes.', true)
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
@@ -2288,15 +2304,17 @@ App::post('/v1/users/:userId/tokens')
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$secret = Auth::tokenGenerator($length);
|
||||
$proofForToken = new Token($length);
|
||||
$proofForToken->setHash(new Sha());
|
||||
$secret = $proofForToken->generate();
|
||||
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $expire));
|
||||
|
||||
$token = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'userId' => $user->getId(),
|
||||
'userInternalId' => $user->getSequence(),
|
||||
'type' => Auth::TOKEN_TYPE_GENERIC,
|
||||
'secret' => Auth::hash($secret),
|
||||
'type' => TOKEN_TYPE_GENERIC,
|
||||
'secret' => $proofForToken->hash($secret),
|
||||
'expire' => $expire,
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
'ip' => $request->getIP()
|
||||
|
||||
@@ -4,7 +4,6 @@ require_once __DIR__ . '/../init.php';
|
||||
|
||||
use Ahc\Jwt\JWT;
|
||||
use Ahc\Jwt\JWTException;
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Auth\Key;
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Event;
|
||||
@@ -17,6 +16,7 @@ use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Transformation\Adapter\Preview;
|
||||
use Appwrite\Transformation\Transformation;
|
||||
use Appwrite\Utopia\Database\Documents\User as DBUser;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Request\Filters\V16 as RequestV16;
|
||||
use Appwrite\Utopia\Request\Filters\V17 as RequestV17;
|
||||
@@ -223,7 +223,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
|
||||
*/
|
||||
$requirePreview = \is_null($apiKey) || !$apiKey->isPreviewAuthDisabled();
|
||||
if ($isPreview && $requirePreview) {
|
||||
$cookie = $request->getCookie(Auth::$cookieNamePreview, '');
|
||||
$cookie = $request->getCookie(COOKIE_NAME_PREVIEW, '');
|
||||
$authorized = false;
|
||||
|
||||
// Security checks to mark authorized true
|
||||
@@ -1260,7 +1260,7 @@ App::error()
|
||||
* If not a publishable error, track usage stats. Publishable errors are >= 500 or those explicitly marked as publish=true in errors.php
|
||||
*/
|
||||
if (!$publish && $project->getId() !== 'console') {
|
||||
if (!Auth::isPrivilegedUser(Authorization::getRoles())) {
|
||||
if (!DBUser::isPrivileged(Authorization::getRoles())) {
|
||||
$fileSize = 0;
|
||||
$file = $request->getFiles('file');
|
||||
if (!empty($file)) {
|
||||
@@ -1617,7 +1617,7 @@ App::get('/_appwrite/authorize')
|
||||
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
|
||||
|
||||
$response
|
||||
->addCookie(Auth::$cookieNamePreview, $jwt, (new \DateTime($expire))->getTimestamp(), '/', $host, ('https' === $protocol), true, null)
|
||||
->addCookie(COOKIE_NAME_PREVIEW, $jwt, (new \DateTime($expire))->getTimestamp(), '/', $host, ('https' === $protocol), true, null)
|
||||
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||
->addHeader('Pragma', 'no-cache')
|
||||
->redirect($protocol . '://' . $host . $path);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Auth\Key;
|
||||
use Appwrite\Auth\MFA\Type\TOTP;
|
||||
use Appwrite\Event\Audit;
|
||||
@@ -16,13 +15,13 @@ use Appwrite\Event\Webhook;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Extend\Exception as AppwriteException;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Abuse\Abuse;
|
||||
use Utopia\App;
|
||||
use Utopia\Cache\Adapter\Filesystem;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
@@ -34,7 +33,7 @@ use Utopia\System\System;
|
||||
use Utopia\Telemetry\Adapter as Telemetry;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
$parseLabel = function (string $label, array $responsePayload, array $requestParams, Document $user) {
|
||||
$parseLabel = function (string $label, array $responsePayload, array $requestParams, User $user) {
|
||||
preg_match_all('/{(.*?)}/', $label, $matches);
|
||||
foreach ($matches[1] ?? [] as $pos => $match) {
|
||||
$find = $matches[0][$pos];
|
||||
@@ -232,44 +231,97 @@ App::init()
|
||||
->inject('mode')
|
||||
->inject('team')
|
||||
->inject('apiKey')
|
||||
->action(function (App $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey) {
|
||||
->action(function (App $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey) {
|
||||
$route = $utopia->getRoute();
|
||||
if (System::getEnv('_APP_EDITION', 'self-hosted') === 'self-hosted' && str_starts_with($route->getPath(), '/v1/backups')) {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Database Backups are available on Appwrite Cloud');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle user authentication and session validation.
|
||||
*
|
||||
* This function follows a series of steps to determine the appropriate user session
|
||||
* based on cookies, headers, and JWT tokens.
|
||||
*
|
||||
* Process:
|
||||
*
|
||||
* Project & Role Validation:
|
||||
* 1. Check if the project is empty. If so, throw an exception.
|
||||
* 2. Get the roles configuration.
|
||||
* 3. Determine the role for the user based on the user document.
|
||||
* 4. Get the scopes for the role.
|
||||
*
|
||||
* API Key Authentication:
|
||||
* 5. If there is an API key:
|
||||
* - Verify no user session exists simultaneously
|
||||
* - Check if key is expired
|
||||
* - Set role and scopes from API key
|
||||
* - Handle special app role case
|
||||
* - For standard keys, update last accessed time
|
||||
*
|
||||
* User Activity:
|
||||
* 6. If the project is not the console and user is not admin:
|
||||
* - Update user's last activity timestamp
|
||||
*
|
||||
* Access Control:
|
||||
* 7. Get the method from the route
|
||||
* 8. Validate namespace permissions
|
||||
* 9. Validate scope permissions
|
||||
* 10. Check if user is blocked
|
||||
*
|
||||
* Security Checks:
|
||||
* 11. Verify password status (check if reset required)
|
||||
* 12. Validate MFA requirements:
|
||||
* - Check if MFA is enabled
|
||||
* - Verify email status
|
||||
* - Verify phone status
|
||||
* - Verify authenticator status
|
||||
* 13. Handle Multi-Factor Authentication:
|
||||
* - Check remaining required factors
|
||||
* - Validate factor completion
|
||||
* - Throw exception if factors incomplete
|
||||
*/
|
||||
|
||||
// Step 1: Check if project is empty
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
// Step 2: Get roles configuration
|
||||
$roles = Config::getParam('roles', []);
|
||||
|
||||
// Step 3: Determine role for user
|
||||
// TODO get scopes from the identity instead of the user roles config. The identity will containn the scopes the user authorized for the access token.
|
||||
|
||||
$role = $user->isEmpty()
|
||||
? Role::guests()->toString()
|
||||
: Role::users()->toString();
|
||||
|
||||
// Step 4: Get scopes for the role
|
||||
$scopes = $roles[$role]['scopes'];
|
||||
|
||||
// API Key authentication
|
||||
// Step 5: API Key Authentication
|
||||
if (!empty($apiKey)) {
|
||||
// Verify no user session exists simultaneously
|
||||
if (!$user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
|
||||
}
|
||||
// Check if key is expired
|
||||
if ($apiKey->isExpired()) {
|
||||
throw new Exception(Exception::PROJECT_KEY_EXPIRED);
|
||||
}
|
||||
|
||||
// Set role and scopes from API key
|
||||
$role = $apiKey->getRole();
|
||||
$scopes = $apiKey->getScopes();
|
||||
|
||||
|
||||
if ($apiKey->getRole() === Auth::USER_ROLE_APPS) {
|
||||
// Handle special app role case
|
||||
if ($apiKey->getRole() === User::ROLE_APPS) {
|
||||
// Disable authorization checks for API keys
|
||||
Authorization::setDefaultStatus(false);
|
||||
|
||||
$user = new Document([
|
||||
$user = new User([
|
||||
'$id' => '',
|
||||
'status' => true,
|
||||
'type' => Auth::ACTIVITY_TYPE_APP,
|
||||
'type' => ACTIVITY_TYPE_APP,
|
||||
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
|
||||
'password' => '',
|
||||
'name' => $apiKey->getName(),
|
||||
@@ -278,6 +330,7 @@ App::init()
|
||||
$queueForAudits->setUser($user);
|
||||
}
|
||||
|
||||
// For standard keys, update last accessed time
|
||||
if ($apiKey->getType() === API_KEY_STANDARD) {
|
||||
$dbKey = $project->find(
|
||||
key: 'secret',
|
||||
@@ -343,11 +396,11 @@ App::init()
|
||||
$scopes = \array_unique($scopes);
|
||||
|
||||
Authorization::setRole($role);
|
||||
foreach (Auth::getRoles($user) as $authRole) {
|
||||
foreach ($user->getRoles() as $authRole) {
|
||||
Authorization::setRole($authRole);
|
||||
}
|
||||
|
||||
// Update project last activity
|
||||
// Step 6: Update project and user last activity
|
||||
if (!$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$accessedAt = $project->getAttribute('accessedAt', 0);
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
|
||||
@@ -356,7 +409,6 @@ App::init()
|
||||
}
|
||||
}
|
||||
|
||||
// Update user last activity
|
||||
if (!empty($user->getId())) {
|
||||
$accessedAt = $user->getAttribute('accessedAt', 0);
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCESS)) > $accessedAt) {
|
||||
@@ -370,6 +422,7 @@ App::init()
|
||||
}
|
||||
}
|
||||
|
||||
// Steps 7-9: Access Control - Method, Namespace and Scope Validation
|
||||
/**
|
||||
* @var ?Method $method
|
||||
*/
|
||||
@@ -387,27 +440,29 @@ App::init()
|
||||
if (
|
||||
array_key_exists($namespace, $project->getAttribute('services', []))
|
||||
&& !$project->getAttribute('services', [])[$namespace]
|
||||
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
|
||||
&& !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles()))
|
||||
) {
|
||||
throw new Exception(Exception::GENERAL_SERVICE_DISABLED);
|
||||
}
|
||||
}
|
||||
|
||||
// Do now allow access if scope is not allowed
|
||||
// Step 9: Validate scope permissions
|
||||
$allowed = (array)$route->getLabel('scope', 'none');
|
||||
if (empty(\array_intersect($allowed, $scopes))) {
|
||||
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, $user->getAttribute('email', 'User') . ' (role: ' . \strtolower($roles[$role]['label']) . ') missing scopes (' . \json_encode($allowed) . ')');
|
||||
}
|
||||
|
||||
// Do not allow access to blocked accounts
|
||||
// Step 10: Check if user is blocked
|
||||
if (false === $user->getAttribute('status')) { // Account is blocked
|
||||
throw new Exception(Exception::USER_BLOCKED);
|
||||
}
|
||||
|
||||
// Step 11: Verify password status
|
||||
if ($user->getAttribute('reset')) {
|
||||
throw new Exception(Exception::USER_PASSWORD_RESET_REQUIRED);
|
||||
}
|
||||
|
||||
// Step 12: Validate MFA requirements
|
||||
$mfaEnabled = $user->getAttribute('mfa', false);
|
||||
$hasVerifiedEmail = $user->getAttribute('emailVerification', false);
|
||||
$hasVerifiedPhone = $user->getAttribute('phoneVerification', false);
|
||||
@@ -415,6 +470,7 @@ App::init()
|
||||
$hasMoreFactors = $hasVerifiedEmail || $hasVerifiedPhone || $hasVerifiedAuthenticator;
|
||||
$minimumFactors = ($mfaEnabled && $hasMoreFactors) ? 2 : 1;
|
||||
|
||||
// Step 13: Handle Multi-Factor Authentication
|
||||
if (!in_array('mfa', $route->getGroups())) {
|
||||
if ($session && \count($session->getAttribute('factors', [])) < $minimumFactors) {
|
||||
throw new Exception(Exception::USER_MORE_FACTORS_REQUIRED);
|
||||
@@ -454,7 +510,7 @@ App::init()
|
||||
if (
|
||||
array_key_exists('rest', $project->getAttribute('apis', []))
|
||||
&& !$project->getAttribute('apis', [])['rest']
|
||||
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
|
||||
&& !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles()))
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
|
||||
}
|
||||
@@ -485,8 +541,8 @@ App::init()
|
||||
$closestLimit = null;
|
||||
|
||||
$roles = Authorization::getRoles();
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
|
||||
$isAppUser = Auth::isAppUser($roles);
|
||||
$isPrivilegedUser = User::isPrivileged($roles);
|
||||
$isAppUser = User::isApp($roles);
|
||||
|
||||
foreach ($timeLimitArray as $timeLimit) {
|
||||
foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys
|
||||
@@ -541,7 +597,7 @@ App::init()
|
||||
if (!$user->isEmpty()) {
|
||||
$userClone = clone $user;
|
||||
// $user doesn't support `type` and can cause unintended effects.
|
||||
$userClone->setAttribute('type', Auth::ACTIVITY_TYPE_USER);
|
||||
$userClone->setAttribute('type', ACTIVITY_TYPE_USER);
|
||||
$queueForAudits->setUser($userClone);
|
||||
}
|
||||
|
||||
@@ -584,7 +640,7 @@ App::init()
|
||||
if ($useCache) {
|
||||
$route = $utopia->match($request);
|
||||
$isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview';
|
||||
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && !Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && !User::isPrivileged(Authorization::getRoles());
|
||||
|
||||
$key = $request->cacheIdentifier();
|
||||
$cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key));
|
||||
@@ -607,7 +663,7 @@ App::init()
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isToken = !$resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence();
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAppUser && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
@@ -641,7 +697,7 @@ App::init()
|
||||
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
|
||||
}
|
||||
//Do not update transformedAt if it's a console user
|
||||
if (!Auth::isPrivilegedUser(Authorization::getRoles())) {
|
||||
if (!User::isPrivileged(Authorization::getRoles())) {
|
||||
$transformedAt = $file->getAttribute('transformedAt', '');
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) {
|
||||
$file->setAttribute('transformedAt', DateTime::now());
|
||||
@@ -741,7 +797,7 @@ App::shutdown()
|
||||
->inject('queueForWebhooks')
|
||||
->inject('queueForRealtime')
|
||||
->inject('dbForProject')
|
||||
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject) use ($parseLabel) {
|
||||
->action(function (App $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject) use ($parseLabel) {
|
||||
|
||||
$responsePayload = $response->getPayload();
|
||||
|
||||
@@ -789,7 +845,7 @@ App::shutdown()
|
||||
if (!$user->isEmpty()) {
|
||||
$userClone = clone $user;
|
||||
// $user doesn't support `type` and can cause unintended effects.
|
||||
$userClone->setAttribute('type', Auth::ACTIVITY_TYPE_USER);
|
||||
$userClone->setAttribute('type', ACTIVITY_TYPE_USER);
|
||||
$queueForAudits->setUser($userClone);
|
||||
} elseif ($queueForAudits->getUser() === null || $queueForAudits->getUser()->isEmpty()) {
|
||||
/**
|
||||
@@ -803,7 +859,7 @@ App::shutdown()
|
||||
$user = new Document([
|
||||
'$id' => '',
|
||||
'status' => true,
|
||||
'type' => Auth::ACTIVITY_TYPE_GUEST,
|
||||
'type' => ACTIVITY_TYPE_GUEST,
|
||||
'email' => 'guest.' . $project->getId() . '@service.' . $request->getHostname(),
|
||||
'password' => '',
|
||||
'name' => 'Guest',
|
||||
@@ -893,7 +949,7 @@ App::shutdown()
|
||||
}
|
||||
|
||||
if ($project->getId() !== 'console') {
|
||||
if (!Auth::isPrivilegedUser(Authorization::getRoles())) {
|
||||
if (!User::isPrivileged(Authorization::getRoles())) {
|
||||
$fileSize = 0;
|
||||
$file = $request->getFiles('file');
|
||||
if (!empty($file)) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Request;
|
||||
use MaxMind\Db\Reader;
|
||||
use Utopia\App;
|
||||
@@ -20,7 +20,7 @@ App::init()
|
||||
$lastUpdate = $session->getAttribute('mfaUpdatedAt');
|
||||
if (!empty($lastUpdate)) {
|
||||
$now = DateTime::now();
|
||||
$maxAllowedDate = DateTime::addSeconds(new \DateTime($lastUpdate), Auth::MFA_RECENT_DURATION); // Maximum date until session is considered safe before asking for another challenge
|
||||
$maxAllowedDate = DateTime::addSeconds(new \DateTime($lastUpdate), MFA_RECENT_DURATION); // Maximum date until session is considered safe before asking for another challenge
|
||||
|
||||
$isSessionFresh = DateTime::formatTz($maxAllowedDate) >= DateTime::formatTz($now);
|
||||
}
|
||||
@@ -49,8 +49,8 @@ App::init()
|
||||
|
||||
$route = $utopia->match($request);
|
||||
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAppUser = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
$isAppUser = User::isApp(Authorization::getRoles());
|
||||
|
||||
if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs
|
||||
return;
|
||||
|
||||
@@ -93,6 +93,62 @@ const APP_VCS_GITHUB_EMAIL = 'team@appwrite.io';
|
||||
const APP_VCS_GITHUB_URL = 'https://github.com/TeamAppwrite';
|
||||
const APP_BRANDED_EMAIL_BASE_TEMPLATE = 'email-base-styled';
|
||||
|
||||
/**
|
||||
* Token Expiration times.
|
||||
*/
|
||||
const TOKEN_EXPIRATION_LOGIN_LONG = 31536000; /* 1 year */
|
||||
const TOKEN_EXPIRATION_LOGIN_SHORT = 3600; /* 1 hour */
|
||||
const TOKEN_EXPIRATION_RECOVERY = 3600; /* 1 hour */
|
||||
const TOKEN_EXPIRATION_CONFIRM = 3600 * 1; /* 1 hour */
|
||||
const TOKEN_EXPIRATION_OTP = 60 * 15; /* 15 minutes */
|
||||
const TOKEN_EXPIRATION_GENERIC = 60 * 15; /* 15 minutes */
|
||||
|
||||
/**
|
||||
* Token Lengths.
|
||||
*/
|
||||
const TOKEN_LENGTH_MAGIC_URL = 64;
|
||||
const TOKEN_LENGTH_VERIFICATION = 256;
|
||||
const TOKEN_LENGTH_RECOVERY = 256;
|
||||
const TOKEN_LENGTH_OAUTH2 = 64;
|
||||
const TOKEN_LENGTH_SESSION = 256;
|
||||
|
||||
/**
|
||||
* Token Types.
|
||||
*/
|
||||
const TOKEN_TYPE_LOGIN = 1; // Deprecated
|
||||
const TOKEN_TYPE_VERIFICATION = 2;
|
||||
const TOKEN_TYPE_RECOVERY = 3;
|
||||
const TOKEN_TYPE_INVITE = 4;
|
||||
const TOKEN_TYPE_MAGIC_URL = 5;
|
||||
const TOKEN_TYPE_PHONE = 6;
|
||||
const TOKEN_TYPE_OAUTH2 = 7;
|
||||
const TOKEN_TYPE_GENERIC = 8;
|
||||
const TOKEN_TYPE_EMAIL = 9; // OTP
|
||||
|
||||
/**
|
||||
* Session Providers.
|
||||
*/
|
||||
const SESSION_PROVIDER_EMAIL = 'email';
|
||||
const SESSION_PROVIDER_ANONYMOUS = 'anonymous';
|
||||
const SESSION_PROVIDER_MAGIC_URL = 'magic-url';
|
||||
const SESSION_PROVIDER_PHONE = 'phone';
|
||||
const SESSION_PROVIDER_OAUTH2 = 'oauth2';
|
||||
const SESSION_PROVIDER_TOKEN = 'token';
|
||||
const SESSION_PROVIDER_SERVER = 'server';
|
||||
|
||||
/**
|
||||
* Activity associated with user or the app.
|
||||
*/
|
||||
const ACTIVITY_TYPE_APP = 'app';
|
||||
const ACTIVITY_TYPE_USER = 'user';
|
||||
const ACTIVITY_TYPE_GUEST = 'guest';
|
||||
|
||||
/**
|
||||
* MFA
|
||||
*/
|
||||
const MFA_RECENT_DURATION = 1800; // 30 mins
|
||||
|
||||
|
||||
// Database Reconnect
|
||||
const DATABASE_RECONNECT_SLEEP = 2;
|
||||
const DATABASE_RECONNECT_MAX_ATTEMPTS = 10;
|
||||
@@ -297,3 +353,6 @@ const TOKENS_RESOURCE_TYPE_DATABASES = 'databases';
|
||||
const SCHEDULE_RESOURCE_TYPE_EXECUTION = 'execution';
|
||||
const SCHEDULE_RESOURCE_TYPE_FUNCTION = 'function';
|
||||
const SCHEDULE_RESOURCE_TYPE_MESSAGE = 'message';
|
||||
|
||||
/** Preview cookie */
|
||||
const COOKIE_NAME_PREVIEW = 'a_jwt_console';
|
||||
|
||||
+97
-47
@@ -2,7 +2,6 @@
|
||||
|
||||
use Ahc\Jwt\JWT;
|
||||
use Ahc\Jwt\JWTException;
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Auth\Key;
|
||||
use Appwrite\Databases\TransactionState;
|
||||
use Appwrite\Event\Audit;
|
||||
@@ -23,10 +22,18 @@ use Appwrite\Extend\Exception;
|
||||
use Appwrite\GraphQL\Schema;
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\Network\Validator\Origin;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Executor\Executor;
|
||||
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
|
||||
use Utopia\App;
|
||||
use Utopia\Auth\Hashes\Argon2;
|
||||
use Utopia\Auth\Hashes\Sha;
|
||||
use Utopia\Auth\Proofs\Code;
|
||||
use Utopia\Auth\Proofs\Password;
|
||||
use Utopia\Auth\Proofs\Token;
|
||||
use Utopia\Auth\Store;
|
||||
use Utopia\Cache\Adapter\Pool as CachePool;
|
||||
use Utopia\Cache\Adapter\Sharding;
|
||||
use Utopia\Cache\Cache;
|
||||
@@ -226,76 +233,91 @@ App::setResource('platforms', function (Request $request, Document $console, Doc
|
||||
];
|
||||
}, ['request', 'console', 'project', 'dbForPlatform']);
|
||||
|
||||
App::setResource('user', function ($mode, $project, $console, $request, $response, $dbForProject, $dbForPlatform) {
|
||||
/** @var Appwrite\Utopia\Request $request */
|
||||
/** @var Appwrite\Utopia\Response $response */
|
||||
/** @var Utopia\Database\Document $project */
|
||||
/** @var Utopia\Database\Database $dbForProject */
|
||||
/** @var Utopia\Database\Database $dbForPlatform */
|
||||
/** @var string $mode */
|
||||
App::setResource('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken) {
|
||||
/**
|
||||
* Handles user authentication and session validation.
|
||||
*
|
||||
* This function follows a series of steps to determine the appropriate user session
|
||||
* based on cookies, headers, and JWT tokens.
|
||||
*
|
||||
* Process:
|
||||
* 1. Checks the cookie based on mode:
|
||||
* - If in admin mode, uses console project id for key.
|
||||
* - Otherwise, sets the key using the project ID
|
||||
* 2. If no cookie is found, attempts to retrieve the fallback header `x-fallback-cookies`.
|
||||
* - If this method is used, returns the header: `X-Debug-Fallback: true`.
|
||||
* 3. Fetches the user document from the appropriate database based on the mode.
|
||||
* 4. If the user document is empty or the session key cannot be verified, sets an empty user document.
|
||||
* 5. Regardless of the results from steps 1-4, attempts to fetch the JWT token.
|
||||
* 6. If the JWT user has a valid session ID, updates the user variable with the user from `projectDB`,
|
||||
* overwriting the previous value.
|
||||
*/
|
||||
|
||||
Authorization::setDefaultStatus(true);
|
||||
|
||||
Auth::setCookieName('a_session_' . $project->getId());
|
||||
$store->setKey('a_session_' . $project->getId());
|
||||
|
||||
if (APP_MODE_ADMIN === $mode) {
|
||||
Auth::setCookieName('a_session_' . $console->getId());
|
||||
$store->setKey('a_session_' . $console->getId());
|
||||
}
|
||||
|
||||
$session = Auth::decodeSession(
|
||||
$store->decode(
|
||||
$request->getCookie(
|
||||
Auth::$cookieName, // Get sessions
|
||||
$request->getCookie(Auth::$cookieName . '_legacy', '')
|
||||
$store->getKey(), // Get sessions
|
||||
$request->getCookie($store->getKey() . '_legacy', '')
|
||||
)
|
||||
);
|
||||
|
||||
// Get session from header for SSR clients
|
||||
if (empty($session['id']) && empty($session['secret'])) {
|
||||
if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) {
|
||||
$sessionHeader = $request->getHeader('x-appwrite-session', '');
|
||||
|
||||
if (!empty($sessionHeader)) {
|
||||
$session = Auth::decodeSession($sessionHeader);
|
||||
$store->decode($sessionHeader);
|
||||
}
|
||||
}
|
||||
|
||||
// Get fallback session from old clients (no SameSite support) or clients who block 3rd-party cookies
|
||||
if ($response) {
|
||||
if ($response) { // if in http context - add debug header
|
||||
$response->addHeader('X-Debug-Fallback', 'false');
|
||||
}
|
||||
|
||||
if (empty($session['id']) && empty($session['secret'])) {
|
||||
if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) {
|
||||
if ($response) {
|
||||
$response->addHeader('X-Debug-Fallback', 'true');
|
||||
}
|
||||
$fallback = $request->getHeader('x-fallback-cookies', '');
|
||||
$fallback = \json_decode($fallback, true);
|
||||
$session = Auth::decodeSession(((isset($fallback[Auth::$cookieName])) ? $fallback[Auth::$cookieName] : ''));
|
||||
$store->decode(((is_array($fallback) && isset($fallback[$store->getKey()])) ? $fallback[$store->getKey()] : ''));
|
||||
}
|
||||
|
||||
Auth::$unique = $session['id'] ?? '';
|
||||
Auth::$secret = $session['secret'] ?? '';
|
||||
|
||||
$user = new Document([]);
|
||||
|
||||
if (!empty(Auth::$unique)) {
|
||||
if ($mode === APP_MODE_ADMIN) {
|
||||
$user = $dbForPlatform->getDocument('users', Auth::$unique);
|
||||
} elseif (!$project->isEmpty()) {
|
||||
if ($project->getId() === 'console') {
|
||||
$user = $dbForPlatform->getDocument('users', Auth::$unique);
|
||||
} else {
|
||||
$user = $dbForProject->getDocument('users', Auth::$unique);
|
||||
$user = null;
|
||||
if (APP_MODE_ADMIN === $mode) {
|
||||
/** @var User $user */
|
||||
$user = $dbForPlatform->getDocument('users', $store->getProperty('id', ''));
|
||||
} else {
|
||||
if ($project->isEmpty()) {
|
||||
$user = new User([]);
|
||||
} else {
|
||||
if (!empty($store->getProperty('id', ''))) {
|
||||
if ($project->getId() === 'console') {
|
||||
/** @var User $user */
|
||||
$user = $dbForPlatform->getDocument('users', $store->getProperty('id', ''));
|
||||
} else {
|
||||
/** @var User $user */
|
||||
$user = $dbForProject->getDocument('users', $store->getProperty('id', ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!$user ||
|
||||
$user->isEmpty() // Check a document has been found in the DB
|
||||
|| !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret)
|
||||
|| !$user->sessionVerify($store->getProperty('secret', ''), $proofForToken)
|
||||
) { // Validate user has valid login token
|
||||
$user = new Document([]);
|
||||
$user = new User([]);
|
||||
}
|
||||
|
||||
// if (APP_MODE_ADMIN === $mode) {
|
||||
// if ($user->find('teamInternalId', $project->getAttribute('teamInternalId'), 'memberships')) {
|
||||
// Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users.
|
||||
@@ -303,18 +325,14 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
|
||||
// $user = new Document([]);
|
||||
// }
|
||||
// }
|
||||
|
||||
$authJWT = $request->getHeader('x-appwrite-jwt', '');
|
||||
|
||||
if (!empty($authJWT) && !$project->isEmpty()) { // JWT authentication
|
||||
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0);
|
||||
|
||||
try {
|
||||
$payload = $jwt->decode($authJWT);
|
||||
} catch (JWTException $error) {
|
||||
throw new Exception(Exception::USER_JWT_INVALID, 'Failed to verify JWT. ' . $error->getMessage());
|
||||
}
|
||||
|
||||
$jwtUserId = $payload['userId'] ?? '';
|
||||
if (!empty($jwtUserId)) {
|
||||
if ($mode === APP_MODE_ADMIN) {
|
||||
@@ -323,20 +341,18 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
|
||||
$user = $dbForProject->getDocument('users', $jwtUserId);
|
||||
}
|
||||
}
|
||||
|
||||
$jwtSessionId = $payload['sessionId'] ?? '';
|
||||
if (!empty($jwtSessionId)) {
|
||||
if (empty($user->find('$id', $jwtSessionId, 'sessions'))) { // Match JWT to active token
|
||||
$user = new Document([]);
|
||||
$user = new User([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$dbForProject->setMetadata('user', $user->getId());
|
||||
$dbForPlatform->setMetadata('user', $user->getId());
|
||||
|
||||
return $user;
|
||||
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform']);
|
||||
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken']);
|
||||
|
||||
App::setResource('project', function ($dbForPlatform, $request, $console) {
|
||||
/** @var Appwrite\Utopia\Request $request */
|
||||
@@ -354,31 +370,61 @@ App::setResource('project', function ($dbForPlatform, $request, $console) {
|
||||
return $project;
|
||||
}, ['dbForPlatform', 'request', 'console']);
|
||||
|
||||
App::setResource('session', function (Document $user) {
|
||||
App::setResource('session', function (User $user, Store $store, Token $proofForToken) {
|
||||
if ($user->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sessions = $user->getAttribute('sessions', []);
|
||||
$sessionId = Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret);
|
||||
$sessionId = $user->sessionVerify($store->getProperty('secret', ''), $proofForToken);
|
||||
|
||||
if (!$sessionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($sessions as $session) {/** @var Document $session */
|
||||
foreach ($sessions as $session) {
|
||||
/** @var Document $session */
|
||||
if ($sessionId === $session->getId()) {
|
||||
return $session;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}, ['user']);
|
||||
}, ['user', 'store', 'proofForToken']);
|
||||
|
||||
App::setResource('console', function () {
|
||||
return new Document(Config::getParam('console'));
|
||||
}, []);
|
||||
|
||||
App::setResource('store', function (): Store {
|
||||
return new Store();
|
||||
});
|
||||
|
||||
App::setResource('proofForPassword', function (): Password {
|
||||
$hash = new Argon2();
|
||||
$hash
|
||||
->setMemoryCost(7168)
|
||||
->setTimeCost(5)
|
||||
->setThreads(1);
|
||||
|
||||
$password = new Password();
|
||||
$password
|
||||
->setHash($hash);
|
||||
|
||||
return $password;
|
||||
});
|
||||
|
||||
App::setResource('proofForToken', function (): Token {
|
||||
$token = new Token();
|
||||
$token->setHash(new Sha());
|
||||
return $token;
|
||||
});
|
||||
|
||||
App::setResource('proofForCode', function (): Code {
|
||||
$code = new Code();
|
||||
$code->setHash(new Sha());
|
||||
return $code;
|
||||
});
|
||||
|
||||
App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $dbForPlatform;
|
||||
@@ -399,6 +445,7 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform
|
||||
->setMetadata('project', $project->getId())
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
|
||||
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
|
||||
$database->setDocumentType('users', User::class);
|
||||
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
@@ -428,6 +475,8 @@ App::setResource('dbForPlatform', function (Group $pools, Cache $cache) {
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
|
||||
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
|
||||
|
||||
$database->setDocumentType('users', User::class);
|
||||
|
||||
return $database;
|
||||
}, ['pools', 'cache']);
|
||||
|
||||
@@ -452,6 +501,7 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform
|
||||
->setMetadata('project', $project->getId())
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
|
||||
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
|
||||
$database->setDocumentType('users', User::class);
|
||||
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
|
||||
+29
-11
@@ -1,11 +1,11 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Extend\Exception as AppwriteException;
|
||||
use Appwrite\Messaging\Adapter\Realtime;
|
||||
use Appwrite\Network\Validator\Origin;
|
||||
use Appwrite\PubSub\Adapter\Pool as PubSubPool;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Swoole\Http\Request as SwooleRequest;
|
||||
@@ -16,6 +16,9 @@ use Swoole\Timer;
|
||||
use Utopia\Abuse\Abuse;
|
||||
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
|
||||
use Utopia\App;
|
||||
use Utopia\Auth\Hashes\Sha;
|
||||
use Utopia\Auth\Proofs\Token;
|
||||
use Utopia\Auth\Store;
|
||||
use Utopia\Cache\Adapter\Pool as CachePool;
|
||||
use Utopia\Cache\Adapter\Sharding;
|
||||
use Utopia\Cache\Cache;
|
||||
@@ -67,6 +70,8 @@ if (!function_exists('getConsoleDB')) {
|
||||
->setMetadata('host', \gethostname())
|
||||
->setMetadata('project', '_console');
|
||||
|
||||
$database->setDocumentType('users', User::class);
|
||||
|
||||
return $database;
|
||||
}
|
||||
}
|
||||
@@ -118,6 +123,8 @@ if (!function_exists('getProjectDB')) {
|
||||
->setMetadata('host', \gethostname())
|
||||
->setMetadata('project', $project->getId());
|
||||
|
||||
$database->setDocumentType('users', User::class);
|
||||
|
||||
return $databases[$project->getSequence()] = $database;
|
||||
}
|
||||
}
|
||||
@@ -457,9 +464,10 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
$project = Authorization::skip(fn () => $consoleDatabase->getDocument('projects', $projectId));
|
||||
$database = getProjectDB($project);
|
||||
|
||||
/** @var Appwrite\Utopia\Database\Documents\User $user */
|
||||
$user = $database->getDocument('users', $userId);
|
||||
|
||||
$roles = Auth::getRoles($user);
|
||||
$roles = $user->getRoles();
|
||||
$channels = $realtime->connections[$connection]['channels'];
|
||||
|
||||
$realtime->unsubscribe($connection);
|
||||
@@ -526,14 +534,14 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
if (
|
||||
array_key_exists('realtime', $project->getAttribute('apis', []))
|
||||
&& !$project->getAttribute('apis', [])['realtime']
|
||||
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
|
||||
&& !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles()))
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
|
||||
}
|
||||
|
||||
$timelimit = $app->getResource('timelimit');
|
||||
$platforms = $app->getResource('platforms');
|
||||
$user = $app->getResource('user'); /** @var Document $user */
|
||||
$user = $app->getResource('user'); /** @var User $user */
|
||||
|
||||
/*
|
||||
* Abuse Check
|
||||
@@ -563,7 +571,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $originValidator->getDescription());
|
||||
}
|
||||
|
||||
$roles = Auth::getRoles($user);
|
||||
$roles = $user->getRoles();
|
||||
|
||||
$channels = Realtime::convertChannels($request->getQuery('channels', []), $user->getId());
|
||||
|
||||
@@ -678,21 +686,31 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Payload is not valid.');
|
||||
}
|
||||
|
||||
$session = Auth::decodeSession($message['data']['session']);
|
||||
Auth::$unique = $session['id'] ?? '';
|
||||
Auth::$secret = $session['secret'] ?? '';
|
||||
$store = new Store();
|
||||
|
||||
$user = $database->getDocument('users', Auth::$unique);
|
||||
$store->decode($message['data']['session']);
|
||||
|
||||
/** @var User $user */
|
||||
$user = $database->getDocument('users', $store->getProperty('id', ''));
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
* Moving forward, we should try to use our dependency injection container
|
||||
* to inject the proof for token.
|
||||
* This way we will have one source of truth for the proof for token.
|
||||
*/
|
||||
$proofForToken = new Token();
|
||||
$proofForToken->setHash(new Sha());
|
||||
|
||||
if (
|
||||
empty($user->getId()) // Check a document has been found in the DB
|
||||
|| !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret) // Validate user has valid login token
|
||||
|| !$user->sessionVerify($store->getProperty('secret', ''), $proofForToken) // Validate user has valid login token
|
||||
) {
|
||||
// cookie not valid
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Session is not valid.');
|
||||
}
|
||||
|
||||
$roles = Auth::getRoles($user);
|
||||
$roles = $user->getRoles();
|
||||
$channels = Realtime::convertChannels(array_flip($realtime->connections[$connection]['channels']), $user->getId());
|
||||
$realtime->subscribe($realtime->connections[$connection]['projectId'], $connection, $roles, $channels);
|
||||
|
||||
|
||||
+3
-1
@@ -17,6 +17,7 @@ use Appwrite\Event\Realtime;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Event\Webhook;
|
||||
use Appwrite\Platform\Appwrite;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Executor\Executor;
|
||||
use Swoole\Runtime;
|
||||
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
|
||||
@@ -55,7 +56,7 @@ Server::setResource('dbForPlatform', function (Cache $cache, Registry $register)
|
||||
$adapter = new DatabasePool($pools->get('console'));
|
||||
$dbForPlatform = new Database($adapter, $cache);
|
||||
$dbForPlatform->setNamespace('_console');
|
||||
|
||||
$dbForPlatform->setDocumentType('users', User::class);
|
||||
return $dbForPlatform;
|
||||
}, ['cache', 'register']);
|
||||
|
||||
@@ -86,6 +87,7 @@ Server::setResource('dbForProject', function (Cache $cache, Registry $register,
|
||||
|
||||
$adapter = new DatabasePool($pools->get($dsn->getHost()));
|
||||
$database = new Database($adapter, $cache);
|
||||
$database->setDocumentType('users', User::class);
|
||||
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"utopia-php/abuse": "1.*",
|
||||
"utopia-php/analytics": "0.10.*",
|
||||
"utopia-php/audit": "1.*",
|
||||
"utopia-php/auth": "0.5.*",
|
||||
"utopia-php/cache": "0.13.*",
|
||||
"utopia-php/cli": "0.15.*",
|
||||
"utopia-php/config": "1.*.*",
|
||||
|
||||
Generated
+129
-73
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "1d3d4b19a835b3be79a63146fcdd389b",
|
||||
"content-hash": "479b161d08b29d22267c4c7798751842",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
@@ -1236,16 +1236,16 @@
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/api",
|
||||
"version": "1.7.0",
|
||||
"version": "1.7.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/opentelemetry-php/api.git",
|
||||
"reference": "610b79ad9d6d97e8368bcb6c4d42394fbb87b522"
|
||||
"reference": "45bda7efa8fcdd9bdb0daa2f26c8e31f062f49d4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/api/zipball/610b79ad9d6d97e8368bcb6c4d42394fbb87b522",
|
||||
"reference": "610b79ad9d6d97e8368bcb6c4d42394fbb87b522",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/api/zipball/45bda7efa8fcdd9bdb0daa2f26c8e31f062f49d4",
|
||||
"reference": "45bda7efa8fcdd9bdb0daa2f26c8e31f062f49d4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1265,7 +1265,7 @@
|
||||
]
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-main": "1.7.x-dev"
|
||||
"dev-main": "1.8.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
@@ -1298,11 +1298,11 @@
|
||||
],
|
||||
"support": {
|
||||
"chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V",
|
||||
"docs": "https://opentelemetry.io/docs/php",
|
||||
"docs": "https://opentelemetry.io/docs/languages/php",
|
||||
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
|
||||
"source": "https://github.com/open-telemetry/opentelemetry-php"
|
||||
},
|
||||
"time": "2025-10-02T23:44:28+00:00"
|
||||
"time": "2025-10-19T10:49:48+00:00"
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/context",
|
||||
@@ -1365,16 +1365,16 @@
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/exporter-otlp",
|
||||
"version": "1.3.2",
|
||||
"version": "1.3.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/opentelemetry-php/exporter-otlp.git",
|
||||
"reference": "196f3a1dbce3b2c0f8110d164232c11ac00ddbb2"
|
||||
"reference": "07b02bc71838463f6edcc78d3485c04b48fb263d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/196f3a1dbce3b2c0f8110d164232c11ac00ddbb2",
|
||||
"reference": "196f3a1dbce3b2c0f8110d164232c11ac00ddbb2",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/07b02bc71838463f6edcc78d3485c04b48fb263d",
|
||||
"reference": "07b02bc71838463f6edcc78d3485c04b48fb263d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1421,11 +1421,11 @@
|
||||
],
|
||||
"support": {
|
||||
"chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V",
|
||||
"docs": "https://opentelemetry.io/docs/php",
|
||||
"docs": "https://opentelemetry.io/docs/languages/php",
|
||||
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
|
||||
"source": "https://github.com/open-telemetry/opentelemetry-php"
|
||||
},
|
||||
"time": "2025-06-16T00:24:51+00:00"
|
||||
"time": "2025-11-13T08:04:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/gen-otlp-protobuf",
|
||||
@@ -1492,16 +1492,16 @@
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/sdk",
|
||||
"version": "1.9.0",
|
||||
"version": "1.10.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/opentelemetry-php/sdk.git",
|
||||
"reference": "8986bcbcbea79cb1ba9e91c1d621541ad63d6b3e"
|
||||
"reference": "3dfc3d1ad729ec7eb25f1b9a4ae39fe779affa99"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/8986bcbcbea79cb1ba9e91c1d621541ad63d6b3e",
|
||||
"reference": "8986bcbcbea79cb1ba9e91c1d621541ad63d6b3e",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/3dfc3d1ad729ec7eb25f1b9a4ae39fe779affa99",
|
||||
"reference": "3dfc3d1ad729ec7eb25f1b9a4ae39fe779affa99",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1581,11 +1581,11 @@
|
||||
],
|
||||
"support": {
|
||||
"chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V",
|
||||
"docs": "https://opentelemetry.io/docs/php",
|
||||
"docs": "https://opentelemetry.io/docs/languages/php",
|
||||
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
|
||||
"source": "https://github.com/open-telemetry/opentelemetry-php"
|
||||
},
|
||||
"time": "2025-10-02T23:44:28+00:00"
|
||||
"time": "2025-11-25T10:59:15+00:00"
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/sem-conv",
|
||||
@@ -3596,6 +3596,61 @@
|
||||
},
|
||||
"time": "2025-10-20T07:14:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/auth",
|
||||
"version": "0.5.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/auth.git",
|
||||
"reference": "5ad0ded3a79f153ee904b97b49f8dfe4669e4fd0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/auth/zipball/5ad0ded3a79f153ee904b97b49f8dfe4669e4fd0",
|
||||
"reference": "5ad0ded3a79f153ee904b97b49f8dfe4669e4fd0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-hash": "*",
|
||||
"ext-scrypt": "*",
|
||||
"ext-sodium": "*",
|
||||
"php": ">=8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "1.2.*",
|
||||
"phpstan/phpstan": "1.9.x-dev",
|
||||
"phpunit/phpunit": "^9.3",
|
||||
"vimeo/psalm": "4.0.1"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Utopia\\Auth\\": "src/Auth"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Utopia PHP",
|
||||
"email": "team@appwrite.io"
|
||||
}
|
||||
],
|
||||
"description": "A simple PHP authentication library",
|
||||
"keywords": [
|
||||
"Authentication",
|
||||
"auth",
|
||||
"php",
|
||||
"security"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/auth/issues",
|
||||
"source": "https://github.com/utopia-php/auth/tree/0.5.0"
|
||||
},
|
||||
"time": "2025-10-29T07:11:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/cache",
|
||||
"version": "0.13.1",
|
||||
@@ -3943,16 +3998,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/dns",
|
||||
"version": "1.1.3",
|
||||
"version": "1.1.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/dns.git",
|
||||
"reference": "1e6b4bac735329c9e5ec69a6a5d899ec2d050707"
|
||||
"reference": "eea6b9299a1420ae6c574f16eb1e9da8689ac56b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/dns/zipball/1e6b4bac735329c9e5ec69a6a5d899ec2d050707",
|
||||
"reference": "1e6b4bac735329c9e5ec69a6a5d899ec2d050707",
|
||||
"url": "https://api.github.com/repos/utopia-php/dns/zipball/eea6b9299a1420ae6c574f16eb1e9da8689ac56b",
|
||||
"reference": "eea6b9299a1420ae6c574f16eb1e9da8689ac56b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3960,7 +4015,7 @@
|
||||
"utopia-php/console": "0.0.*",
|
||||
"utopia-php/domains": "0.9.*",
|
||||
"utopia-php/telemetry": "0.1.*",
|
||||
"utopia-php/validators": "^0.0.2"
|
||||
"utopia-php/validators": "0.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "1.25.*",
|
||||
@@ -3994,32 +4049,32 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/dns/issues",
|
||||
"source": "https://github.com/utopia-php/dns/tree/1.1.3"
|
||||
"source": "https://github.com/utopia-php/dns/tree/1.1.4"
|
||||
},
|
||||
"time": "2025-11-06T19:08:29+00:00"
|
||||
"time": "2025-11-26T13:38:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/domains",
|
||||
"version": "0.9.1",
|
||||
"version": "0.9.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/domains.git",
|
||||
"reference": "99b4ec95d5d6b7a5c990a66c56412212d9af37e7"
|
||||
"reference": "52b654f8a0e170bfa2e54cb47755b256822477c7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/domains/zipball/99b4ec95d5d6b7a5c990a66c56412212d9af37e7",
|
||||
"reference": "99b4ec95d5d6b7a5c990a66c56412212d9af37e7",
|
||||
"url": "https://api.github.com/repos/utopia-php/domains/zipball/52b654f8a0e170bfa2e54cb47755b256822477c7",
|
||||
"reference": "52b654f8a0e170bfa2e54cb47755b256822477c7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.0",
|
||||
"php": ">=8.2",
|
||||
"utopia-php/cache": "0.13.*",
|
||||
"utopia-php/validators": "0.0.*"
|
||||
"utopia-php/validators": "0.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "1.2.*",
|
||||
"phpstan/phpstan": "1.9.x-dev",
|
||||
"laravel/pint": "^1.18",
|
||||
"phpstan/phpstan": "^1.12",
|
||||
"phpunit/phpunit": "^9.3"
|
||||
},
|
||||
"type": "library",
|
||||
@@ -4056,9 +4111,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/domains/issues",
|
||||
"source": "https://github.com/utopia-php/domains/tree/0.9.1"
|
||||
"source": "https://github.com/utopia-php/domains/tree/0.9.2"
|
||||
},
|
||||
"time": "2025-10-21T14:52:27+00:00"
|
||||
"time": "2025-11-26T12:16:36+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/dsn",
|
||||
@@ -4109,16 +4164,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/emails",
|
||||
"version": "0.6.2",
|
||||
"version": "0.6.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/emails.git",
|
||||
"reference": "9c4c40cf7c03c2e9e21364566f9b192d03ea93c9"
|
||||
"reference": "9524d7f7bd1651a06fef8a3d964f774b04fe2918"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/emails/zipball/9c4c40cf7c03c2e9e21364566f9b192d03ea93c9",
|
||||
"reference": "9c4c40cf7c03c2e9e21364566f9b192d03ea93c9",
|
||||
"url": "https://api.github.com/repos/utopia-php/emails/zipball/9524d7f7bd1651a06fef8a3d964f774b04fe2918",
|
||||
"reference": "9524d7f7bd1651a06fef8a3d964f774b04fe2918",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4126,7 +4181,7 @@
|
||||
"utopia-php/cli": "^0.15",
|
||||
"utopia-php/domains": "^0.9",
|
||||
"utopia-php/fetch": "^0.4",
|
||||
"utopia-php/validators": "^0.0.2"
|
||||
"utopia-php/validators": "0.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "1.25.*",
|
||||
@@ -4163,9 +4218,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/emails/issues",
|
||||
"source": "https://github.com/utopia-php/emails/tree/0.6.2"
|
||||
"source": "https://github.com/utopia-php/emails/tree/0.6.3"
|
||||
},
|
||||
"time": "2025-10-28T16:08:17+00:00"
|
||||
"time": "2025-11-26T12:27:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/fetch",
|
||||
@@ -4208,22 +4263,23 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/framework",
|
||||
"version": "0.33.30",
|
||||
"version": "0.33.33",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/http.git",
|
||||
"reference": "07cf699a7c47bd1a03b4da1812f1719a66b3c924"
|
||||
"reference": "838e3a28276e73187bc34a314f014096dc92191b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/http/zipball/07cf699a7c47bd1a03b4da1812f1719a66b3c924",
|
||||
"reference": "07cf699a7c47bd1a03b4da1812f1719a66b3c924",
|
||||
"url": "https://api.github.com/repos/utopia-php/http/zipball/838e3a28276e73187bc34a314f014096dc92191b",
|
||||
"reference": "838e3a28276e73187bc34a314f014096dc92191b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"utopia-php/compression": "0.1.*",
|
||||
"utopia-php/telemetry": "0.1.*"
|
||||
"utopia-php/telemetry": "0.1.*",
|
||||
"utopia-php/validators": "0.1.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "^1.2",
|
||||
@@ -4249,9 +4305,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/http/issues",
|
||||
"source": "https://github.com/utopia-php/http/tree/0.33.30"
|
||||
"source": "https://github.com/utopia-php/http/tree/0.33.33"
|
||||
},
|
||||
"time": "2025-11-18T12:18:00+00:00"
|
||||
"time": "2025-11-25T10:21:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/image",
|
||||
@@ -5110,26 +5166,25 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/validators",
|
||||
"version": "0.0.2",
|
||||
"version": "0.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/validators.git",
|
||||
"reference": "894210695c5d35fa248fb65f7fe7237b6ff4fb0b"
|
||||
"reference": "5c57d5b6cf964f8981807c1d3ea8df620c869080"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/validators/zipball/894210695c5d35fa248fb65f7fe7237b6ff4fb0b",
|
||||
"reference": "894210695c5d35fa248fb65f7fe7237b6ff4fb0b",
|
||||
"url": "https://api.github.com/repos/utopia-php/validators/zipball/5c57d5b6cf964f8981807c1d3ea8df620c869080",
|
||||
"reference": "5c57d5b6cf964f8981807c1d3ea8df620c869080",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
"php": ">=8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-xdebug": "*",
|
||||
"laravel/pint": "^1.2",
|
||||
"laravel/pint": "1.*",
|
||||
"phpstan/phpstan": "1.*",
|
||||
"phpunit/phpunit": "^9.5.25"
|
||||
"phpunit/phpunit": "11.*"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
@@ -5150,9 +5205,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/validators/issues",
|
||||
"source": "https://github.com/utopia-php/validators/tree/0.0.2"
|
||||
"source": "https://github.com/utopia-php/validators/tree/0.1.0"
|
||||
},
|
||||
"time": "2025-10-20T21:52:28+00:00"
|
||||
"time": "2025-11-18T11:05:46+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/vcs",
|
||||
@@ -5654,16 +5709,16 @@
|
||||
},
|
||||
{
|
||||
"name": "laravel/pint",
|
||||
"version": "v1.25.1",
|
||||
"version": "v1.26.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/pint.git",
|
||||
"reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9"
|
||||
"reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/pint/zipball/5016e263f95d97670d71b9a987bd8996ade6d8d9",
|
||||
"reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9",
|
||||
"url": "https://api.github.com/repos/laravel/pint/zipball/69dcca060ecb15e4b564af63d1f642c81a241d6f",
|
||||
"reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5674,13 +5729,13 @@
|
||||
"php": "^8.2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.87.2",
|
||||
"illuminate/view": "^11.46.0",
|
||||
"larastan/larastan": "^3.7.1",
|
||||
"laravel-zero/framework": "^11.45.0",
|
||||
"friendsofphp/php-cs-fixer": "^3.90.0",
|
||||
"illuminate/view": "^12.40.1",
|
||||
"larastan/larastan": "^3.8.0",
|
||||
"laravel-zero/framework": "^12.0.4",
|
||||
"mockery/mockery": "^1.6.12",
|
||||
"nunomaduro/termwind": "^2.3.1",
|
||||
"pestphp/pest": "^2.36.0"
|
||||
"nunomaduro/termwind": "^2.3.3",
|
||||
"pestphp/pest": "^3.8.4"
|
||||
},
|
||||
"bin": [
|
||||
"builds/pint"
|
||||
@@ -5706,6 +5761,7 @@
|
||||
"description": "An opinionated code formatter for PHP.",
|
||||
"homepage": "https://laravel.com",
|
||||
"keywords": [
|
||||
"dev",
|
||||
"format",
|
||||
"formatter",
|
||||
"lint",
|
||||
@@ -5716,7 +5772,7 @@
|
||||
"issues": "https://github.com/laravel/pint/issues",
|
||||
"source": "https://github.com/laravel/pint"
|
||||
},
|
||||
"time": "2025-09-19T02:57:12+00:00"
|
||||
"time": "2025-11-25T21:15:52+00:00"
|
||||
},
|
||||
{
|
||||
"name": "matthiasmullie/minify",
|
||||
@@ -8917,5 +8973,5 @@
|
||||
"platform-overrides": {
|
||||
"php": "8.3"
|
||||
},
|
||||
"plugin-api-version": "2.9.0"
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
||||
@@ -1,515 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Auth;
|
||||
|
||||
use Appwrite\Auth\Hash\Argon2;
|
||||
use Appwrite\Auth\Hash\Bcrypt;
|
||||
use Appwrite\Auth\Hash\Md5;
|
||||
use Appwrite\Auth\Hash\Phpass;
|
||||
use Appwrite\Auth\Hash\Scrypt;
|
||||
use Appwrite\Auth\Hash\Scryptmodified;
|
||||
use Appwrite\Auth\Hash\Sha;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Roles;
|
||||
|
||||
class Auth
|
||||
{
|
||||
public const SUPPORTED_ALGOS = [
|
||||
'argon2',
|
||||
'bcrypt',
|
||||
'md5',
|
||||
'sha',
|
||||
'phpass',
|
||||
'scrypt',
|
||||
'scryptMod',
|
||||
'plaintext'
|
||||
];
|
||||
|
||||
public const DEFAULT_ALGO = 'argon2';
|
||||
public const DEFAULT_ALGO_OPTIONS = ['type' => 'argon2', 'memoryCost' => 2048, 'timeCost' => 4, 'threads' => 3];
|
||||
|
||||
/**
|
||||
* User Roles.
|
||||
*/
|
||||
public const USER_ROLE_ANY = 'any';
|
||||
public const USER_ROLE_GUESTS = 'guests';
|
||||
public const USER_ROLE_USERS = 'users';
|
||||
public const USER_ROLE_ADMIN = 'admin';
|
||||
public const USER_ROLE_DEVELOPER = 'developer';
|
||||
public const USER_ROLE_OWNER = 'owner';
|
||||
public const USER_ROLE_APPS = 'apps';
|
||||
public const USER_ROLE_SYSTEM = 'system';
|
||||
|
||||
/**
|
||||
* Activity associated with user or the app.
|
||||
*/
|
||||
public const ACTIVITY_TYPE_APP = 'app';
|
||||
public const ACTIVITY_TYPE_USER = 'user';
|
||||
public const ACTIVITY_TYPE_GUEST = 'guest';
|
||||
|
||||
/**
|
||||
* Token Types.
|
||||
*/
|
||||
public const TOKEN_TYPE_LOGIN = 1; // Deprecated
|
||||
public const TOKEN_TYPE_VERIFICATION = 2;
|
||||
public const TOKEN_TYPE_RECOVERY = 3;
|
||||
public const TOKEN_TYPE_INVITE = 4;
|
||||
public const TOKEN_TYPE_MAGIC_URL = 5;
|
||||
public const TOKEN_TYPE_PHONE = 6;
|
||||
public const TOKEN_TYPE_OAUTH2 = 7;
|
||||
public const TOKEN_TYPE_GENERIC = 8;
|
||||
public const TOKEN_TYPE_EMAIL = 9; // OTP
|
||||
|
||||
/**
|
||||
* Session Providers.
|
||||
*/
|
||||
public const SESSION_PROVIDER_EMAIL = 'email';
|
||||
public const SESSION_PROVIDER_ANONYMOUS = 'anonymous';
|
||||
public const SESSION_PROVIDER_MAGIC_URL = 'magic-url';
|
||||
public const SESSION_PROVIDER_PHONE = 'phone';
|
||||
public const SESSION_PROVIDER_OAUTH2 = 'oauth2';
|
||||
public const SESSION_PROVIDER_TOKEN = 'token';
|
||||
public const SESSION_PROVIDER_SERVER = 'server';
|
||||
|
||||
/**
|
||||
* Token Expiration times.
|
||||
*/
|
||||
public const TOKEN_EXPIRATION_LOGIN_LONG = 31536000; /* 1 year */
|
||||
public const TOKEN_EXPIRATION_LOGIN_SHORT = 3600; /* 1 hour */
|
||||
public const TOKEN_EXPIRATION_RECOVERY = 3600; /* 1 hour */
|
||||
public const TOKEN_EXPIRATION_CONFIRM = 3600 * 1; /* 1 hour */
|
||||
public const TOKEN_EXPIRATION_OTP = 60 * 15; /* 15 minutes */
|
||||
public const TOKEN_EXPIRATION_GENERIC = 60 * 15; /* 15 minutes */
|
||||
|
||||
/**
|
||||
* Token Lengths.
|
||||
*/
|
||||
public const TOKEN_LENGTH_MAGIC_URL = 64;
|
||||
public const TOKEN_LENGTH_VERIFICATION = 256;
|
||||
public const TOKEN_LENGTH_RECOVERY = 256;
|
||||
public const TOKEN_LENGTH_OAUTH2 = 64;
|
||||
public const TOKEN_LENGTH_SESSION = 256;
|
||||
|
||||
/**
|
||||
* MFA
|
||||
*/
|
||||
public const MFA_RECENT_DURATION = 1800; // 30 mins
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public static $cookieName = 'a_session';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public static $cookieNamePreview = 'a_jwt_console';
|
||||
|
||||
/**
|
||||
* User Unique ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $unique = '';
|
||||
|
||||
/**
|
||||
* User Secret Key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $secret = '';
|
||||
|
||||
/**
|
||||
* Set Cookie Name.
|
||||
*
|
||||
* @param $string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function setCookieName($string)
|
||||
{
|
||||
return self::$cookieName = $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode Session.
|
||||
*
|
||||
* @param string $id
|
||||
* @param string $secret
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function encodeSession($id, $secret)
|
||||
{
|
||||
return \base64_encode(\json_encode([
|
||||
'id' => $id,
|
||||
'secret' => $secret,
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Token type to session provider mapping.
|
||||
*/
|
||||
public static function getSessionProviderByTokenType(int $type): string
|
||||
{
|
||||
switch ($type) {
|
||||
case Auth::TOKEN_TYPE_VERIFICATION:
|
||||
case Auth::TOKEN_TYPE_RECOVERY:
|
||||
case Auth::TOKEN_TYPE_INVITE:
|
||||
return Auth::SESSION_PROVIDER_EMAIL;
|
||||
case Auth::TOKEN_TYPE_MAGIC_URL:
|
||||
return Auth::SESSION_PROVIDER_MAGIC_URL;
|
||||
case Auth::TOKEN_TYPE_PHONE:
|
||||
return Auth::SESSION_PROVIDER_PHONE;
|
||||
case Auth::TOKEN_TYPE_OAUTH2:
|
||||
return Auth::SESSION_PROVIDER_OAUTH2;
|
||||
default:
|
||||
return Auth::SESSION_PROVIDER_TOKEN;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode Session.
|
||||
*
|
||||
* @param string $session
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function decodeSession($session)
|
||||
{
|
||||
$session = \json_decode(\base64_decode($session), true);
|
||||
$default = ['id' => null, 'secret' => ''];
|
||||
|
||||
if (!\is_array($session)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return \array_merge($default, $session);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode.
|
||||
*
|
||||
* One-way encryption
|
||||
*
|
||||
* @param $string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function hash(string $string)
|
||||
{
|
||||
return \hash('sha256', $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Password Hash.
|
||||
*
|
||||
* One way string hashing for user passwords
|
||||
*
|
||||
* @param string $string
|
||||
* @param string $algo hashing algorithm to use
|
||||
* @param array $options algo-specific options
|
||||
*
|
||||
* @return bool|string|null
|
||||
*/
|
||||
public static function passwordHash(string $string, string $algo, array $options = [])
|
||||
{
|
||||
// Plain text not supported, just an alias. Switch to recommended algo
|
||||
if ($algo === 'plaintext') {
|
||||
$algo = Auth::DEFAULT_ALGO;
|
||||
$options = Auth::DEFAULT_ALGO_OPTIONS;
|
||||
}
|
||||
|
||||
if (!\in_array($algo, Auth::SUPPORTED_ALGOS)) {
|
||||
throw new \Exception('Hashing algorithm \'' . $algo . '\' is not supported.');
|
||||
}
|
||||
|
||||
switch ($algo) {
|
||||
case 'argon2':
|
||||
$hasher = new Argon2($options);
|
||||
return $hasher->hash($string);
|
||||
case 'bcrypt':
|
||||
$hasher = new Bcrypt($options);
|
||||
return $hasher->hash($string);
|
||||
case 'md5':
|
||||
$hasher = new Md5($options);
|
||||
return $hasher->hash($string);
|
||||
case 'sha':
|
||||
$hasher = new Sha($options);
|
||||
return $hasher->hash($string);
|
||||
case 'phpass':
|
||||
$hasher = new Phpass($options);
|
||||
return $hasher->hash($string);
|
||||
case 'scrypt':
|
||||
$hasher = new Scrypt($options);
|
||||
return $hasher->hash($string);
|
||||
case 'scryptMod':
|
||||
$hasher = new Scryptmodified($options);
|
||||
return $hasher->hash($string);
|
||||
default:
|
||||
throw new \Exception('Hashing algorithm \'' . $algo . '\' is not supported.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Password verify.
|
||||
*
|
||||
* @param string $plain
|
||||
* @param string $hash
|
||||
* @param string $algo hashing algorithm used to hash
|
||||
* @param array $options algo-specific options
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function passwordVerify(string $plain, string $hash, string $algo, array $options = [])
|
||||
{
|
||||
// Plain text not supported, just an alias. Switch to recommended algo
|
||||
if ($algo === 'plaintext') {
|
||||
$algo = Auth::DEFAULT_ALGO;
|
||||
$options = Auth::DEFAULT_ALGO_OPTIONS;
|
||||
}
|
||||
|
||||
if (!\in_array($algo, Auth::SUPPORTED_ALGOS)) {
|
||||
throw new \Exception('Hashing algorithm \'' . $algo . '\' is not supported.');
|
||||
}
|
||||
|
||||
switch ($algo) {
|
||||
case 'argon2':
|
||||
$hasher = new Argon2($options);
|
||||
return $hasher->verify($plain, $hash);
|
||||
case 'bcrypt':
|
||||
$hasher = new Bcrypt($options);
|
||||
return $hasher->verify($plain, $hash);
|
||||
case 'md5':
|
||||
$hasher = new Md5($options);
|
||||
return $hasher->verify($plain, $hash);
|
||||
case 'sha':
|
||||
$hasher = new Sha($options);
|
||||
return $hasher->verify($plain, $hash);
|
||||
case 'phpass':
|
||||
$hasher = new Phpass($options);
|
||||
return $hasher->verify($plain, $hash);
|
||||
case 'scrypt':
|
||||
$hasher = new Scrypt($options);
|
||||
return $hasher->verify($plain, $hash);
|
||||
case 'scryptMod':
|
||||
$hasher = new Scryptmodified($options);
|
||||
return $hasher->verify($plain, $hash);
|
||||
default:
|
||||
throw new \Exception('Hashing algorithm \'' . $algo . '\' is not supported.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Password Generator.
|
||||
*
|
||||
* Generate random password string
|
||||
*
|
||||
* @param int $length
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function passwordGenerator(int $length = 20): string
|
||||
{
|
||||
return \bin2hex(\random_bytes($length));
|
||||
}
|
||||
|
||||
/**
|
||||
* Token Generator.
|
||||
*
|
||||
* Generate random password string
|
||||
*
|
||||
* @param int $length Length of returned token
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function tokenGenerator(int $length = 256): string
|
||||
{
|
||||
if ($length <= 0) {
|
||||
throw new \Exception('Token length must be greater than 0');
|
||||
}
|
||||
|
||||
$bytesLength = (int) ceil($length / 2);
|
||||
$token = \bin2hex(\random_bytes($bytesLength));
|
||||
|
||||
return substr($token, 0, $length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Code Generator.
|
||||
*
|
||||
* Generate random code string
|
||||
*
|
||||
* @param int $length
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function codeGenerator(int $length = 6): string
|
||||
{
|
||||
$value = '';
|
||||
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$value .= random_int(0, 9);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify token and check that its not expired.
|
||||
*
|
||||
* @param array<Document> $tokens
|
||||
* @param int $type Type of token to verify, if null will verify any type
|
||||
* @param string $secret
|
||||
*
|
||||
* @return false|Document
|
||||
*/
|
||||
public static function tokenVerify(array $tokens, int $type = null, string $secret): false|Document
|
||||
{
|
||||
foreach ($tokens as $token) {
|
||||
if (
|
||||
$token->isSet('secret') &&
|
||||
$token->isSet('expire') &&
|
||||
$token->isSet('type') &&
|
||||
($type === null || $token->getAttribute('type') === $type) &&
|
||||
$token->getAttribute('secret') === self::hash($secret) &&
|
||||
DateTime::formatTz($token->getAttribute('expire')) >= DateTime::formatTz(DateTime::now())
|
||||
) {
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify session and check that its not expired.
|
||||
*
|
||||
* @param array<Document> $sessions
|
||||
* @param string $secret
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public static function sessionVerify(array $sessions, string $secret)
|
||||
{
|
||||
foreach ($sessions as $session) {
|
||||
if (
|
||||
$session->isSet('secret') &&
|
||||
$session->isSet('provider') &&
|
||||
$session->getAttribute('secret') === self::hash($secret) &&
|
||||
DateTime::formatTz(DateTime::format(new \DateTime($session->getAttribute('expire')))) >= DateTime::formatTz(DateTime::now())
|
||||
) {
|
||||
return $session->getId();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is Privileged User?
|
||||
*
|
||||
* @param array<string> $roles
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isPrivilegedUser(array $roles): bool
|
||||
{
|
||||
if (
|
||||
in_array(self::USER_ROLE_OWNER, $roles) ||
|
||||
in_array(self::USER_ROLE_DEVELOPER, $roles) ||
|
||||
in_array(self::USER_ROLE_ADMIN, $roles)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is App User?
|
||||
*
|
||||
* @param array<string> $roles
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isAppUser(array $roles): bool
|
||||
{
|
||||
if (in_array(self::USER_ROLE_APPS, $roles)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all roles for a user.
|
||||
*
|
||||
* @param Document $user
|
||||
* @return array<string>
|
||||
*/
|
||||
public static function getRoles(Document $user): array
|
||||
{
|
||||
$roles = [];
|
||||
|
||||
if (!self::isPrivilegedUser(Authorization::getRoles()) && !self::isAppUser(Authorization::getRoles())) {
|
||||
if ($user->getId()) {
|
||||
$roles[] = Role::user($user->getId())->toString();
|
||||
$roles[] = Role::users()->toString();
|
||||
|
||||
$emailVerified = $user->getAttribute('emailVerification', false);
|
||||
$phoneVerified = $user->getAttribute('phoneVerification', false);
|
||||
|
||||
if ($emailVerified || $phoneVerified) {
|
||||
$roles[] = Role::user($user->getId(), Roles::DIMENSION_VERIFIED)->toString();
|
||||
$roles[] = Role::users(Roles::DIMENSION_VERIFIED)->toString();
|
||||
} else {
|
||||
$roles[] = Role::user($user->getId(), Roles::DIMENSION_UNVERIFIED)->toString();
|
||||
$roles[] = Role::users(Roles::DIMENSION_UNVERIFIED)->toString();
|
||||
}
|
||||
} else {
|
||||
return [Role::guests()->toString()];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($user->getAttribute('memberships', []) as $node) {
|
||||
if (!isset($node['confirm']) || !$node['confirm']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($node['$id']) && isset($node['teamId'])) {
|
||||
$roles[] = Role::team($node['teamId'])->toString();
|
||||
$roles[] = Role::member($node['$id'])->toString();
|
||||
|
||||
if (isset($node['roles'])) {
|
||||
foreach ($node['roles'] as $nodeRole) { // Set all team roles
|
||||
$roles[] = Role::team($node['teamId'], $nodeRole)->toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($user->getAttribute('labels', []) as $label) {
|
||||
$roles[] = 'label:' . $label;
|
||||
}
|
||||
|
||||
return $roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is anonymous.
|
||||
*
|
||||
* @param Document $user
|
||||
* @return bool
|
||||
*/
|
||||
public static function isAnonymousUser(Document $user): bool
|
||||
{
|
||||
return is_null($user->getAttribute('email'))
|
||||
&& is_null($user->getAttribute('phone'));
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Auth;
|
||||
|
||||
abstract class Hash
|
||||
{
|
||||
/**
|
||||
* @var array $options Hashing-algo specific options
|
||||
*/
|
||||
protected array $options = [];
|
||||
|
||||
/**
|
||||
* @param array $options Hashing-algo specific options
|
||||
*/
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
$this->setOptions($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set hashing algo options
|
||||
*
|
||||
* @param array $options Hashing-algo specific options
|
||||
*/
|
||||
public function setOptions(array $options): self
|
||||
{
|
||||
$this->options = \array_merge([], $this->getDefaultOptions(), $options);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hashing algo options
|
||||
*
|
||||
* @return array $options Hashing-algo specific options
|
||||
*/
|
||||
public function getOptions(): array
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $password Input password to hash
|
||||
*
|
||||
* @return string hash
|
||||
*/
|
||||
abstract public function hash(string $password): string;
|
||||
|
||||
/**
|
||||
* @param string $password Input password to validate
|
||||
* @param string $hash Hash to verify password against
|
||||
*
|
||||
* @return boolean true if password matches hash
|
||||
*/
|
||||
abstract public function verify(string $password, string $hash): bool;
|
||||
|
||||
/**
|
||||
* Get default options for specific hashing algo
|
||||
*
|
||||
* @return array options named array
|
||||
*/
|
||||
abstract public function getDefaultOptions(): array;
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Auth\Hash;
|
||||
|
||||
use Appwrite\Auth\Hash;
|
||||
|
||||
/*
|
||||
* Argon2 accepted options:
|
||||
* int threads
|
||||
* int time_cost
|
||||
* int memory_cost
|
||||
*
|
||||
* Reference: https://www.php.net/manual/en/function.password-hash.php#example-983
|
||||
*/
|
||||
class Argon2 extends Hash
|
||||
{
|
||||
/**
|
||||
* @param string $password Input password to hash
|
||||
*
|
||||
* @return string hash
|
||||
*/
|
||||
public function hash(string $password): string
|
||||
{
|
||||
return \password_hash($password, PASSWORD_ARGON2ID, $this->getOptions());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $password Input password to validate
|
||||
* @param string $hash Hash to verify password against
|
||||
*
|
||||
* @return boolean true if password matches hash
|
||||
*/
|
||||
public function verify(string $password, string $hash): bool
|
||||
{
|
||||
return \password_verify($password, $hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default options for specific hashing algo
|
||||
*
|
||||
* @return array options named array
|
||||
*/
|
||||
public function getDefaultOptions(): array
|
||||
{
|
||||
return ['memory_cost' => 65536, 'time_cost' => 4, 'threads' => 3];
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Auth\Hash;
|
||||
|
||||
use Appwrite\Auth\Hash;
|
||||
|
||||
/*
|
||||
* Bcrypt accepted options:
|
||||
* int cost
|
||||
* string? salt; auto-generated if empty
|
||||
*
|
||||
* Reference: https://www.php.net/manual/en/password.constants.php
|
||||
*/
|
||||
class Bcrypt extends Hash
|
||||
{
|
||||
/**
|
||||
* @param string $password Input password to hash
|
||||
*
|
||||
* @return string hash
|
||||
*/
|
||||
public function hash(string $password): string
|
||||
{
|
||||
return \password_hash($password, PASSWORD_BCRYPT, $this->getOptions());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $password Input password to validate
|
||||
* @param string $hash Hash to verify password against
|
||||
*
|
||||
* @return boolean true if password matches hash
|
||||
*/
|
||||
public function verify(string $password, string $hash): bool
|
||||
{
|
||||
return \password_verify($password, $hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default options for specific hashing algo
|
||||
*
|
||||
* @return array options named array
|
||||
*/
|
||||
public function getDefaultOptions(): array
|
||||
{
|
||||
return [ 'cost' => 8 ];
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Auth\Hash;
|
||||
|
||||
use Appwrite\Auth\Hash;
|
||||
|
||||
/*
|
||||
* MD5 does not accept any options.
|
||||
*
|
||||
* Reference: https://www.php.net/manual/en/function.md5.php
|
||||
*/
|
||||
class Md5 extends Hash
|
||||
{
|
||||
/**
|
||||
* @param string $password Input password to hash
|
||||
*
|
||||
* @return string hash
|
||||
*/
|
||||
public function hash(string $password): string
|
||||
{
|
||||
return \md5($password);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $password Input password to validate
|
||||
* @param string $hash Hash to verify password against
|
||||
*
|
||||
* @return boolean true if password matches hash
|
||||
*/
|
||||
public function verify(string $password, string $hash): bool
|
||||
{
|
||||
return $this->hash($password) === $hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default options for specific hashing algo
|
||||
*
|
||||
* @return array options named array
|
||||
*/
|
||||
public function getDefaultOptions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -1,290 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Portable PHP password hashing framework.
|
||||
* source Version 0.5 / genuine.
|
||||
* Written by Solar Designer <solar at openwall.com> in 2004-2017 and placed in
|
||||
* the public domain. Revised in subsequent years, still public domain.
|
||||
* There's absolutely no warranty.
|
||||
* The homepage URL for the source framework is: http://www.openwall.com/phpass/
|
||||
* Please be sure to update the Version line if you edit this file in any way.
|
||||
* It is suggested that you leave the main version number intact, but indicate
|
||||
* your project name (after the slash) and add your own revision information.
|
||||
* Please do not change the "private" password hashing method implemented in
|
||||
* here, thereby making your hashes incompatible. However, if you must, please
|
||||
* change the hash type identifier (the "$P$") to something different.
|
||||
* Obviously, since this code is in the public domain, the above are not
|
||||
* requirements (there can be none), but merely suggestions.
|
||||
*
|
||||
* @author Solar Designer <solar@openwall.com>
|
||||
* @copyright Copyright (C) 2017 All rights reserved.
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Appwrite\Auth\Hash;
|
||||
|
||||
use Appwrite\Auth\Hash;
|
||||
|
||||
/*
|
||||
* PHPass accepted options:
|
||||
* int iteration_count_log2; The Logarithmic cost value used when generating hash values indicating the number of rounds used to generate hashes
|
||||
* string portable_hashes
|
||||
* string random_state; The cached random state
|
||||
*
|
||||
* Reference: https://github.com/photodude/phpass
|
||||
*/
|
||||
class Phpass extends Hash
|
||||
{
|
||||
/**
|
||||
* Alphabet used in itoa64 conversions.
|
||||
*
|
||||
* @var string
|
||||
* @since 0.1.0
|
||||
*/
|
||||
protected string $itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
/**
|
||||
* Get default options for specific hashing algo
|
||||
*
|
||||
* @return array options named array
|
||||
*/
|
||||
public function getDefaultOptions(): array
|
||||
{
|
||||
$randomState = \microtime();
|
||||
if (\function_exists('getmypid')) {
|
||||
$randomState .= getmypid();
|
||||
}
|
||||
|
||||
return ['iteration_count_log2' => 8, 'portable_hashes' => false, 'random_state' => $randomState];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $password Input password to hash
|
||||
*
|
||||
* @return string hash
|
||||
*/
|
||||
public function hash(string $password): string
|
||||
{
|
||||
$options = $this->getDefaultOptions();
|
||||
|
||||
$random = '';
|
||||
if (CRYPT_BLOWFISH === 1 && !$options['portable_hashes']) {
|
||||
$random = $this->getRandomBytes(16, $options);
|
||||
$hash = crypt($password, $this->gensaltBlowfish($random, $options));
|
||||
if (strlen($hash) === 60) {
|
||||
return $hash;
|
||||
}
|
||||
}
|
||||
if (strlen($random) < 6) {
|
||||
$random = $this->getRandomBytes(6, $options);
|
||||
}
|
||||
$hash = $this->cryptPrivate($password, $this->gensaltPrivate($random, $options));
|
||||
if (strlen($hash) === 34) {
|
||||
return $hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returning '*' on error is safe here, but would _not_ be safe
|
||||
* in a crypt(3)-like function used _both_ for generating new
|
||||
* hashes and for validating passwords against existing hashes.
|
||||
*/
|
||||
return '*';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $password Input password to validate
|
||||
* @param string $hash Hash to verify password against
|
||||
*
|
||||
* @return boolean true if password matches hash
|
||||
*/
|
||||
public function verify(string $password, string $hash): bool
|
||||
{
|
||||
$verificationHash = $this->cryptPrivate($password, $hash);
|
||||
if ($verificationHash[0] === '*') {
|
||||
$verificationHash = crypt($password, $hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is not constant-time. In order to keep the code simple,
|
||||
* for timing safety we currently rely on the salts being
|
||||
* unpredictable, which they are at least in the non-fallback
|
||||
* cases (that is, when we use /dev/urandom and bcrypt).
|
||||
*/
|
||||
return $hash === $verificationHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $count
|
||||
*
|
||||
* @return String $output
|
||||
* @since 0.1.0
|
||||
* @throws Exception Thows an Exception if the $count parameter is not a positive integer.
|
||||
*/
|
||||
protected function getRandomBytes(int $count, array $options): string
|
||||
{
|
||||
if (!is_int($count) || $count < 1) {
|
||||
throw new \Exception('Argument count must be a positive integer');
|
||||
}
|
||||
$output = '';
|
||||
if (@is_readable('/dev/urandom') && ($fh = @fopen('/dev/urandom', 'rb'))) {
|
||||
$output = fread($fh, $count);
|
||||
fclose($fh);
|
||||
}
|
||||
|
||||
if (strlen($output) < $count) {
|
||||
$output = '';
|
||||
|
||||
for ($i = 0; $i < $count; $i += 16) {
|
||||
$options['iteration_count_log2'] = md5(microtime() . $options['iteration_count_log2']);
|
||||
$output .= md5($options['iteration_count_log2'], true);
|
||||
}
|
||||
|
||||
$output = substr($output, 0, $count);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param String $input
|
||||
* @param int $count
|
||||
*
|
||||
* @return String $output
|
||||
* @since 0.1.0
|
||||
* @throws Exception Thows an Exception if the $count parameter is not a positive integer.
|
||||
*/
|
||||
protected function encode64($input, $count)
|
||||
{
|
||||
if (!is_int($count) || $count < 1) {
|
||||
throw new \Exception('Argument count must be a positive integer');
|
||||
}
|
||||
$output = '';
|
||||
$i = 0;
|
||||
do {
|
||||
$value = ord($input[$i++]);
|
||||
$output .= $this->itoa64[$value & 0x3f];
|
||||
if ($i < $count) {
|
||||
$value |= ord($input[$i]) << 8;
|
||||
}
|
||||
$output .= $this->itoa64[($value >> 6) & 0x3f];
|
||||
if ($i++ >= $count) {
|
||||
break;
|
||||
}
|
||||
if ($i < $count) {
|
||||
$value |= ord($input[$i]) << 16;
|
||||
}
|
||||
$output .= $this->itoa64[($value >> 12) & 0x3f];
|
||||
if ($i++ >= $count) {
|
||||
break;
|
||||
}
|
||||
$output .= $this->itoa64[($value >> 18) & 0x3f];
|
||||
} while ($i < $count);
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param String $input
|
||||
*
|
||||
* @return String $output
|
||||
* @since 0.1.0
|
||||
*/
|
||||
private function gensaltPrivate($input, $options)
|
||||
{
|
||||
$output = '$P$';
|
||||
$output .= $this->itoa64[min($options['iteration_count_log2'] + ((PHP_VERSION >= '5') ? 5 : 3), 30)];
|
||||
$output .= $this->encode64($input, 6);
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param String $password
|
||||
* @param String $setting
|
||||
*
|
||||
* @return String $output
|
||||
* @since 0.1.0
|
||||
*/
|
||||
private function cryptPrivate($password, $setting)
|
||||
{
|
||||
$output = '*0';
|
||||
if (substr($setting, 0, 2) === $output) {
|
||||
$output = '*1';
|
||||
}
|
||||
$id = substr($setting, 0, 3);
|
||||
// We use "$P$", phpBB3 uses "$H$" for the same thing
|
||||
if ($id !== '$P$' && $id !== '$H$') {
|
||||
return $output;
|
||||
}
|
||||
$count_log2 = strpos($this->itoa64, $setting[3]);
|
||||
if ($count_log2 < 7 || $count_log2 > 30) {
|
||||
return $output;
|
||||
}
|
||||
$count = 1 << $count_log2;
|
||||
$salt = substr($setting, 4, 8);
|
||||
if (strlen($salt) !== 8) {
|
||||
return $output;
|
||||
}
|
||||
/**
|
||||
* We were kind of forced to use MD5 here since it's the only
|
||||
* cryptographic primitive that was available in all versions of PHP
|
||||
* in use. To implement our own low-level crypto in PHP
|
||||
* would have result in much worse performance and
|
||||
* consequently in lower iteration counts and hashes that are
|
||||
* quicker to crack (by non-PHP code).
|
||||
*/
|
||||
$hash = md5($salt . $password, true);
|
||||
do {
|
||||
$hash = md5($hash . $password, true);
|
||||
} while (--$count);
|
||||
$output = substr($setting, 0, 12);
|
||||
$output .= $this->encode64($hash, 16);
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param String $input
|
||||
*
|
||||
* @return String $output
|
||||
* @since 0.1.0
|
||||
*/
|
||||
private function gensaltBlowfish($input, $options)
|
||||
{
|
||||
/**
|
||||
* This one needs to use a different order of characters and a
|
||||
* different encoding scheme from the one in encode64() above.
|
||||
* We care because the last character in our encoded string will
|
||||
* only represent 2 bits. While two known implementations of
|
||||
* bcrypt will happily accept and correct a salt string which
|
||||
* has the 4 unused bits set to non-zero, we do not want to take
|
||||
* chances and we also do not want to waste an additional byte
|
||||
* of entropy.
|
||||
*/
|
||||
$itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
$output = '$2a$';
|
||||
$output .= chr(ord('0') + intval($options['iteration_count_log2'] / 10));
|
||||
$output .= chr(ord('0') + $options['iteration_count_log2'] % 10);
|
||||
$output .= '$';
|
||||
$i = 0;
|
||||
do {
|
||||
$c1 = ord($input[$i++]);
|
||||
$output .= $itoa64[$c1 >> 2];
|
||||
$c1 = ($c1 & 0x03) << 4;
|
||||
if ($i >= 16) {
|
||||
$output .= $itoa64[$c1];
|
||||
break;
|
||||
}
|
||||
$c2 = ord($input[$i++]);
|
||||
$c1 |= $c2 >> 4;
|
||||
$output .= $itoa64[$c1];
|
||||
$c1 = ($c2 & 0x0f) << 2;
|
||||
$c2 = ord($input[$i++]);
|
||||
$c1 |= $c2 >> 6;
|
||||
$output .= $itoa64[$c1];
|
||||
$output .= $itoa64[$c2 & 0x3f];
|
||||
} while (1);
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Auth\Hash;
|
||||
|
||||
use Appwrite\Auth\Hash;
|
||||
|
||||
/*
|
||||
* Scrypt accepted options:
|
||||
* string? salt; auto-generated if empty
|
||||
* int costCpu
|
||||
* int costMemory
|
||||
* int costParallel
|
||||
* int length
|
||||
*
|
||||
* Reference: https://github.com/DomBlack/php-scrypt/blob/master/scrypt.php#L112-L116
|
||||
*/
|
||||
class Scrypt extends Hash
|
||||
{
|
||||
/**
|
||||
* @param string $password Input password to hash
|
||||
*
|
||||
* @return string hash
|
||||
*/
|
||||
public function hash(string $password): string
|
||||
{
|
||||
$options = $this->getOptions();
|
||||
|
||||
return \scrypt($password, $options['salt'], $options['costCpu'], $options['costMemory'], $options['costParallel'], $options['length']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $password Input password to validate
|
||||
* @param string $hash Hash to verify password against
|
||||
*
|
||||
* @return boolean true if password matches hash
|
||||
*/
|
||||
public function verify(string $password, string $hash): bool
|
||||
{
|
||||
return $hash === $this->hash($password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default options for specific hashing algo
|
||||
*
|
||||
* @return array options named array
|
||||
*/
|
||||
public function getDefaultOptions(): array
|
||||
{
|
||||
return [ 'costCpu' => 8, 'costMemory' => 14, 'costParallel' => 1, 'length' => 64 ];
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Auth\Hash;
|
||||
|
||||
use Appwrite\Auth\Hash;
|
||||
|
||||
/*
|
||||
* This is Scrypt hash with some additional steps added by Google.
|
||||
*
|
||||
* string salt
|
||||
* string saltSeparator
|
||||
* strin signerKey
|
||||
*
|
||||
* Reference: https://github.com/DomBlack/php-scrypt/blob/master/scrypt.php#L112-L116
|
||||
*/
|
||||
class Scryptmodified extends Hash
|
||||
{
|
||||
/**
|
||||
* @param string $password Input password to hash
|
||||
*
|
||||
* @return string hash
|
||||
*/
|
||||
public function hash(string $password): string
|
||||
{
|
||||
$options = $this->getOptions();
|
||||
|
||||
$derivedKeyBytes = $this->generateDerivedKey($password);
|
||||
$signerKeyBytes = \base64_decode($options['signerKey']);
|
||||
|
||||
$hashedPassword = $this->hashKeys($signerKeyBytes, $derivedKeyBytes);
|
||||
|
||||
return \base64_encode($hashedPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $password Input password to validate
|
||||
* @param string $hash Hash to verify password against
|
||||
*
|
||||
* @return boolean true if password matches hash
|
||||
*/
|
||||
public function verify(string $password, string $hash): bool
|
||||
{
|
||||
return $this->hash($password) === $hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default options for specific hashing algo
|
||||
*
|
||||
* @return array options named array
|
||||
*/
|
||||
public function getDefaultOptions(): array
|
||||
{
|
||||
return [ ];
|
||||
}
|
||||
|
||||
private function generateDerivedKey(string $password)
|
||||
{
|
||||
$options = $this->getOptions();
|
||||
|
||||
$saltBytes = \base64_decode($options['salt']);
|
||||
$saltSeparatorBytes = \base64_decode($options['saltSeparator']);
|
||||
|
||||
$password = mb_convert_encoding($password, 'UTF-8');
|
||||
$derivedKey = \scrypt($password, $saltBytes . $saltSeparatorBytes, 16384, 8, 1, 64);
|
||||
$derivedKey = \hex2bin($derivedKey);
|
||||
|
||||
return $derivedKey;
|
||||
}
|
||||
|
||||
private function hashKeys($signerKeyBytes, $derivedKeyBytes): string
|
||||
{
|
||||
$key = \substr($derivedKeyBytes, 0, 32);
|
||||
|
||||
$iv = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
|
||||
|
||||
$hash = \openssl_encrypt($signerKeyBytes, 'aes-256-ctr', $key, OPENSSL_RAW_DATA, $iv);
|
||||
|
||||
return $hash;
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Auth\Hash;
|
||||
|
||||
use Appwrite\Auth\Hash;
|
||||
|
||||
/*
|
||||
* SHA accepted options:
|
||||
* string? version. Allowed:
|
||||
* - Version 1: sha1
|
||||
* - Version 2: sha224, sha256, sha384, sha512/224, sha512/256, sha512
|
||||
* - Version 3: sha3-224, sha3-256, sha3-384, sha3-512
|
||||
*
|
||||
* Reference: https://www.php.net/manual/en/function.hash-algos.php
|
||||
*/
|
||||
class Sha extends Hash
|
||||
{
|
||||
/**
|
||||
* @param string $password Input password to hash
|
||||
*
|
||||
* @return string hash
|
||||
*/
|
||||
public function hash(string $password): string
|
||||
{
|
||||
$algo = $this->getOptions()['version'];
|
||||
|
||||
return \hash($algo, $password);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $password Input password to validate
|
||||
* @param string $hash Hash to verify password against
|
||||
*
|
||||
* @return boolean true if password matches hash
|
||||
*/
|
||||
public function verify(string $password, string $hash): bool
|
||||
{
|
||||
return $this->hash($password) === $hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default options for specific hashing algo
|
||||
*
|
||||
* @return array options named array
|
||||
*/
|
||||
public function getDefaultOptions(): array
|
||||
{
|
||||
return [ 'version' => 'sha3-512' ];
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace Appwrite\Auth;
|
||||
use Ahc\Jwt\JWT;
|
||||
use Ahc\Jwt\JWTException;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
@@ -110,16 +111,16 @@ class Key
|
||||
$secret = $key;
|
||||
}
|
||||
|
||||
$role = Auth::USER_ROLE_APPS;
|
||||
$role = User::ROLE_APPS;
|
||||
$roles = Config::getParam('roles', []);
|
||||
$scopes = $roles[Auth::USER_ROLE_APPS]['scopes'] ?? [];
|
||||
$scopes = $roles[User::ROLE_APPS]['scopes'] ?? [];
|
||||
$expired = false;
|
||||
|
||||
$guestKey = new Key(
|
||||
$project->getId(),
|
||||
$type,
|
||||
Auth::USER_ROLE_GUESTS,
|
||||
$roles[Auth::USER_ROLE_GUESTS]['scopes'] ?? [],
|
||||
User::ROLE_GUESTS,
|
||||
$roles[User::ROLE_GUESTS]['scopes'] ?? [],
|
||||
'UNKNOWN'
|
||||
);
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace Appwrite\Auth\MFA;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use OTPHP\OTP;
|
||||
use Utopia\Auth\Proofs\Token;
|
||||
|
||||
abstract class Type
|
||||
{
|
||||
@@ -51,9 +51,10 @@ abstract class Type
|
||||
public static function generateBackupCodes(int $length = 10, int $total = 6): array
|
||||
{
|
||||
$backups = [];
|
||||
$token = new Token($length);
|
||||
|
||||
for ($i = 0; $i < $total; $i++) {
|
||||
$backups[] = Auth::tokenGenerator($length);
|
||||
$backups[] = $token->generate();
|
||||
}
|
||||
|
||||
return $backups;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Appwrite\Auth\Validator;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Utopia\Auth\Hash;
|
||||
|
||||
/**
|
||||
* Password.
|
||||
@@ -12,16 +12,14 @@ use Appwrite\Auth\Auth;
|
||||
class PasswordHistory extends Password
|
||||
{
|
||||
protected array $history;
|
||||
protected string $algo;
|
||||
protected array $algoOptions;
|
||||
protected Hash $hash;
|
||||
|
||||
public function __construct(array $history, string $algo, array $algoOptions = [])
|
||||
public function __construct(array $history, Hash $hash)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->history = $history;
|
||||
$this->algo = $algo;
|
||||
$this->algoOptions = $algoOptions;
|
||||
$this->hash = $hash;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,7 +44,7 @@ class PasswordHistory extends Password
|
||||
public function isValid($value): bool
|
||||
{
|
||||
foreach ($this->history as $hash) {
|
||||
if (!empty($hash) && Auth::passwordVerify($value, $hash, $this->algo, $this->algoOptions)) {
|
||||
if (!empty($hash) && $this->hash->verify($value, $hash)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Appwrite\Migration\Version;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Migration\Migration;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Config\Config;
|
||||
@@ -118,7 +117,7 @@ class V16 extends Migration
|
||||
* Set default authDuration
|
||||
*/
|
||||
$document->setAttribute('auths', array_merge($document->getAttribute('auths', []), [
|
||||
'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG
|
||||
'duration' => TOKEN_EXPIRATION_LOGIN_LONG
|
||||
]));
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace Appwrite\Migration\Version;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Migration\Migration;
|
||||
use Utopia\Auth\Proofs\Password;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
@@ -270,7 +270,7 @@ class V17 extends Migration
|
||||
* Set hashOptions type
|
||||
*/
|
||||
$document->setAttribute('hashOptions', array_merge($document->getAttribute('hashOptions', []), [
|
||||
'type' => $document->getAttribute('hash', Auth::DEFAULT_ALGO)
|
||||
'type' => $document->getAttribute('hash', (new Password())->getHash()->getName())
|
||||
]));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Appwrite\Migration\Version;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Migration\Migration;
|
||||
use Exception;
|
||||
use PDOException;
|
||||
@@ -632,15 +631,15 @@ class V20 extends Migration
|
||||
}
|
||||
break;
|
||||
case 'sessions':
|
||||
$duration = $this->project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
$duration = $this->project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
$expire = DateTime::addSeconds(new \DateTime(), $duration);
|
||||
$document->setAttribute('expire', $expire);
|
||||
|
||||
$factors = match ($document->getAttribute('provider')) {
|
||||
Auth::SESSION_PROVIDER_EMAIL => ['password'],
|
||||
Auth::SESSION_PROVIDER_PHONE => ['phone'],
|
||||
Auth::SESSION_PROVIDER_ANONYMOUS => ['anonymous'],
|
||||
Auth::SESSION_PROVIDER_TOKEN => ['token'],
|
||||
SESSION_PROVIDER_EMAIL => ['password'],
|
||||
SESSION_PROVIDER_PHONE => ['phone'],
|
||||
SESSION_PROVIDER_ANONYMOUS => ['anonymous'],
|
||||
SESSION_PROVIDER_TOKEN => ['token'],
|
||||
default => ['email'],
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Account\Http\Account\MFA\Challenges;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Auth\MFA\Type;
|
||||
use Appwrite\Detector\Detector;
|
||||
use Appwrite\Event\Event;
|
||||
@@ -19,6 +18,8 @@ use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use libphonenumber\PhoneNumberUtil;
|
||||
use Utopia\Abuse\Abuse;
|
||||
use Utopia\Auth\Proofs\Code as ProofsCode;
|
||||
use Utopia\Auth\Proofs\Token as ProofsToken;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
@@ -102,6 +103,8 @@ class Create extends Action
|
||||
->inject('timelimit')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('plan')
|
||||
->inject('proofForToken')
|
||||
->inject('proofForCode')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -118,15 +121,18 @@ class Create extends Action
|
||||
Mail $queueForMails,
|
||||
callable $timelimit,
|
||||
StatsUsage $queueForStatsUsage,
|
||||
array $plan
|
||||
array $plan,
|
||||
ProofsToken $proofForToken,
|
||||
ProofsCode $proofForCode
|
||||
): void {
|
||||
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_CONFIRM));
|
||||
$code = Auth::codeGenerator();
|
||||
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), TOKEN_EXPIRATION_CONFIRM));
|
||||
|
||||
$code = $proofForCode->generate();
|
||||
$challenge = new Document([
|
||||
'userId' => $user->getId(),
|
||||
'userInternalId' => $user->getSequence(),
|
||||
'type' => $factor,
|
||||
'token' => Auth::tokenGenerator(),
|
||||
'token' => $proofForToken->generate(),
|
||||
'code' => $code,
|
||||
'expire' => $expire,
|
||||
'$permissions' => [
|
||||
|
||||
+3
-3
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Attribute;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
@@ -12,6 +11,7 @@ use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Deprecated;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use InvalidArgumentException;
|
||||
use Utopia\Database\Database;
|
||||
@@ -90,8 +90,8 @@ class Decrement extends Action
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $min, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, array $plan): void
|
||||
{
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAPIKey = User::isApp(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty()) {
|
||||
|
||||
+3
-3
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Attribute;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
@@ -12,6 +11,7 @@ use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Deprecated;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use InvalidArgumentException;
|
||||
use Utopia\Database\Database;
|
||||
@@ -90,8 +90,8 @@ class Increment extends Action
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, array $plan): void
|
||||
{
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAPIKey = User::isApp(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty()) {
|
||||
|
||||
+3
-3
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
@@ -12,6 +11,7 @@ use Appwrite\SDK\Deprecated;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Parameter;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
@@ -178,8 +178,8 @@ class Create extends Action
|
||||
$documents = [$data];
|
||||
}
|
||||
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAPIKey = User::isApp(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
|
||||
if ($isBulk && !$isAPIKey && !$isPrivilegedUser) {
|
||||
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE);
|
||||
|
||||
+3
-3
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Databases\TransactionState;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
@@ -12,6 +11,7 @@ use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Deprecated;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
@@ -101,8 +101,8 @@ class Delete extends Action
|
||||
): void {
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAPIKey = User::isApp(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
|
||||
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
|
||||
+3
-3
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Databases\TransactionState;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
@@ -11,6 +10,7 @@ use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Deprecated;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
@@ -75,8 +75,8 @@ class Get extends Action
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, TransactionState $transactionState): void
|
||||
{
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAPIKey = User::isApp(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
|
||||
+3
-3
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Databases\TransactionState;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
@@ -12,6 +11,7 @@ use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Deprecated;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
@@ -100,8 +100,8 @@ class Update extends Action
|
||||
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAPIKey = User::isApp(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
|
||||
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
|
||||
+3
-3
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Databases\TransactionState;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
@@ -12,6 +11,7 @@ use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Deprecated;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
@@ -106,8 +106,8 @@ class Upsert extends Action
|
||||
throw new Exception($this->getMissingPayloadException());
|
||||
}
|
||||
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAPIKey = User::isApp(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
|
||||
+3
-3
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Databases\TransactionState;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
@@ -11,6 +10,7 @@ use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Deprecated;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
@@ -79,8 +79,8 @@ class XList extends Action
|
||||
|
||||
public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, bool $includeTotal, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, TransactionState $transactionState): void
|
||||
{
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAPIKey = User::isApp(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
|
||||
+3
-3
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Operations;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Databases\TransactionState;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Transactions\Action;
|
||||
@@ -10,6 +9,7 @@ use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Database\Validator\Operation;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
@@ -72,8 +72,8 @@ class Create extends Action
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Operations array cannot be empty');
|
||||
}
|
||||
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAPIKey = User::isApp(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
|
||||
// API keys and admins can read any transaction, regular users need permissions
|
||||
$transaction = ($isAPIKey || $isPrivilegedUser)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Databases\TransactionState;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
@@ -12,6 +11,7 @@ use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
@@ -111,8 +111,8 @@ class Update extends Action
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Cannot commit and rollback at the same time');
|
||||
}
|
||||
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAPIKey = User::isApp(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
|
||||
$transaction = ($isAPIKey || $isPrivilegedUser)
|
||||
? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
|
||||
@@ -240,13 +240,12 @@ class Update extends Action
|
||||
->setType(DELETE_TYPE_DOCUMENT)
|
||||
->setDocument($transaction);
|
||||
});
|
||||
|
||||
} catch (NotFoundException $e) {
|
||||
Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
|
||||
'status' => 'failed',
|
||||
])));
|
||||
throw new Exception(Exception::DOCUMENT_NOT_FOUND, previous: $e);
|
||||
} catch (DuplicateException|ConflictException $e) {
|
||||
} catch (DuplicateException | ConflictException $e) {
|
||||
Authorization::skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
|
||||
'status' => 'failed',
|
||||
])));
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace Appwrite\Platform\Modules\Functions\Http\Executions;
|
||||
|
||||
use Ahc\Jwt\JWT;
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
@@ -15,9 +14,12 @@ use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Executor\Executor;
|
||||
use MaxMind\Db\Reader;
|
||||
use Utopia\Auth\Proofs\Token;
|
||||
use Utopia\Auth\Store;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
@@ -93,6 +95,8 @@ class Create extends Base
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('queueForFunctions')
|
||||
->inject('geodb')
|
||||
->inject('store')
|
||||
->inject('proofForToken')
|
||||
->inject('executor')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
@@ -115,6 +119,8 @@ class Create extends Base
|
||||
StatsUsage $queueForStatsUsage,
|
||||
Func $queueForFunctions,
|
||||
Reader $geodb,
|
||||
Store $store,
|
||||
Token $proofForToken,
|
||||
Executor $executor
|
||||
) {
|
||||
$async = \strval($async) === 'true' || \strval($async) === '1';
|
||||
@@ -155,8 +161,8 @@ class Create extends Base
|
||||
|
||||
$function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId));
|
||||
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAPIKey = User::isApp(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
|
||||
if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::FUNCTION_NOT_FOUND);
|
||||
@@ -199,7 +205,7 @@ class Create extends Base
|
||||
|
||||
foreach ($sessions as $session) {
|
||||
/** @var Utopia\Database\Document $session */
|
||||
if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too
|
||||
if ($proofForToken->verify($store->getProperty('secret', ''), $session->getAttribute('secret'))) { // Find most recent active session for user ID and JWT headers
|
||||
$current = $session;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Functions\Http\Executions;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
@@ -63,8 +63,8 @@ class Get extends Base
|
||||
) {
|
||||
$function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId));
|
||||
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAPIKey = User::isApp(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
|
||||
if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::FUNCTION_NOT_FOUND);
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Functions\Http\Executions;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Executions;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
@@ -72,8 +72,8 @@ class XList extends Base
|
||||
) {
|
||||
$function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId));
|
||||
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAPIKey = User::isApp(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
|
||||
if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::FUNCTION_NOT_FOUND);
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Tokens\Http\Tokens\Buckets\Files;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Platform\Action as UtopiaAction;
|
||||
@@ -14,8 +14,8 @@ class Action extends UtopiaAction
|
||||
{
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAPIKey = User::isApp(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Tokens\Http\Tokens\Buckets\Files;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\SDK\AuthType;
|
||||
@@ -10,6 +9,7 @@ use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Auth\Proofs\Token;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
@@ -91,7 +91,7 @@ class Create extends Action
|
||||
|
||||
$token = $dbForProject->createDocument('resourceTokens', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'secret' => Auth::tokenGenerator(128),
|
||||
'secret' => (new Token(128))->generate(),
|
||||
'resourceId' => $bucketId . ':' . $fileId,
|
||||
'resourceInternalId' => $bucket->getSequence() . ':' . $file->getSequence(),
|
||||
'resourceType' => TOKENS_RESOURCE_TYPE_FILES,
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
namespace Appwrite\Platform\Tasks;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Docker\Compose;
|
||||
use Appwrite\Docker\Env;
|
||||
use Appwrite\Utopia\View;
|
||||
use Utopia\Auth\Proofs\Password;
|
||||
use Utopia\Auth\Proofs\Token;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Platform\Action;
|
||||
@@ -149,6 +150,8 @@ class Install extends Action
|
||||
|
||||
$input = [];
|
||||
|
||||
$password = new Password();
|
||||
$token = new Token();
|
||||
foreach ($vars as $var) {
|
||||
if (!empty($var['filter']) && ($interactive !== 'Y' || !Console::isInteractive())) {
|
||||
if ($data && $var['default'] !== null) {
|
||||
@@ -157,12 +160,12 @@ class Install extends Action
|
||||
}
|
||||
|
||||
if ($var['filter'] === 'token') {
|
||||
$input[$var['name']] = Auth::tokenGenerator();
|
||||
$input[$var['name']] = $token->generate();
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($var['filter'] === 'password') {
|
||||
$input[$var['name']] = Auth::passwordGenerator();
|
||||
$input[$var['name']] = $password->generate();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Appwrite\Platform\Workers;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Exception;
|
||||
use Throwable;
|
||||
use Utopia\Audit\Audit;
|
||||
@@ -85,7 +84,7 @@ class Audits extends Action
|
||||
|
||||
$userName = $user->getAttribute('name', '');
|
||||
$userEmail = $user->getAttribute('email', '');
|
||||
$userType = $user->getAttribute('type', Auth::ACTIVITY_TYPE_USER);
|
||||
$userType = $user->getAttribute('type', ACTIVITY_TYPE_USER);
|
||||
|
||||
// Create event data
|
||||
$eventData = [
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Appwrite\Platform\Workers;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Certificates\Adapter as CertificatesAdapter;
|
||||
use Appwrite\Deletes\Identities;
|
||||
use Appwrite\Deletes\Targets;
|
||||
@@ -711,7 +710,7 @@ class Deletes extends Action
|
||||
private function deleteExpiredSessions(Document $project, callable $getProjectDB): void
|
||||
{
|
||||
$dbForProject = $getProjectDB($project);
|
||||
$duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
$duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
$expired = DateTime::addSeconds(new \DateTime(), -1 * $duration);
|
||||
|
||||
// Delete Sessions
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Database\Documents;
|
||||
|
||||
use Utopia\Auth\Proof;
|
||||
use Utopia\Auth\Proofs\Token;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Roles;
|
||||
|
||||
class User extends Document
|
||||
{
|
||||
public const ROLE_ANY = 'any';
|
||||
public const ROLE_GUESTS = 'guests';
|
||||
public const ROLE_USERS = 'users';
|
||||
public const ROLE_ADMIN = 'admin';
|
||||
public const ROLE_DEVELOPER = 'developer';
|
||||
public const ROLE_OWNER = 'owner';
|
||||
public const ROLE_APPS = 'apps';
|
||||
public const ROLE_SYSTEM = 'system';
|
||||
|
||||
public function getEmail(): ?string
|
||||
{
|
||||
return $this->getAttribute('email');
|
||||
}
|
||||
|
||||
public function getPhone(): ?string
|
||||
{
|
||||
return $this->getAttribute('phone');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all roles for a user.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getRoles(): array
|
||||
{
|
||||
$roles = [];
|
||||
|
||||
if (!$this->isPrivileged(Authorization::getRoles()) && !$this->isApp(Authorization::getRoles())) {
|
||||
if ($this->getId()) {
|
||||
$roles[] = Role::user($this->getId())->toString();
|
||||
$roles[] = Role::users()->toString();
|
||||
|
||||
$emailVerified = $this->getAttribute('emailVerification', false);
|
||||
$phoneVerified = $this->getAttribute('phoneVerification', false);
|
||||
|
||||
if ($emailVerified || $phoneVerified) {
|
||||
$roles[] = Role::user($this->getId(), Roles::DIMENSION_VERIFIED)->toString();
|
||||
$roles[] = Role::users(Roles::DIMENSION_VERIFIED)->toString();
|
||||
} else {
|
||||
$roles[] = Role::user($this->getId(), Roles::DIMENSION_UNVERIFIED)->toString();
|
||||
$roles[] = Role::users(Roles::DIMENSION_UNVERIFIED)->toString();
|
||||
}
|
||||
} else {
|
||||
return [Role::guests()->toString()];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->getAttribute('memberships', []) as $node) {
|
||||
if (!isset($node['confirm']) || !$node['confirm']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($node['$id']) && isset($node['teamId'])) {
|
||||
$roles[] = Role::team($node['teamId'])->toString();
|
||||
$roles[] = Role::member($node['$id'])->toString();
|
||||
|
||||
if (isset($node['roles'])) {
|
||||
foreach ($node['roles'] as $nodeRole) { // Set all team roles
|
||||
$roles[] = Role::team($node['teamId'], $nodeRole)->toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->getAttribute('labels', []) as $label) {
|
||||
$roles[] = 'label:' . $label;
|
||||
}
|
||||
|
||||
return $roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is anonymous.
|
||||
*
|
||||
* @param Document $this
|
||||
* @return bool
|
||||
*/
|
||||
public function isAnonymous(): bool
|
||||
{
|
||||
return is_null($this->getEmail())
|
||||
&& is_null($this->getPhone());
|
||||
}
|
||||
|
||||
/**
|
||||
* Is Privileged User?
|
||||
*
|
||||
* @param array<string> $roles
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isPrivileged(array $roles): bool
|
||||
{
|
||||
if (
|
||||
in_array(self::ROLE_OWNER, $roles) ||
|
||||
in_array(self::ROLE_DEVELOPER, $roles) ||
|
||||
in_array(self::ROLE_ADMIN, $roles)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is App User?
|
||||
*
|
||||
* @param array<string> $roles
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isApp(array $roles): bool
|
||||
{
|
||||
if (in_array(self::ROLE_APPS, $roles)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function tokenVerify(int $type = null, string $secret, Proof $proofForToken): false|Document
|
||||
{
|
||||
$tokens = $this->getAttribute('tokens', []);
|
||||
foreach ($tokens as $token) {
|
||||
if (
|
||||
$token->isSet('secret') &&
|
||||
$token->isSet('expire') &&
|
||||
$token->isSet('type') &&
|
||||
($type === null || $token->getAttribute('type') === $type) &&
|
||||
$proofForToken->verify($secret, $token->getAttribute('secret')) &&
|
||||
DateTime::formatTz($token->getAttribute('expire')) >= DateTime::formatTz(DateTime::now())
|
||||
) {
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify session and check that its not expired.
|
||||
*
|
||||
* @param array<Document> $sessions
|
||||
* @param string $secret
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function sessionVerify(string $secret, Token $proofForToken)
|
||||
{
|
||||
$sessions = $this->getAttribute('sessions', []);
|
||||
|
||||
foreach ($sessions as $session) {
|
||||
if (
|
||||
$session->isSet('secret') &&
|
||||
$session->isSet('provider') &&
|
||||
$session->isSet('expire') &&
|
||||
$proofForToken->verify($secret, $session->getAttribute('secret')) &&
|
||||
DateTime::formatTz(DateTime::format(new \DateTime($session->getAttribute('expire')))) >= DateTime::formatTz(DateTime::now())
|
||||
) {
|
||||
return $session->getId();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace Appwrite\Utopia;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Request\Filter;
|
||||
use Swoole\Http\Request as SwooleRequest;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
@@ -199,19 +199,19 @@ class Request extends UtopiaRequest
|
||||
}
|
||||
|
||||
/**
|
||||
* Get User Agent
|
||||
*
|
||||
* Method for getting User Agent. Preferring forwarded agent for privileged users; otherwise returns default.
|
||||
*
|
||||
* @param string $default
|
||||
* @return string
|
||||
*/
|
||||
* Get User Agent
|
||||
*
|
||||
* Method for getting User Agent. Preferring forwarded agent for privileged users; otherwise returns default.
|
||||
*
|
||||
* @param string $default
|
||||
* @return string
|
||||
*/
|
||||
public function getUserAgent(string $default = ''): string
|
||||
{
|
||||
$forwardedUserAgent = $this->getHeader('x-forwarded-user-agent');
|
||||
if (!empty($forwardedUserAgent)) {
|
||||
$roles = Authorization::getRoles();
|
||||
$isAppUser = Auth::isAppUser($roles);
|
||||
$isAppUser = User::isApp($roles);
|
||||
|
||||
if ($isAppUser) {
|
||||
return $forwardedUserAgent;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Appwrite\Utopia;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Utopia\Database\Documents\User as DBUser;
|
||||
use Appwrite\Utopia\Fetch\BodyMultipart;
|
||||
use Appwrite\Utopia\Response\Filter;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
@@ -146,8 +146,8 @@ use Appwrite\Utopia\Response\Model\VcsContent;
|
||||
use Appwrite\Utopia\Response\Model\Webhook;
|
||||
use Exception;
|
||||
use JsonException;
|
||||
use Swoole\Http\Response as SwooleHTTPResponse;
|
||||
// Keep last
|
||||
use Swoole\Http\Response as SwooleHTTPResponse;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Swoole\Response as SwooleResponse;
|
||||
@@ -813,8 +813,8 @@ class Response extends SwooleResponse
|
||||
|
||||
if ($rule['sensitive']) {
|
||||
$roles = Authorization::getRoles();
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
|
||||
$isAppUser = Auth::isAppUser($roles);
|
||||
$isPrivilegedUser = DBUser::isPrivileged($roles);
|
||||
$isAppUser = DBUser::isApp($roles);
|
||||
|
||||
if ((!$isPrivilegedUser && !$isAppUser) && !self::$showSensitive) {
|
||||
$data->setAttribute($key, '');
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
use Utopia\Config\Config;
|
||||
@@ -105,7 +104,7 @@ class Project extends Model
|
||||
->addRule('authDuration', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Session duration in seconds.',
|
||||
'default' => Auth::TOKEN_EXPIRATION_LOGIN_LONG,
|
||||
'default' => TOKEN_EXPIRATION_LOGIN_LONG,
|
||||
'example' => 60,
|
||||
])
|
||||
->addRule('authLimit', [
|
||||
@@ -372,7 +371,7 @@ class Project extends Model
|
||||
$auth = Config::getParam('auth', []);
|
||||
|
||||
$document->setAttribute('authLimit', $authValues['limit'] ?? 0);
|
||||
$document->setAttribute('authDuration', $authValues['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG);
|
||||
$document->setAttribute('authDuration', $authValues['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG);
|
||||
$document->setAttribute('authSessionsLimit', $authValues['maxSessions'] ?? APP_LIMIT_USER_SESSIONS_DEFAULT);
|
||||
$document->setAttribute('authPasswordHistory', $authValues['passwordHistory'] ?? 0);
|
||||
$document->setAttribute('authPasswordDictionary', $authValues['passwordDictionary'] ?? false);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Tests\E2E\Services\Projects;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Tests\Async;
|
||||
use Tests\E2E\Client;
|
||||
@@ -866,7 +865,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(Auth::TOKEN_EXPIRATION_LOGIN_LONG, $response['body']['authDuration']); // 1 Year
|
||||
$this->assertEquals(TOKEN_EXPIRATION_LOGIN_LONG, $response['body']['authDuration']); // 1 Year
|
||||
|
||||
/**
|
||||
* Test for SUCCESS
|
||||
@@ -1009,7 +1008,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG,
|
||||
'duration' => TOKEN_EXPIRATION_LOGIN_LONG,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
@@ -1022,7 +1021,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(Auth::TOKEN_EXPIRATION_LOGIN_LONG, $response['body']['authDuration']); // 1 Year
|
||||
$this->assertEquals(TOKEN_EXPIRATION_LOGIN_LONG, $response['body']['authDuration']); // 1 Year
|
||||
|
||||
return ['projectId' => $projectId];
|
||||
}
|
||||
|
||||
@@ -1,502 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Auth;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Roles;
|
||||
|
||||
class AuthTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Reset Roles
|
||||
*/
|
||||
public function tearDown(): void
|
||||
{
|
||||
Authorization::cleanRoles();
|
||||
Authorization::setRole(Role::any()->toString());
|
||||
}
|
||||
|
||||
public function testCookieName(): void
|
||||
{
|
||||
$name = 'cookie-name';
|
||||
|
||||
$this->assertEquals(Auth::setCookieName($name), $name);
|
||||
$this->assertEquals(Auth::$cookieName, $name);
|
||||
}
|
||||
|
||||
public function testEncodeDecodeSession(): void
|
||||
{
|
||||
$id = 'id';
|
||||
$secret = 'secret';
|
||||
$session = 'eyJpZCI6ImlkIiwic2VjcmV0Ijoic2VjcmV0In0=';
|
||||
|
||||
$this->assertEquals(Auth::encodeSession($id, $secret), $session);
|
||||
$this->assertEquals(Auth::decodeSession($session), ['id' => $id, 'secret' => $secret]);
|
||||
}
|
||||
|
||||
public function testHash(): void
|
||||
{
|
||||
$secret = 'secret';
|
||||
$this->assertEquals(Auth::hash($secret), '2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b');
|
||||
}
|
||||
|
||||
public function testPassword(): void
|
||||
{
|
||||
/*
|
||||
General tests, using pre-defined hashes generated by online tools
|
||||
*/
|
||||
|
||||
// Bcrypt - Version Y
|
||||
$plain = 'secret';
|
||||
$hash = '$2y$08$PDbMtV18J1KOBI9tIYabBuyUwBrtXPGhLxCy9pWP6xkldVOKLrLKy';
|
||||
$generatedHash = Auth::passwordHash($plain, 'bcrypt');
|
||||
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt'));
|
||||
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt'));
|
||||
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt'));
|
||||
|
||||
// Bcrypt - Version A
|
||||
$plain = 'test123';
|
||||
$hash = '$2a$12$3f2ZaARQ1AmhtQWx2nmQpuXcWfTj1YV2/Hl54e8uKxIzJe3IfwLiu';
|
||||
$generatedHash = Auth::passwordHash($plain, 'bcrypt');
|
||||
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt'));
|
||||
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt'));
|
||||
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt'));
|
||||
|
||||
// Bcrypt - Cost 5
|
||||
$plain = 'hello-world';
|
||||
$hash = '$2a$05$IjrtSz6SN7UJ6Sh3l.b5jODEvEG2LMJTPAHIaLWRvlWx7if3VMkFO';
|
||||
$generatedHash = Auth::passwordHash($plain, 'bcrypt');
|
||||
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt'));
|
||||
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt'));
|
||||
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt'));
|
||||
|
||||
// Bcrypt - Cost 15
|
||||
$plain = 'super-secret-password';
|
||||
$hash = '$2a$15$DS0ZzbsFZYumH/E4Qj5oeOHnBcM3nCCsCA2m4Goigat/0iMVQC4Na';
|
||||
$generatedHash = Auth::passwordHash($plain, 'bcrypt');
|
||||
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt'));
|
||||
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt'));
|
||||
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt'));
|
||||
|
||||
// MD5 - Short
|
||||
$plain = 'appwrite';
|
||||
$hash = '144fa7eaa4904e8ee120651997f70dcc';
|
||||
$generatedHash = Auth::passwordHash($plain, 'md5');
|
||||
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'md5'));
|
||||
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'md5'));
|
||||
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'md5'));
|
||||
|
||||
// MD5 - Long
|
||||
$plain = 'AppwriteIsAwesomeBackendAsAServiceThatIsAlsoOpenSourced';
|
||||
$hash = '8410e96cf7ac64e0b84c3f8517a82616';
|
||||
$generatedHash = Auth::passwordHash($plain, 'md5');
|
||||
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'md5'));
|
||||
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'md5'));
|
||||
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'md5'));
|
||||
|
||||
// PHPass
|
||||
$plain = 'pass123';
|
||||
$hash = '$P$BVKPmJBZuLch27D4oiMRTEykGLQ9tX0';
|
||||
$generatedHash = Auth::passwordHash($plain, 'phpass');
|
||||
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'phpass'));
|
||||
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'phpass'));
|
||||
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'phpass'));
|
||||
|
||||
// SHA
|
||||
$plain = 'developersAreAwesome!';
|
||||
$hash = '2455118438cb125354b89bb5888346e9bd23355462c40df393fab514bf2220b5a08e4e2d7b85d7327595a450d0ac965cc6661152a46a157c66d681bed20a4735';
|
||||
$generatedHash = Auth::passwordHash($plain, 'sha');
|
||||
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'sha'));
|
||||
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'sha'));
|
||||
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'sha'));
|
||||
|
||||
// Argon2
|
||||
$plain = 'safe-argon-password';
|
||||
$hash = '$argon2id$v=19$m=2048,t=3,p=4$MWc5NWRmc2QxZzU2$41mp7rSgBZ49YxLbbxIac7aRaxfp5/e1G45ckwnK0g8';
|
||||
$generatedHash = Auth::passwordHash($plain, 'argon2');
|
||||
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'argon2'));
|
||||
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'argon2'));
|
||||
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'argon2'));
|
||||
|
||||
// Scrypt
|
||||
$plain = 'some-scrypt-password';
|
||||
$hash = 'b448ad7ba88b653b5b56b8053a06806724932d0751988bc9cd0ef7ff059e8ba8a020e1913b7069a650d3f99a1559aba0221f2c277826919513a054e76e339028';
|
||||
$generatedHash = Auth::passwordHash($plain, 'scrypt', [ 'salt' => 'some-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 12, 'costParallel' => 2]);
|
||||
|
||||
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'scrypt', [ 'salt' => 'some-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 12, 'costParallel' => 2]));
|
||||
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'scrypt', [ 'salt' => 'some-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 12, 'costParallel' => 2]));
|
||||
$this->assertEquals(false, Auth::passwordVerify($plain, $hash, 'scrypt', [ 'salt' => 'some-wrong-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 12, 'costParallel' => 2]));
|
||||
$this->assertEquals(false, Auth::passwordVerify($plain, $hash, 'scrypt', [ 'salt' => 'some-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 10, 'costParallel' => 2]));
|
||||
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'scrypt', [ 'salt' => 'some-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 12, 'costParallel' => 2]));
|
||||
|
||||
// ScryptModified tested are in provider-specific tests below
|
||||
|
||||
/*
|
||||
Provider-specific tests, ensuring functionality of specific use-cases
|
||||
*/
|
||||
|
||||
// Provider #1 (Database)
|
||||
$plain = 'example-password';
|
||||
$hash = '$2a$10$3bIGRWUes86CICsuchGLj.e.BqdCdg2/1Ud9LvBhJr0j7Dze8PBdS';
|
||||
$generatedHash = Auth::passwordHash($plain, 'bcrypt');
|
||||
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt'));
|
||||
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt'));
|
||||
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt'));
|
||||
|
||||
// Provider #2 (Blog)
|
||||
$plain = 'your-password';
|
||||
$hash = '$P$BkiNDJTpAWXtpaMhEUhUdrv7M0I1g6.';
|
||||
$generatedHash = Auth::passwordHash($plain, 'phpass');
|
||||
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'phpass'));
|
||||
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'phpass'));
|
||||
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'phpass'));
|
||||
|
||||
// Provider #2 (Google)
|
||||
$plain = 'users-password';
|
||||
$hash = 'EPKgfALpS9Tvgr/y1ki7ubY4AEGJeWL3teakrnmOacN4XGiyD00lkzEHgqCQ71wGxoi/zb7Y9a4orOtvMV3/Jw==';
|
||||
$salt = '56dFqW+kswqktw==';
|
||||
$saltSeparator = 'Bw==';
|
||||
$signerKey = 'XyEKE9RcTDeLEsL/RjwPDBv/RqDl8fb3gpYEOQaPihbxf1ZAtSOHCjuAAa7Q3oHpCYhXSN9tizHgVOwn6krflQ==';
|
||||
|
||||
$options = [ 'salt' => $salt, 'saltSeparator' => $saltSeparator, 'signerKey' => $signerKey ];
|
||||
$generatedHash = Auth::passwordHash($plain, 'scryptMod', $options);
|
||||
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'scryptMod', $options));
|
||||
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'scryptMod', $options));
|
||||
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'scryptMod', $options));
|
||||
}
|
||||
|
||||
public function testUnknownAlgo()
|
||||
{
|
||||
$this->expectExceptionMessage('Hashing algorithm \'md8\' is not supported.');
|
||||
|
||||
// Bcrypt - Cost 5
|
||||
$plain = 'whatIsMd8?!?';
|
||||
$generatedHash = Auth::passwordHash($plain, 'md8');
|
||||
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'md8'));
|
||||
}
|
||||
|
||||
public function testPasswordGenerator(): void
|
||||
{
|
||||
$this->assertEquals(\mb_strlen(Auth::passwordGenerator()), 40);
|
||||
$this->assertEquals(\mb_strlen(Auth::passwordGenerator(5)), 10);
|
||||
}
|
||||
|
||||
public function testTokenGenerator(): void
|
||||
{
|
||||
$this->assertEquals(\strlen(Auth::tokenGenerator()), 256);
|
||||
$this->assertEquals(\strlen(Auth::tokenGenerator(5)), 5);
|
||||
}
|
||||
|
||||
public function testCodeGenerator(): void
|
||||
{
|
||||
$this->assertEquals(6, \strlen(Auth::codeGenerator()));
|
||||
$this->assertEquals(\mb_strlen(Auth::codeGenerator(256)), 256);
|
||||
$this->assertEquals(\mb_strlen(Auth::codeGenerator(10)), 10);
|
||||
$this->assertTrue(is_numeric(Auth::codeGenerator(5)));
|
||||
}
|
||||
|
||||
public function testSessionVerify(): void
|
||||
{
|
||||
$expireTime1 = 60 * 60 * 24;
|
||||
|
||||
$secret = 'secret1';
|
||||
$hash = Auth::hash($secret);
|
||||
$tokens1 = [
|
||||
new Document([
|
||||
'$id' => ID::custom('token1'),
|
||||
'secret' => $hash,
|
||||
'provider' => Auth::SESSION_PROVIDER_EMAIL,
|
||||
'providerUid' => 'test@example.com',
|
||||
'expire' => DateTime::addSeconds(new \DateTime(), $expireTime1),
|
||||
]),
|
||||
new Document([
|
||||
'$id' => ID::custom('token2'),
|
||||
'secret' => 'secret2',
|
||||
'provider' => Auth::SESSION_PROVIDER_EMAIL,
|
||||
'providerUid' => 'test@example.com',
|
||||
'expire' => DateTime::addSeconds(new \DateTime(), $expireTime1),
|
||||
]),
|
||||
];
|
||||
|
||||
$expireTime2 = -60 * 60 * 24;
|
||||
|
||||
$tokens2 = [
|
||||
new Document([ // Correct secret and type time, wrong expire time
|
||||
'$id' => ID::custom('token1'),
|
||||
'secret' => $hash,
|
||||
'provider' => Auth::SESSION_PROVIDER_EMAIL,
|
||||
'providerUid' => 'test@example.com',
|
||||
'expire' => DateTime::addSeconds(new \DateTime(), $expireTime2),
|
||||
]),
|
||||
new Document([
|
||||
'$id' => ID::custom('token2'),
|
||||
'secret' => 'secret2',
|
||||
'provider' => Auth::SESSION_PROVIDER_EMAIL,
|
||||
'providerUid' => 'test@example.com',
|
||||
'expire' => DateTime::addSeconds(new \DateTime(), $expireTime2),
|
||||
]),
|
||||
];
|
||||
|
||||
$this->assertEquals(Auth::sessionVerify($tokens1, $secret), 'token1');
|
||||
$this->assertEquals(Auth::sessionVerify($tokens1, 'false-secret'), false);
|
||||
$this->assertEquals(Auth::sessionVerify($tokens2, $secret), false);
|
||||
$this->assertEquals(Auth::sessionVerify($tokens2, 'false-secret'), false);
|
||||
}
|
||||
|
||||
public function testTokenVerify(): void
|
||||
{
|
||||
$secret = 'secret1';
|
||||
$hash = Auth::hash($secret);
|
||||
$tokens1 = [
|
||||
new Document([
|
||||
'$id' => ID::custom('token1'),
|
||||
'type' => Auth::TOKEN_TYPE_RECOVERY,
|
||||
'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), 60 * 60 * 24)),
|
||||
'secret' => $hash,
|
||||
]),
|
||||
new Document([
|
||||
'$id' => ID::custom('token2'),
|
||||
'type' => Auth::TOKEN_TYPE_RECOVERY,
|
||||
'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)),
|
||||
'secret' => 'secret2',
|
||||
]),
|
||||
];
|
||||
|
||||
$tokens2 = [
|
||||
new Document([ // Correct secret and type time, wrong expire time
|
||||
'$id' => ID::custom('token1'),
|
||||
'type' => Auth::TOKEN_TYPE_RECOVERY,
|
||||
'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)),
|
||||
'secret' => $hash,
|
||||
]),
|
||||
new Document([
|
||||
'$id' => ID::custom('token2'),
|
||||
'type' => Auth::TOKEN_TYPE_RECOVERY,
|
||||
'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)),
|
||||
'secret' => 'secret2',
|
||||
]),
|
||||
];
|
||||
|
||||
$tokens3 = [ // Correct secret and expire time, wrong type
|
||||
new Document([
|
||||
'$id' => ID::custom('token1'),
|
||||
'type' => Auth::TOKEN_TYPE_INVITE,
|
||||
'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), 60 * 60 * 24)),
|
||||
'secret' => $hash,
|
||||
]),
|
||||
new Document([
|
||||
'$id' => ID::custom('token2'),
|
||||
'type' => Auth::TOKEN_TYPE_RECOVERY,
|
||||
'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)),
|
||||
'secret' => 'secret2',
|
||||
]),
|
||||
];
|
||||
|
||||
$this->assertEquals(Auth::tokenVerify($tokens1, Auth::TOKEN_TYPE_RECOVERY, $secret), $tokens1[0]);
|
||||
$this->assertEquals(Auth::tokenVerify($tokens1, null, $secret), $tokens1[0]);
|
||||
$this->assertEquals(Auth::tokenVerify($tokens1, Auth::TOKEN_TYPE_RECOVERY, 'false-secret'), false);
|
||||
$this->assertEquals(Auth::tokenVerify($tokens2, Auth::TOKEN_TYPE_RECOVERY, $secret), false);
|
||||
$this->assertEquals(Auth::tokenVerify($tokens2, Auth::TOKEN_TYPE_RECOVERY, 'false-secret'), false);
|
||||
$this->assertEquals(Auth::tokenVerify($tokens3, Auth::TOKEN_TYPE_RECOVERY, $secret), false);
|
||||
$this->assertEquals(Auth::tokenVerify($tokens3, Auth::TOKEN_TYPE_RECOVERY, 'false-secret'), false);
|
||||
}
|
||||
|
||||
public function testIsPrivilegedUser(): void
|
||||
{
|
||||
$this->assertEquals(false, Auth::isPrivilegedUser([]));
|
||||
$this->assertEquals(false, Auth::isPrivilegedUser([Role::guests()->toString()]));
|
||||
$this->assertEquals(false, Auth::isPrivilegedUser([Role::users()->toString()]));
|
||||
$this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_ADMIN]));
|
||||
$this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_DEVELOPER]));
|
||||
$this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_OWNER]));
|
||||
$this->assertEquals(false, Auth::isPrivilegedUser([Auth::USER_ROLE_APPS]));
|
||||
$this->assertEquals(false, Auth::isPrivilegedUser([Auth::USER_ROLE_SYSTEM]));
|
||||
|
||||
$this->assertEquals(false, Auth::isPrivilegedUser([Auth::USER_ROLE_APPS, Auth::USER_ROLE_APPS]));
|
||||
$this->assertEquals(false, Auth::isPrivilegedUser([Auth::USER_ROLE_APPS, Role::guests()->toString()]));
|
||||
$this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_OWNER, Role::guests()->toString()]));
|
||||
$this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_OWNER, Auth::USER_ROLE_ADMIN, Auth::USER_ROLE_DEVELOPER]));
|
||||
}
|
||||
|
||||
public function testIsAppUser(): void
|
||||
{
|
||||
$this->assertEquals(false, Auth::isAppUser([]));
|
||||
$this->assertEquals(false, Auth::isAppUser([Role::guests()->toString()]));
|
||||
$this->assertEquals(false, Auth::isAppUser([Role::users()->toString()]));
|
||||
$this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_ADMIN]));
|
||||
$this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_DEVELOPER]));
|
||||
$this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_OWNER]));
|
||||
$this->assertEquals(true, Auth::isAppUser([Auth::USER_ROLE_APPS]));
|
||||
$this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_SYSTEM]));
|
||||
|
||||
$this->assertEquals(true, Auth::isAppUser([Auth::USER_ROLE_APPS, Auth::USER_ROLE_APPS]));
|
||||
$this->assertEquals(true, Auth::isAppUser([Auth::USER_ROLE_APPS, Role::guests()->toString()]));
|
||||
$this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_OWNER, Role::guests()->toString()]));
|
||||
$this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_OWNER, Auth::USER_ROLE_ADMIN, Auth::USER_ROLE_DEVELOPER]));
|
||||
}
|
||||
|
||||
public function testGuestRoles(): void
|
||||
{
|
||||
$user = new Document([
|
||||
'$id' => ''
|
||||
]);
|
||||
|
||||
$roles = Auth::getRoles($user);
|
||||
$this->assertCount(1, $roles);
|
||||
$this->assertContains(Role::guests()->toString(), $roles);
|
||||
}
|
||||
|
||||
public function testUserRoles(): void
|
||||
{
|
||||
$user = new Document([
|
||||
'$id' => ID::custom('123'),
|
||||
'labels' => [
|
||||
'vip',
|
||||
'admin'
|
||||
],
|
||||
'emailVerification' => true,
|
||||
'phoneVerification' => true,
|
||||
'memberships' => [
|
||||
[
|
||||
'$id' => ID::custom('456'),
|
||||
'teamId' => ID::custom('abc'),
|
||||
'confirm' => true,
|
||||
'roles' => [
|
||||
'administrator',
|
||||
'moderator'
|
||||
]
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('abc'),
|
||||
'teamId' => ID::custom('def'),
|
||||
'confirm' => true,
|
||||
'roles' => [
|
||||
'guest'
|
||||
]
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$roles = Auth::getRoles($user);
|
||||
|
||||
$this->assertCount(13, $roles);
|
||||
$this->assertContains(Role::users()->toString(), $roles);
|
||||
$this->assertContains(Role::user(ID::custom('123'))->toString(), $roles);
|
||||
$this->assertContains(Role::users(Roles::DIMENSION_VERIFIED)->toString(), $roles);
|
||||
$this->assertContains(Role::user(ID::custom('123'), Roles::DIMENSION_VERIFIED)->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('abc'))->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('abc'), 'administrator')->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('abc'), 'moderator')->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('def'))->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('def'), 'guest')->toString(), $roles);
|
||||
$this->assertContains(Role::member(ID::custom('456'))->toString(), $roles);
|
||||
$this->assertContains(Role::member(ID::custom('abc'))->toString(), $roles);
|
||||
$this->assertContains('label:vip', $roles);
|
||||
$this->assertContains('label:admin', $roles);
|
||||
|
||||
// Disable all verification
|
||||
$user['emailVerification'] = false;
|
||||
$user['phoneVerification'] = false;
|
||||
|
||||
$roles = Auth::getRoles($user);
|
||||
$this->assertContains(Role::users(Roles::DIMENSION_UNVERIFIED)->toString(), $roles);
|
||||
$this->assertContains(Role::user(ID::custom('123'), Roles::DIMENSION_UNVERIFIED)->toString(), $roles);
|
||||
|
||||
// Enable single verification type
|
||||
$user['emailVerification'] = true;
|
||||
|
||||
$roles = Auth::getRoles($user);
|
||||
$this->assertContains(Role::users(Roles::DIMENSION_VERIFIED)->toString(), $roles);
|
||||
$this->assertContains(Role::user(ID::custom('123'), Roles::DIMENSION_VERIFIED)->toString(), $roles);
|
||||
}
|
||||
|
||||
public function testPrivilegedUserRoles(): void
|
||||
{
|
||||
Authorization::setRole(Auth::USER_ROLE_OWNER);
|
||||
$user = new Document([
|
||||
'$id' => ID::custom('123'),
|
||||
'emailVerification' => true,
|
||||
'phoneVerification' => true,
|
||||
'memberships' => [
|
||||
[
|
||||
'$id' => ID::custom('def'),
|
||||
'teamId' => ID::custom('abc'),
|
||||
'confirm' => true,
|
||||
'roles' => [
|
||||
'administrator',
|
||||
'moderator'
|
||||
]
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('abc'),
|
||||
'teamId' => ID::custom('def'),
|
||||
'confirm' => true,
|
||||
'roles' => [
|
||||
'guest'
|
||||
]
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$roles = Auth::getRoles($user);
|
||||
|
||||
$this->assertCount(7, $roles);
|
||||
$this->assertNotContains(Role::users()->toString(), $roles);
|
||||
$this->assertNotContains(Role::user(ID::custom('123'))->toString(), $roles);
|
||||
$this->assertNotContains(Role::users(Roles::DIMENSION_VERIFIED)->toString(), $roles);
|
||||
$this->assertNotContains(Role::user(ID::custom('123'), Roles::DIMENSION_VERIFIED)->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('abc'))->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('abc'), 'administrator')->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('abc'), 'moderator')->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('def'))->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('def'), 'guest')->toString(), $roles);
|
||||
$this->assertContains(Role::member(ID::custom('def'))->toString(), $roles);
|
||||
$this->assertContains(Role::member(ID::custom('abc'))->toString(), $roles);
|
||||
}
|
||||
|
||||
public function testAppUserRoles(): void
|
||||
{
|
||||
Authorization::setRole(Auth::USER_ROLE_APPS);
|
||||
$user = new Document([
|
||||
'$id' => ID::custom('123'),
|
||||
'memberships' => [
|
||||
[
|
||||
'$id' => ID::custom('def'),
|
||||
'teamId' => ID::custom('abc'),
|
||||
'confirm' => true,
|
||||
'roles' => [
|
||||
'administrator',
|
||||
'moderator'
|
||||
]
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('abc'),
|
||||
'teamId' => ID::custom('def'),
|
||||
'confirm' => true,
|
||||
'roles' => [
|
||||
'guest'
|
||||
]
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$roles = Auth::getRoles($user);
|
||||
|
||||
$this->assertCount(7, $roles);
|
||||
$this->assertNotContains(Role::users()->toString(), $roles);
|
||||
$this->assertNotContains(Role::user(ID::custom('123'))->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('abc'))->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('abc'), 'administrator')->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('abc'), 'moderator')->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('def'))->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('def'), 'guest')->toString(), $roles);
|
||||
$this->assertContains(Role::member(ID::custom('def'))->toString(), $roles);
|
||||
$this->assertContains(Role::member(ID::custom('abc'))->toString(), $roles);
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@
|
||||
namespace Tests\Unit\Auth;
|
||||
|
||||
use Ahc\Jwt\JWT;
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Auth\Key;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Document;
|
||||
@@ -21,7 +21,7 @@ class KeyTest extends TestCase
|
||||
'collections.read',
|
||||
'documents.read',
|
||||
];
|
||||
$roleScopes = Config::getParam('roles', [])[Auth::USER_ROLE_APPS]['scopes'];
|
||||
$roleScopes = Config::getParam('roles', [])[User::ROLE_APPS]['scopes'];
|
||||
|
||||
$key = static::generateKey($projectId, $usage, $scopes);
|
||||
$project = new Document(['$id' => $projectId,]);
|
||||
@@ -29,7 +29,7 @@ class KeyTest extends TestCase
|
||||
|
||||
$this->assertEquals($projectId, $decoded->getProjectId());
|
||||
$this->assertEquals(API_KEY_DYNAMIC, $decoded->getType());
|
||||
$this->assertEquals(Auth::USER_ROLE_APPS, $decoded->getRole());
|
||||
$this->assertEquals(User::ROLE_APPS, $decoded->getRole());
|
||||
$this->assertEquals(\array_merge($scopes, $roleScopes), $decoded->getScopes());
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
namespace Tests\Unit\Messaging;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Messaging\Adapter\Realtime;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
|
||||
@@ -50,7 +49,7 @@ class MessagingChannelsTest extends TestCase
|
||||
*/
|
||||
for ($i = 0; $i < $this->connectionsPerChannel; $i++) {
|
||||
foreach ($this->allChannels as $index => $channel) {
|
||||
$user = new Document([
|
||||
$user = new User([
|
||||
'$id' => ID::custom('user' . $this->connectionsCount),
|
||||
'memberships' => [
|
||||
[
|
||||
@@ -59,14 +58,14 @@ class MessagingChannelsTest extends TestCase
|
||||
'confirm' => true,
|
||||
'roles' => [
|
||||
empty($index % 2)
|
||||
? Auth::USER_ROLE_ADMIN
|
||||
? User::ROLE_ADMIN
|
||||
: 'member',
|
||||
]
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$roles = Auth::getRoles($user);
|
||||
$roles = $user->getRoles();
|
||||
|
||||
$parsedChannels = Realtime::convertChannels([0 => $channel], $user->getId());
|
||||
|
||||
@@ -86,11 +85,11 @@ class MessagingChannelsTest extends TestCase
|
||||
*/
|
||||
for ($i = 0; $i < $this->connectionsPerChannel; $i++) {
|
||||
foreach ($this->allChannels as $index => $channel) {
|
||||
$user = new Document([
|
||||
$user = new User([
|
||||
'$id' => ''
|
||||
]);
|
||||
|
||||
$roles = Auth::getRoles($user);
|
||||
$roles = $user->getRoles();
|
||||
|
||||
$parsedChannels = Realtime::convertChannels([0 => $channel], $user->getId());
|
||||
|
||||
@@ -294,7 +293,7 @@ class MessagingChannelsTest extends TestCase
|
||||
}
|
||||
|
||||
$role = empty($index % 2)
|
||||
? Auth::USER_ROLE_ADMIN
|
||||
? User::ROLE_ADMIN
|
||||
: 'member';
|
||||
|
||||
$permissions = [
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Utopia\Database\Documents;
|
||||
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Utopia\Auth\Proofs\Token;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Roles;
|
||||
|
||||
class UserTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Reset Roles
|
||||
*/
|
||||
public function tearDown(): void
|
||||
{
|
||||
Authorization::cleanRoles();
|
||||
Authorization::setRole(Role::any()->toString());
|
||||
}
|
||||
|
||||
public function testSessionVerify(): void
|
||||
{
|
||||
$proofForToken = new Token();
|
||||
$expireTime1 = 60 * 60 * 24;
|
||||
|
||||
$secret = 'secret1';
|
||||
$hash = $proofForToken->hash($secret);
|
||||
$tokens1 = [
|
||||
new Document([
|
||||
'$id' => ID::custom('token1'),
|
||||
'secret' => $hash,
|
||||
'provider' => SESSION_PROVIDER_EMAIL,
|
||||
'providerUid' => 'test@example.com',
|
||||
'expire' => DateTime::addSeconds(new \DateTime(), $expireTime1),
|
||||
]),
|
||||
new Document([
|
||||
'$id' => ID::custom('token2'),
|
||||
'secret' => 'secret2',
|
||||
'provider' => SESSION_PROVIDER_EMAIL,
|
||||
'providerUid' => 'test@example.com',
|
||||
'expire' => DateTime::addSeconds(new \DateTime(), $expireTime1),
|
||||
]),
|
||||
];
|
||||
|
||||
$expireTime2 = -60 * 60 * 24;
|
||||
|
||||
$tokens2 = [
|
||||
new Document([ // Correct secret and type time, wrong expire time
|
||||
'$id' => ID::custom('token1'),
|
||||
'secret' => $hash,
|
||||
'provider' => SESSION_PROVIDER_EMAIL,
|
||||
'providerUid' => 'test@example.com',
|
||||
'expire' => DateTime::addSeconds(new \DateTime(), $expireTime2),
|
||||
]),
|
||||
new Document([
|
||||
'$id' => ID::custom('token2'),
|
||||
'secret' => 'secret2',
|
||||
'provider' => SESSION_PROVIDER_EMAIL,
|
||||
'providerUid' => 'test@example.com',
|
||||
'expire' => DateTime::addSeconds(new \DateTime(), $expireTime2),
|
||||
]),
|
||||
];
|
||||
|
||||
$user1 = new User([
|
||||
'$id' => ID::custom('user1'),
|
||||
'sessions' => $tokens1,
|
||||
|
||||
]);
|
||||
|
||||
$user2 = new User([
|
||||
'$id' => ID::custom('user2'),
|
||||
'sessions' => $tokens2,
|
||||
]);
|
||||
|
||||
$this->assertEquals('token1', $user1->sessionVerify($secret, $proofForToken));
|
||||
$this->assertEquals($user1->sessionVerify('false-secret', $proofForToken), false);
|
||||
$this->assertEquals($user2->sessionVerify($secret, $proofForToken), false);
|
||||
$this->assertEquals($user2->sessionVerify('false-secret', $proofForToken), false);
|
||||
}
|
||||
|
||||
public function testTokenVerify(): void
|
||||
{
|
||||
$proofForToken = new Token();
|
||||
$secret = 'secret1';
|
||||
$hash = $proofForToken->hash($secret);
|
||||
$tokens1 = [
|
||||
new Document([
|
||||
'$id' => ID::custom('token1'),
|
||||
'type' => TOKEN_TYPE_RECOVERY,
|
||||
'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), 60 * 60 * 24)),
|
||||
'secret' => $hash,
|
||||
]),
|
||||
new Document([
|
||||
'$id' => ID::custom('token2'),
|
||||
'type' => TOKEN_TYPE_RECOVERY,
|
||||
'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)),
|
||||
'secret' => 'secret2',
|
||||
]),
|
||||
];
|
||||
|
||||
$tokens2 = [
|
||||
new Document([ // Correct secret and type time, wrong expire time
|
||||
'$id' => ID::custom('token1'),
|
||||
'type' => TOKEN_TYPE_RECOVERY,
|
||||
'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)),
|
||||
'secret' => $hash,
|
||||
]),
|
||||
new Document([
|
||||
'$id' => ID::custom('token2'),
|
||||
'type' => TOKEN_TYPE_RECOVERY,
|
||||
'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)),
|
||||
'secret' => 'secret2',
|
||||
]),
|
||||
];
|
||||
|
||||
$tokens3 = [ // Correct secret and expire time, wrong type
|
||||
new Document([
|
||||
'$id' => ID::custom('token1'),
|
||||
'type' => TOKEN_TYPE_INVITE,
|
||||
'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), 60 * 60 * 24)),
|
||||
'secret' => $hash,
|
||||
]),
|
||||
new Document([
|
||||
'$id' => ID::custom('token2'),
|
||||
'type' => TOKEN_TYPE_RECOVERY,
|
||||
'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)),
|
||||
'secret' => 'secret2',
|
||||
]),
|
||||
];
|
||||
|
||||
$user1 = new User([
|
||||
'$id' => ID::custom('user1'),
|
||||
'tokens' => $tokens1,
|
||||
]);
|
||||
|
||||
$user2 = new User([
|
||||
'$id' => ID::custom('user2'),
|
||||
'tokens' => $tokens2,
|
||||
]);
|
||||
|
||||
$user3 = new User([
|
||||
'$id' => ID::custom('user3'),
|
||||
'tokens' => $tokens3,
|
||||
]);
|
||||
|
||||
$this->assertEquals($user1->tokenVerify(TOKEN_TYPE_RECOVERY, $secret, $proofForToken), $tokens1[0]);
|
||||
$this->assertEquals($user1->tokenVerify(null, $secret, $proofForToken), $tokens1[0]);
|
||||
$this->assertEquals($user1->tokenVerify(TOKEN_TYPE_RECOVERY, 'false-secret', $proofForToken), false);
|
||||
$this->assertEquals($user2->tokenVerify(TOKEN_TYPE_RECOVERY, $secret, $proofForToken), false);
|
||||
$this->assertEquals($user2->tokenVerify(TOKEN_TYPE_RECOVERY, 'false-secret', $proofForToken), false);
|
||||
$this->assertEquals($user3->tokenVerify(TOKEN_TYPE_RECOVERY, $secret, $proofForToken), false);
|
||||
$this->assertEquals($user3->tokenVerify(TOKEN_TYPE_RECOVERY, 'false-secret', $proofForToken), false);
|
||||
}
|
||||
|
||||
public function testIsPrivilegedUser(): void
|
||||
{
|
||||
$this->assertEquals(false, User::isPrivileged([]));
|
||||
$this->assertEquals(false, User::isPrivileged([Role::guests()->toString()]));
|
||||
$this->assertEquals(false, User::isPrivileged([Role::users()->toString()]));
|
||||
$this->assertEquals(true, User::isPrivileged([User::ROLE_ADMIN]));
|
||||
$this->assertEquals(true, User::isPrivileged([User::ROLE_DEVELOPER]));
|
||||
$this->assertEquals(true, User::isPrivileged([User::ROLE_OWNER]));
|
||||
$this->assertEquals(false, User::isPrivileged([User::ROLE_APPS]));
|
||||
$this->assertEquals(false, User::isPrivileged([User::ROLE_SYSTEM]));
|
||||
|
||||
$this->assertEquals(false, User::isPrivileged([User::ROLE_APPS, User::ROLE_APPS]));
|
||||
$this->assertEquals(false, User::isPrivileged([User::ROLE_APPS, Role::guests()->toString()]));
|
||||
$this->assertEquals(true, User::isPrivileged([User::ROLE_OWNER, Role::guests()->toString()]));
|
||||
$this->assertEquals(true, User::isPrivileged([User::ROLE_OWNER, User::ROLE_ADMIN, User::ROLE_DEVELOPER]));
|
||||
}
|
||||
|
||||
public function testIsAppUser(): void
|
||||
{
|
||||
$this->assertEquals(false, User::isApp([]));
|
||||
$this->assertEquals(false, User::isApp([Role::guests()->toString()]));
|
||||
$this->assertEquals(false, User::isApp([Role::users()->toString()]));
|
||||
$this->assertEquals(false, User::isApp([User::ROLE_ADMIN]));
|
||||
$this->assertEquals(false, User::isApp([User::ROLE_DEVELOPER]));
|
||||
$this->assertEquals(false, User::isApp([User::ROLE_OWNER]));
|
||||
$this->assertEquals(true, User::isApp([User::ROLE_APPS]));
|
||||
$this->assertEquals(false, User::isApp([User::ROLE_SYSTEM]));
|
||||
|
||||
$this->assertEquals(true, User::isApp([User::ROLE_APPS, User::ROLE_APPS]));
|
||||
$this->assertEquals(true, User::isApp([User::ROLE_APPS, Role::guests()->toString()]));
|
||||
$this->assertEquals(false, User::isApp([User::ROLE_OWNER, Role::guests()->toString()]));
|
||||
$this->assertEquals(false, User::isApp([User::ROLE_OWNER, User::ROLE_ADMIN, User::ROLE_DEVELOPER]));
|
||||
}
|
||||
|
||||
public function testGuestRoles(): void
|
||||
{
|
||||
$user = new User([
|
||||
'$id' => ''
|
||||
]);
|
||||
|
||||
$roles = $user->getRoles();
|
||||
$this->assertCount(1, $roles);
|
||||
$this->assertContains(Role::guests()->toString(), $roles);
|
||||
}
|
||||
|
||||
public function testUserRoles(): void
|
||||
{
|
||||
$user = new User([
|
||||
'$id' => ID::custom('123'),
|
||||
'labels' => [
|
||||
'vip',
|
||||
'admin'
|
||||
],
|
||||
'emailVerification' => true,
|
||||
'phoneVerification' => true,
|
||||
'memberships' => [
|
||||
[
|
||||
'$id' => ID::custom('456'),
|
||||
'teamId' => ID::custom('abc'),
|
||||
'confirm' => true,
|
||||
'roles' => [
|
||||
'administrator',
|
||||
'moderator'
|
||||
]
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('abc'),
|
||||
'teamId' => ID::custom('def'),
|
||||
'confirm' => true,
|
||||
'roles' => [
|
||||
'guest'
|
||||
]
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$roles = $user->getRoles();
|
||||
|
||||
$this->assertCount(13, $roles);
|
||||
$this->assertContains(Role::users()->toString(), $roles);
|
||||
$this->assertContains(Role::user(ID::custom('123'))->toString(), $roles);
|
||||
$this->assertContains(Role::users(Roles::DIMENSION_VERIFIED)->toString(), $roles);
|
||||
$this->assertContains(Role::user(ID::custom('123'), Roles::DIMENSION_VERIFIED)->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('abc'))->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('abc'), 'administrator')->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('abc'), 'moderator')->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('def'))->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('def'), 'guest')->toString(), $roles);
|
||||
$this->assertContains(Role::member(ID::custom('456'))->toString(), $roles);
|
||||
$this->assertContains(Role::member(ID::custom('abc'))->toString(), $roles);
|
||||
$this->assertContains('label:vip', $roles);
|
||||
$this->assertContains('label:admin', $roles);
|
||||
|
||||
// Disable all verification
|
||||
$user['emailVerification'] = false;
|
||||
$user['phoneVerification'] = false;
|
||||
|
||||
$roles = $user->getRoles();
|
||||
$this->assertContains(Role::users(Roles::DIMENSION_UNVERIFIED)->toString(), $roles);
|
||||
$this->assertContains(Role::user(ID::custom('123'), Roles::DIMENSION_UNVERIFIED)->toString(), $roles);
|
||||
|
||||
// Enable single verification type
|
||||
$user['emailVerification'] = true;
|
||||
|
||||
$roles = $user->getRoles();
|
||||
$this->assertContains(Role::users(Roles::DIMENSION_VERIFIED)->toString(), $roles);
|
||||
$this->assertContains(Role::user(ID::custom('123'), Roles::DIMENSION_VERIFIED)->toString(), $roles);
|
||||
}
|
||||
|
||||
public function testPrivilegedUserRoles(): void
|
||||
{
|
||||
Authorization::setRole(User::ROLE_OWNER);
|
||||
$user = new User([
|
||||
'$id' => ID::custom('123'),
|
||||
'emailVerification' => true,
|
||||
'phoneVerification' => true,
|
||||
'memberships' => [
|
||||
[
|
||||
'$id' => ID::custom('def'),
|
||||
'teamId' => ID::custom('abc'),
|
||||
'confirm' => true,
|
||||
'roles' => [
|
||||
'administrator',
|
||||
'moderator'
|
||||
]
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('abc'),
|
||||
'teamId' => ID::custom('def'),
|
||||
'confirm' => true,
|
||||
'roles' => [
|
||||
'guest'
|
||||
]
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$roles = $user->getRoles();
|
||||
|
||||
$this->assertCount(7, $roles);
|
||||
$this->assertNotContains(Role::users()->toString(), $roles);
|
||||
$this->assertNotContains(Role::user(ID::custom('123'))->toString(), $roles);
|
||||
$this->assertNotContains(Role::users(Roles::DIMENSION_VERIFIED)->toString(), $roles);
|
||||
$this->assertNotContains(Role::user(ID::custom('123'), Roles::DIMENSION_VERIFIED)->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('abc'))->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('abc'), 'administrator')->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('abc'), 'moderator')->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('def'))->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('def'), 'guest')->toString(), $roles);
|
||||
$this->assertContains(Role::member(ID::custom('def'))->toString(), $roles);
|
||||
$this->assertContains(Role::member(ID::custom('abc'))->toString(), $roles);
|
||||
}
|
||||
|
||||
public function testAppUserRoles(): void
|
||||
{
|
||||
Authorization::setRole(User::ROLE_APPS);
|
||||
$user = new User([
|
||||
'$id' => ID::custom('123'),
|
||||
'memberships' => [
|
||||
[
|
||||
'$id' => ID::custom('def'),
|
||||
'teamId' => ID::custom('abc'),
|
||||
'confirm' => true,
|
||||
'roles' => [
|
||||
'administrator',
|
||||
'moderator'
|
||||
]
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('abc'),
|
||||
'teamId' => ID::custom('def'),
|
||||
'confirm' => true,
|
||||
'roles' => [
|
||||
'guest'
|
||||
]
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$roles = $user->getRoles();
|
||||
|
||||
$this->assertCount(7, $roles);
|
||||
$this->assertNotContains(Role::users()->toString(), $roles);
|
||||
$this->assertNotContains(Role::user(ID::custom('123'))->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('abc'))->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('abc'), 'administrator')->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('abc'), 'moderator')->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('def'))->toString(), $roles);
|
||||
$this->assertContains(Role::team(ID::custom('def'), 'guest')->toString(), $roles);
|
||||
$this->assertContains(Role::member(ID::custom('def'))->toString(), $roles);
|
||||
$this->assertContains(Role::member(ID::custom('abc'))->toString(), $roles);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user