diff --git a/app/config/roles.php b/app/config/roles.php index 0f0945a2b4..2688e5edc7 100644 --- a/app/config/roles.php +++ b/app/config/roles.php @@ -128,6 +128,10 @@ return [ 'label' => 'Owner', 'scopes' => \array_merge($member, $admins), ], + Auth::USER_ROLE_MEMBER => [ + 'label' => 'Member', + 'scopes' => \array_merge($member), + ], Auth::USER_ROLE_APPS => [ 'label' => 'Applications', 'scopes' => ['global', 'health.read', 'graphql'], diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 760a4f23a6..bf3586b942 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -172,14 +172,27 @@ App::post('/v1/projects') } try { + $teamIdentifierForRole = ID::custom($teamId); + $projectIdentifierForRole = ID::custom($projectId); + $project = $dbForPlatform->createDocument('projects', new Document([ '$id' => $projectId, '$permissions' => [ - Permission::read(Role::team(ID::custom($teamId))), - Permission::update(Role::team(ID::custom($teamId), 'owner')), - Permission::update(Role::team(ID::custom($teamId), 'developer')), - Permission::delete(Role::team(ID::custom($teamId), 'owner')), - Permission::delete(Role::team(ID::custom($teamId), 'developer')), + // Team-level permissions + Permission::read(Role::team($teamIdentifierForRole)), + Permission::update(Role::team($teamIdentifierForRole, 'owner')), + Permission::update(Role::team($teamIdentifierForRole, 'developer')), + Permission::delete(Role::team($teamIdentifierForRole, 'owner')), + Permission::delete(Role::team($teamIdentifierForRole, 'developer')), + // Project-specific permissions + Permission::read(Role::project($projectIdentifierForRole, 'owner')), + Permission::read(Role::project($projectIdentifierForRole, 'developer')), + Permission::read(Role::project($projectIdentifierForRole, 'editor')), + Permission::read(Role::project($projectIdentifierForRole, 'analyst')), + Permission::update(Role::project($projectIdentifierForRole, 'owner')), + Permission::update(Role::project($projectIdentifierForRole, 'developer')), + Permission::delete(Role::project($projectIdentifierForRole, 'owner')), + Permission::delete(Role::project($projectIdentifierForRole, 'developer')), ], 'name' => $name, 'teamInternalId' => $team->getSequence(), diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 6a146f2bdc..949bf39690 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -29,6 +29,7 @@ use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\Authorization; +use Utopia\Database\Validator\Roles; use Utopia\Queue\Publisher; use Utopia\System\System; use Utopia\Telemetry\Adapter as Telemetry; @@ -334,6 +335,8 @@ App::init() $scopes = []; // Reset scope if admin foreach ($adminRoles as $role) { + $role = Role::parse($role); + $role = $role->getRole() === Roles::ROLE_PROJECT ? $role->getDimension() : $role->getRole(); $scopes = \array_merge($scopes, $roles[$role]['scopes']); } diff --git a/composer.json b/composer.json index 0fa5fee879..e193337694 100644 --- a/composer.json +++ b/composer.json @@ -51,7 +51,7 @@ "utopia-php/cache": "0.13.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "1.*.*", - "utopia-php/database": "3.*", + "utopia-php/database": "dev-ser-541-3.x as 3.3.99", "utopia-php/detector": "0.2.*", "utopia-php/domains": "0.9.*", "utopia-php/emails": "0.6.*", @@ -107,5 +107,11 @@ "php-http/discovery": true, "tbachert/spi": true } - } + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/utopia-php/database" + } + ] } diff --git a/composer.lock b/composer.lock index d66945e3fc..a802b33d39 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "dc4eb1888275a24d596468924e272e25", + "content-hash": "0180f566c380723a1d0d35b44f346abf", "packages": [ { "name": "adhocore/jwt", @@ -3840,16 +3840,16 @@ }, { "name": "utopia-php/database", - "version": "3.5.0", + "version": "dev-ser-541-3.x", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "5da71b65a6123ce2e78795522b05b7458aabfbd7" + "reference": "ad744de0844a61bc232b24544ac49d4168251f91" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/5da71b65a6123ce2e78795522b05b7458aabfbd7", - "reference": "5da71b65a6123ce2e78795522b05b7458aabfbd7", + "url": "https://api.github.com/repos/utopia-php/database/zipball/ad744de0844a61bc232b24544ac49d4168251f91", + "reference": "ad744de0844a61bc232b24544ac49d4168251f91", "shasum": "" }, "require": { @@ -3878,7 +3878,38 @@ "Utopia\\Database\\": "src/Database" } }, - "notification-url": "https://packagist.org/downloads/", + "autoload-dev": { + "psr-4": { + "Tests\\E2E\\": "tests/e2e", + "Tests\\Unit\\": "tests/unit" + } + }, + "scripts": { + "build": [ + "Composer\\Config::disableProcessTimeout", + "docker compose build" + ], + "start": [ + "Composer\\Config::disableProcessTimeout", + "docker compose up -d" + ], + "test": [ + "Composer\\Config::disableProcessTimeout", + "docker compose exec tests vendor/bin/phpunit --configuration phpunit.xml" + ], + "lint": [ + "php -d memory_limit=2G ./vendor/bin/pint --test" + ], + "format": [ + "php -d memory_limit=2G ./vendor/bin/pint" + ], + "check": [ + "./vendor/bin/phpstan analyse --level 7 src tests --memory-limit 2G" + ], + "coverage": [ + "./vendor/bin/coverage-check ./tmp/clover.xml 90" + ] + }, "license": [ "MIT" ], @@ -3891,10 +3922,10 @@ "utopia" ], "support": { - "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/3.5.0" + "source": "https://github.com/utopia-php/database/tree/ser-541-3.x", + "issues": "https://github.com/utopia-php/database/issues" }, - "time": "2025-11-18T08:11:01+00:00" + "time": "2025-11-13T12:53:38+00:00" }, { "name": "utopia-php/detector", @@ -4456,16 +4487,16 @@ }, { "name": "utopia-php/migration", - "version": "1.3.3", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "731b3a963c58c30e0b2368695d57a7e8fcb7455c" + "reference": "18bd7d39dcee09280f40edb12879c727ecec98d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/731b3a963c58c30e0b2368695d57a7e8fcb7455c", - "reference": "731b3a963c58c30e0b2368695d57a7e8fcb7455c", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/18bd7d39dcee09280f40edb12879c727ecec98d3", + "reference": "18bd7d39dcee09280f40edb12879c727ecec98d3", "shasum": "" }, "require": { @@ -4505,9 +4536,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/1.3.3" + "source": "https://github.com/utopia-php/migration/tree/1.4.0" }, - "time": "2025-10-28T04:02:08+00:00" + "time": "2025-11-21T06:08:59+00:00" }, { "name": "utopia-php/mongo", @@ -5379,16 +5410,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.5.7", + "version": "1.5.8", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "dc6720ba92ed98e2c62b2a319d4371f167ccc808" + "reference": "05367bc4a4c3e020e9aca114ae875b626ce8fc55" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/dc6720ba92ed98e2c62b2a319d4371f167ccc808", - "reference": "dc6720ba92ed98e2c62b2a319d4371f167ccc808", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/05367bc4a4c3e020e9aca114ae875b626ce8fc55", + "reference": "05367bc4a4c3e020e9aca114ae875b626ce8fc55", "shasum": "" }, "require": { @@ -5424,9 +5455,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/1.5.7" + "source": "https://github.com/appwrite/sdk-generator/tree/1.5.8" }, - "time": "2025-11-18T05:57:01+00:00" + "time": "2025-11-20T11:00:34+00:00" }, { "name": "doctrine/annotations", @@ -8891,9 +8922,18 @@ "time": "2024-03-07T20:33:40+00:00" } ], - "aliases": [], + "aliases": [ + { + "package": "utopia-php/database", + "version": "dev-ser-541-3.x", + "alias": "3.3.99", + "alias_normalized": "3.3.99.0" + } + ], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "utopia-php/database": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -8917,5 +8957,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/src/Appwrite/Auth/Auth.php b/src/Appwrite/Auth/Auth.php index 9af5045fa4..5769677ef6 100644 --- a/src/Appwrite/Auth/Auth.php +++ b/src/Appwrite/Auth/Auth.php @@ -40,6 +40,7 @@ class Auth public const USER_ROLE_ADMIN = 'admin'; public const USER_ROLE_DEVELOPER = 'developer'; public const USER_ROLE_OWNER = 'owner'; + public const USER_ROLE_MEMBER = 'member'; public const USER_ROLE_APPS = 'apps'; public const USER_ROLE_SYSTEM = 'system'; @@ -478,19 +479,18 @@ class Auth } foreach ($user->getAttribute('memberships', []) as $node) { - if (!isset($node['confirm']) || !$node['confirm']) { + if (!isset($node['confirm']) || !$node['confirm'] || !isset($node['$id']) || !isset($node['teamId'])) { continue; } + $roles[] = Role::member($node['$id'])->toString(); + $projectRoles = \array_filter($node['roles'] ?? [], fn ($role) => str_starts_with($role, Roles::ROLE_PROJECT)); - if (isset($node['$id']) && isset($node['teamId'])) { + if (!empty($projectRoles)) { + $roles[] = Role::team($node['teamId'], Auth::USER_ROLE_MEMBER)->toString(); + $roles = \array_merge($roles, $projectRoles); + } else { $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(); - } - } + $roles = \array_merge($roles, \array_map(fn ($role) => Role::team($node['teamId'], $role)->toString(), $node['roles'] ?? [])); } }