From c7bbf6a987a6e5cd39b9e29bf59e7c88197d0afd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 16 Feb 2026 16:14:43 +0100 Subject: [PATCH 1/9] fix org keys auth --- app/controllers/shared/api.php | 10 ++++++++-- app/init/resources.php | 3 +++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 378c5d5c9c..7e4cff32f4 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -169,8 +169,10 @@ Http::init() // Handle special app role case if ($apiKey->getRole() === User::ROLE_APPS) { - // Disable authorization checks for API keys - $authorization->setDefaultStatus(false); + // Disable authorization checks for project API keys + if ($project->getId() !== 'console') { + $authorization->setDefaultStatus(false); + } $user = new User([ '$id' => '', @@ -245,6 +247,10 @@ Http::init() } } + $authorization->addRole(Role::team($team->getId())->toString()); + $authorization->addRole(Role::team($team->getId(), 'owner')->toString()); + $authorization->addRole(Role::member($team->getId())->toString()); + $queueForAudits->setUser($user); } } // Admin User Authentication diff --git a/app/init/resources.php b/app/init/resources.php index cdc2e8a367..b2a7a0189d 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -1304,6 +1304,7 @@ Http::setResource('team', function (Document $project, Database $dbForPlatform, } else { $route = $utopia->match($request); $path = !empty($route) ? $route->getPath() : $request->getURI(); + $orgHeader = $request->getHeader('x-appwrite-organization', ''); if (str_starts_with($path, '/v1/projects/:projectId')) { $uri = $request->getURI(); $pid = explode('/', $uri)[3]; @@ -1318,6 +1319,8 @@ Http::setResource('team', function (Document $project, Database $dbForPlatform, $team = $authorization->skip(fn () => $dbForPlatform->getDocument('teams', $teamId)); return $team; + } elseif (!empty($orgHeader)) { + return $authorization->skip(fn () => $dbForPlatform->getDocument('teams', $orgHeader)); } } From 4b084849206e5f2b637dc5f0ea7c0ea1be5a2311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 16 Feb 2026 16:24:33 +0100 Subject: [PATCH 2/9] Fix tests --- app/controllers/shared/api.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 7e4cff32f4..15cd925f27 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -170,7 +170,7 @@ Http::init() // Handle special app role case if ($apiKey->getRole() === User::ROLE_APPS) { // Disable authorization checks for project API keys - if ($project->getId() !== 'console') { + if ($apiKey->getType() === API_KEY_STANDARD && $apiKey->getProjectId() === $project->getId()) { $authorization->setDefaultStatus(false); } From 57127d40da3c43267a09e49adc4ff57d8f95aeb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 16 Feb 2026 16:47:19 +0100 Subject: [PATCH 3/9] Fix failing tests --- docker-compose.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 5a6367f402..ab33920060 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -750,6 +750,12 @@ services: - _APP_REDIS_PORT - _APP_REDIS_USER - _APP_REDIS_PASS + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + - _APP_DB_ADAPTER - _APP_SMTP_HOST - _APP_SMTP_PORT - _APP_SMTP_SECURE From 13edcbaf6f257d44b0b83e2d1cf0db51ce88bb47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 16 Feb 2026 17:06:11 +0100 Subject: [PATCH 4/9] fix abuse test; fix mail-related tests --- tests/e2e/Services/Projects/ProjectsConsoleClientTest.php | 4 ++-- tests/resources/docker/docker-compose.yml | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 1d15f10971..64556522f5 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -5384,8 +5384,8 @@ class ProjectsConsoleClientTest extends Scope ], [ 'success' => 'https://example.com', 'failure' => 'https://example.com' - ]); - $this->assertEquals(200, $response['headers']['status-code']); + ], followRedirects: false); + $this->assertEquals(301, $response['headers']['status-code']); /** Ensure any hostname is allowed */ $response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider, [ diff --git a/tests/resources/docker/docker-compose.yml b/tests/resources/docker/docker-compose.yml index 8530df0db6..02593f8123 100644 --- a/tests/resources/docker/docker-compose.yml +++ b/tests/resources/docker/docker-compose.yml @@ -279,6 +279,12 @@ services: - _APP_ENV - _APP_REDIS_HOST - _APP_REDIS_PORT + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + - _APP_DB_ADAPTER - _APP_SMTP_HOST - _APP_SMTP_PORT From bb26a9f583eb20fca0feaadc8a3a82e48a188a9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 16 Feb 2026 17:18:58 +0100 Subject: [PATCH 5/9] improve devkey test for oauth --- docker-compose.yml | 1 + .../Projects/ProjectsConsoleClientTest.php | 31 ++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index ab33920060..635b1cb2cf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -738,6 +738,7 @@ services: depends_on: - redis - maildev + - ${_APP_DB_HOST:-mariadb} # - smtp environment: - _APP_ENV diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 64556522f5..4eccad4966 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -5376,7 +5376,7 @@ class ProjectsConsoleClientTest extends Scope ]); $this->assertEquals(400, $response['headers']['status-code']); - /** Test oauth2 with devKey and now get oauth2 is disabled */ + /** Test oauth2 with devKey and now flow works with untrusted URL too */ $response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider, [ 'content-type' => 'application/json', 'x-appwrite-project' => $projectId, @@ -5385,7 +5385,36 @@ class ProjectsConsoleClientTest extends Scope 'success' => 'https://example.com', 'failure' => 'https://example.com' ], followRedirects: false); + $this->assertEquals(301, $response['headers']['status-code']); + $this->assertArrayHasKey('location', $response['headers']); + + $location = $response['headers']['location']; + + + $locationClient = new Client(); + $locationClient->setEndpoint(''); + $locationClient->addHeader('x-appwrite-dev-key', $devKey['secret']); + + $response = $locationClient->call(Client::METHOD_GET, $location, followRedirects: false); + + $this->assertEquals(301, $response['headers']['status-code']); + $this->assertArrayHasKey('location', $response['headers']); + + $location = $response['headers']['location']; + $this->assertStringStartsWith('http://appwrite:/v1/account/sessions/oauth2/callback/mock/', $response['headers']['location']); + + $response = $locationClient->call(Client::METHOD_GET, $location, followRedirects: false); + $this->assertEquals(301, $response['headers']['status-code']); + $this->assertArrayHasKey('location', $response['headers']); + + $location = $response['headers']['location']; + $this->assertStringStartsWith('http://appwrite:/v1/account/sessions/oauth2/mock/redirect', $response['headers']['location']); + + $response = $locationClient->call(Client::METHOD_GET, $location, followRedirects: false); + + $this->assertEquals(301, $response['headers']['status-code']); + $this->assertSame('https://example.com/#', $response['headers']['location']); /** Ensure any hostname is allowed */ $response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider, [ From 46ed91fe487e45b3a5b8a0ea4303469ff369defb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 16 Feb 2026 17:39:12 +0100 Subject: [PATCH 6/9] Fix migratons with api keys --- app/controllers/shared/api.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 15cd925f27..1b9726ded5 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -170,7 +170,7 @@ Http::init() // Handle special app role case if ($apiKey->getRole() === User::ROLE_APPS) { // Disable authorization checks for project API keys - if ($apiKey->getType() === API_KEY_STANDARD && $apiKey->getProjectId() === $project->getId()) { + if (($apiKey->getType() === API_KEY_STANDARD || $apiKey->getType() === API_KEY_DYNAMIC) && $apiKey->getProjectId() === $project->getId()) { $authorization->setDefaultStatus(false); } From 180ac93871f5a3df89c557652d2c2857b11e290a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 16 Feb 2026 17:48:26 +0100 Subject: [PATCH 7/9] Fix accoutn keys permissions --- app/controllers/shared/api.php | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 1b9726ded5..1596607446 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -30,6 +30,7 @@ use Utopia\Database\Document; use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Authorization\Input; +use Utopia\Database\Validator\Roles; use Utopia\Http\Http; use Utopia\System\System; use Utopia\Telemetry\Adapter as Telemetry; @@ -247,11 +248,37 @@ Http::init() } } + $queueForAudits->setUser($user); + } + + // Apply permission + if ($apiKey->getType() === API_KEY_ORGANIZATION) { $authorization->addRole(Role::team($team->getId())->toString()); $authorization->addRole(Role::team($team->getId(), 'owner')->toString()); $authorization->addRole(Role::member($team->getId())->toString()); + } elseif ($apiKey->getType() === API_KEY_ACCOUNT) { + $authorization->addRole(Role::user($user->getId())->toString()); + $authorization->addRole(Role::users()->toString()); - $queueForAudits->setUser($user); + if ($user->getAttribute('emailVerification', false) && $user->getAttribute('phoneVerification', false)) { + $authorization->addRole(Role::user($user->getId(), Roles::DIMENSION_VERIFIED)->toString()); + $authorization->addRole(Role::users(Roles::DIMENSION_VERIFIED)->toString()); + } else { + $authorization->addRole(Role::user($user->getId(), Roles::DIMENSION_UNVERIFIED)->toString()); + $authorization->addRole(Role::users(Roles::DIMENSION_UNVERIFIED)->toString()); + } + + foreach (\array_filter($user->getAttribute('memberships', []), fn ($membership) => !isset($membership['confirm']) || !$membership['confirm']) as $nodeMembership) { + $authorization->addRole(Role::team($nodeMembership['teamId'])->toString()); + $authorization->addRole(Role::member($nodeMembership->getId())->toString()); + foreach (($node['roles'] ?? []) as $nodeRole) { + $authorization->addRole(Role::team($nodeMembership['teamId'], $nodeRole)->toString()); + } + } + + foreach ($user->getAttribute('labels', []) as $nodeLabel) { + $authorization->addRole('label:' . $nodeLabel); + } } } // Admin User Authentication elseif (($project->getId() === 'console' && !$team->isEmpty() && !$user->isEmpty()) || ($project->getId() !== 'console' && !$user->isEmpty() && $mode === APP_MODE_ADMIN)) { From 343bed9b9dbdf22c092565f38f1b7c46d24897cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 16 Feb 2026 17:59:44 +0100 Subject: [PATCH 8/9] PR review fixes --- app/controllers/shared/api.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 1596607446..1c0c7a89dc 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -255,7 +255,6 @@ Http::init() if ($apiKey->getType() === API_KEY_ORGANIZATION) { $authorization->addRole(Role::team($team->getId())->toString()); $authorization->addRole(Role::team($team->getId(), 'owner')->toString()); - $authorization->addRole(Role::member($team->getId())->toString()); } elseif ($apiKey->getType() === API_KEY_ACCOUNT) { $authorization->addRole(Role::user($user->getId())->toString()); $authorization->addRole(Role::users()->toString()); @@ -268,7 +267,7 @@ Http::init() $authorization->addRole(Role::users(Roles::DIMENSION_UNVERIFIED)->toString()); } - foreach (\array_filter($user->getAttribute('memberships', []), fn ($membership) => !isset($membership['confirm']) || !$membership['confirm']) as $nodeMembership) { + foreach (\array_filter($user->getAttribute('memberships', []), fn ($membership) => ($membership['confirm'] ?? false) === true) as $nodeMembership) { $authorization->addRole(Role::team($nodeMembership['teamId'])->toString()); $authorization->addRole(Role::member($nodeMembership->getId())->toString()); foreach (($node['roles'] ?? []) as $nodeRole) { From 9572201863369060cf6bbf86d7148ea6b605c2f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 16 Feb 2026 18:08:52 +0100 Subject: [PATCH 9/9] AI review fixes --- app/controllers/shared/api.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 1c0c7a89dc..2c0c61332c 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -259,7 +259,7 @@ Http::init() $authorization->addRole(Role::user($user->getId())->toString()); $authorization->addRole(Role::users()->toString()); - if ($user->getAttribute('emailVerification', false) && $user->getAttribute('phoneVerification', false)) { + if ($user->getAttribute('emailVerification', false) || $user->getAttribute('phoneVerification', false)) { $authorization->addRole(Role::user($user->getId(), Roles::DIMENSION_VERIFIED)->toString()); $authorization->addRole(Role::users(Roles::DIMENSION_VERIFIED)->toString()); } else { @@ -270,7 +270,7 @@ Http::init() foreach (\array_filter($user->getAttribute('memberships', []), fn ($membership) => ($membership['confirm'] ?? false) === true) as $nodeMembership) { $authorization->addRole(Role::team($nodeMembership['teamId'])->toString()); $authorization->addRole(Role::member($nodeMembership->getId())->toString()); - foreach (($node['roles'] ?? []) as $nodeRole) { + foreach (($nodeMembership['roles'] ?? []) as $nodeRole) { $authorization->addRole(Role::team($nodeMembership['teamId'], $nodeRole)->toString()); } }