diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index d674355294..446c7a34a2 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; @@ -169,8 +170,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 (($apiKey->getType() === API_KEY_STANDARD || $apiKey->getType() === API_KEY_DYNAMIC) && $apiKey->getProjectId() === $project->getId()) { + $authorization->setDefaultStatus(false); + } $user = new User([ '$id' => '', @@ -247,6 +250,35 @@ 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()); + } elseif ($apiKey->getType() === API_KEY_ACCOUNT) { + $authorization->addRole(Role::user($user->getId())->toString()); + $authorization->addRole(Role::users()->toString()); + + 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) => ($membership['confirm'] ?? false) === true) as $nodeMembership) { + $authorization->addRole(Role::team($nodeMembership['teamId'])->toString()); + $authorization->addRole(Role::member($nodeMembership->getId())->toString()); + foreach (($nodeMembership['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)) { $teamId = $team->getId(); @@ -498,6 +530,7 @@ Http::init() $queueForMessaging->setProject($project); $queueForFunctions->setProject($project); $queueForBuilds->setProject($project); + $queueForMails->setProject($project); /* Auto-set platforms */ $queueForFunctions->setPlatform($platform); diff --git a/app/init/resources.php b/app/init/resources.php index a36a87ae90..914cffffee 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -1298,6 +1298,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]; @@ -1312,6 +1313,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)); } } diff --git a/docker-compose.yml b/docker-compose.yml index 6c4e9f0231..ea5e53ca8c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -748,6 +748,7 @@ services: depends_on: - redis - maildev + - ${_APP_DB_HOST:-mariadb} # - smtp environment: - _APP_ENV @@ -760,6 +761,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 diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index aaa5baf3cf..0245084b5e 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -932,16 +932,6 @@ class Builds extends Action $this->runGitAction('ready', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime, $platform); } - /** Screenshot site */ - if ($resource->getCollection() === 'sites') { - $queueForScreenshots - ->setDeploymentId($deployment->getId()) - ->setProject($project) - ->trigger(); - - Console::log('Site screenshot queued'); - } - /** Set auto deploy */ $activateBuild = false; if ($deployment->getAttribute('activate') === true) { @@ -1023,6 +1013,16 @@ class Builds extends Action Console::log('Deployment activated'); } + /** Screenshot site */ + if ($resource->getCollection() === 'sites') { + $queueForScreenshots + ->setDeploymentId($deployment->getId()) + ->setProject($project) + ->trigger(); + + Console::log('Site screenshot queued'); + } + $this->afterDeploymentSuccess( $project, $deployment, diff --git a/src/Appwrite/Platform/Workers/Mails.php b/src/Appwrite/Platform/Workers/Mails.php index b1f17fc648..72f7cddd06 100644 --- a/src/Appwrite/Platform/Workers/Mails.php +++ b/src/Appwrite/Platform/Workers/Mails.php @@ -6,6 +6,7 @@ use Appwrite\Template\Template; use Exception; use PHPMailer\PHPMailer\PHPMailer; use Swoole\Runtime; +use Utopia\Database\Document; use Utopia\Logger\Log; use Utopia\Platform\Action; use Utopia\Queue\Message; @@ -32,6 +33,7 @@ class Mails extends Action $this ->desc('Mails worker') ->inject('message') + ->inject('project') ->inject('register') ->inject('log') ->callback($this->action(...)); @@ -53,7 +55,7 @@ class Mails extends Action * @return void * @throws Exception */ - public function action(Message $message, Registry $register, Log $log): void + public function action(Message $message, Document $project, Registry $register, Log $log): void { Runtime::setHookFlags(SWOOLE_HOOK_ALL ^ SWOOLE_HOOK_TCP); $payload = $message->getPayload() ?? []; diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index c6b005c334..94cbcf341c 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -735,6 +735,7 @@ class Migrations extends Action ]; $queueForMails + ->setProject($project) ->setSubject($subject) ->setPreview($preview) ->setBody($emailBody) diff --git a/src/Appwrite/Platform/Workers/Webhooks.php b/src/Appwrite/Platform/Workers/Webhooks.php index dbfbe591a6..56839058de 100644 --- a/src/Appwrite/Platform/Workers/Webhooks.php +++ b/src/Appwrite/Platform/Workers/Webhooks.php @@ -253,6 +253,7 @@ class Webhooks extends Action ->setParam('{{year}}', date("Y")); $queueForMails + ->setProject($project) ->setSubject($subject) ->setPreview($preview) ->setBody($body->render()); diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index fd518e4a9a..3fc043725d 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -456,6 +456,8 @@ class Response extends SwooleResponse foreach ($data[$key] as $index => $item) { if ($item instanceof Document) { + $ruleType = null; + if (\is_array($rule['type'])) { foreach ($rule['type'] as $type) { $condition = false; @@ -474,7 +476,7 @@ class Response extends SwooleResponse $ruleType = $rule['type']; } - if (!self::hasModel($ruleType)) { + if ($ruleType === null || !self::hasModel($ruleType)) { throw new Exception('Missing model for rule: ' . $ruleType); } diff --git a/src/Appwrite/Utopia/Response/Model/AttributeList.php b/src/Appwrite/Utopia/Response/Model/AttributeList.php index 6b9a7365bd..0b17a655ab 100644 --- a/src/Appwrite/Utopia/Response/Model/AttributeList.php +++ b/src/Appwrite/Utopia/Response/Model/AttributeList.php @@ -30,6 +30,10 @@ class AttributeList extends Model Response::MODEL_ATTRIBUTE_POINT, Response::MODEL_ATTRIBUTE_LINE, Response::MODEL_ATTRIBUTE_POLYGON, + Response::MODEL_ATTRIBUTE_VARCHAR, + Response::MODEL_ATTRIBUTE_TEXT, + Response::MODEL_ATTRIBUTE_MEDIUMTEXT, + Response::MODEL_ATTRIBUTE_LONGTEXT, Response::MODEL_ATTRIBUTE_STRING // needs to be last, since its condition would dominate any other string attribute ], 'description' => 'List of attributes.', diff --git a/src/Appwrite/Utopia/Response/Model/Collection.php b/src/Appwrite/Utopia/Response/Model/Collection.php index 407db3aea9..4ab7de8e4d 100644 --- a/src/Appwrite/Utopia/Response/Model/Collection.php +++ b/src/Appwrite/Utopia/Response/Model/Collection.php @@ -73,6 +73,10 @@ class Collection extends Model Response::MODEL_ATTRIBUTE_POINT, Response::MODEL_ATTRIBUTE_LINE, Response::MODEL_ATTRIBUTE_POLYGON, + Response::MODEL_ATTRIBUTE_VARCHAR, + Response::MODEL_ATTRIBUTE_TEXT, + Response::MODEL_ATTRIBUTE_MEDIUMTEXT, + Response::MODEL_ATTRIBUTE_LONGTEXT, Response::MODEL_ATTRIBUTE_STRING, // needs to be last, since its condition would dominate any other string attribute ], 'description' => 'Collection attributes.', diff --git a/tests/e2e/Services/Databases/Legacy/DatabasesStringTypesTest.php b/tests/e2e/Services/Databases/Legacy/DatabasesStringTypesTest.php index 8b7886b864..d1ea6ae921 100644 --- a/tests/e2e/Services/Databases/Legacy/DatabasesStringTypesTest.php +++ b/tests/e2e/Services/Databases/Legacy/DatabasesStringTypesTest.php @@ -514,6 +514,63 @@ class DatabasesStringTypesTest extends Scope /** * @depends testCreateLongtextAttribute */ + public function testListStringTypeAttributes(array $data): array + { + $databaseId = $data['databaseId']; + $collectionId = $data['collectionId']; + + // Wait for attributes to be created + sleep(2); + + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + $attributes = $response['body']['attributes']; + $types = array_column($attributes, 'type'); + + $this->assertContains('varchar', $types); + $this->assertContains('text', $types); + $this->assertContains('mediumtext', $types); + $this->assertContains('longtext', $types); + + return $data; + } + + /** + * @depends testListStringTypeAttributes + */ + public function testGetCollectionWithStringTypeAttributes(array $data): array + { + $databaseId = $data['databaseId']; + $collectionId = $data['collectionId']; + + $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + $attributes = $response['body']['attributes']; + $types = array_column($attributes, 'type'); + + $this->assertContains('varchar', $types); + $this->assertContains('text', $types); + $this->assertContains('mediumtext', $types); + $this->assertContains('longtext', $types); + + return $data; + } + + /** + * @depends testGetCollectionWithStringTypeAttributes + */ public function testUpdateVarcharAttribute(array $data): array { $this->markTestSkipped('Skipped until utopia-php/database updateAttribute supports VARCHAR type'); diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 28bec11bdf..c6e7206c85 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -5377,7 +5377,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,8 +5385,37 @@ 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']); + $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, [ 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