mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c63755e05 | ||
|
|
3ee91fd7a3 | ||
|
|
5d78051fb5 | ||
|
|
83820a7b0e | ||
|
|
6cc60ac680 | ||
|
|
bd6edaa3f2 | ||
|
|
4fbf79d815 | ||
|
|
af6ed25e38 | ||
|
|
561761b406 | ||
|
|
3d00c579a4 |
@@ -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'],
|
||||
|
||||
@@ -172,14 +172,25 @@ 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::delete(Role::project($projectIdentifierForRole, 'owner')),
|
||||
],
|
||||
'name' => $name,
|
||||
'teamInternalId' => $team->getSequence(),
|
||||
@@ -423,12 +434,23 @@ App::patch('/v1/projects/:projectId/team')
|
||||
throw new Exception(Exception::TEAM_NOT_FOUND);
|
||||
}
|
||||
|
||||
$teamIdentifierForRole = ID::custom($teamId);
|
||||
$projectIdentifierForRole = ID::custom($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::delete(Role::project($projectIdentifierForRole, 'owner')),
|
||||
];
|
||||
|
||||
$project
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Auth\MFA\Type\TOTP;
|
||||
use Appwrite\Auth\Validator\Phone;
|
||||
use Appwrite\Auth\Validator\Role as RoleValidator;
|
||||
use Appwrite\Detector\Detector;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
@@ -56,7 +57,6 @@ use Utopia\Validator\Assoc;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\URL;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
App::post('/v1/teams')
|
||||
->desc('Create team')
|
||||
@@ -97,6 +97,7 @@ App::post('/v1/teams')
|
||||
'$id' => $teamId,
|
||||
'$permissions' => [
|
||||
Permission::read(Role::team($teamId)),
|
||||
Permission::read(Role::team($teamId, 'member')),
|
||||
Permission::update(Role::team($teamId, 'owner')),
|
||||
Permission::delete(Role::team($teamId, 'owner')),
|
||||
],
|
||||
@@ -479,7 +480,7 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
array_filter($roles, function ($role) {
|
||||
return !in_array($role, [Auth::USER_ROLE_APPS, Auth::USER_ROLE_GUESTS, Auth::USER_ROLE_USERS]);
|
||||
});
|
||||
return new ArrayList(new WhiteList($roles), APP_LIMIT_ARRAY_PARAMS_SIZE);
|
||||
return new ArrayList(new RoleValidator($roles), APP_LIMIT_ARRAY_PARAMS_SIZE);
|
||||
}
|
||||
return new ArrayList(new Key(), APP_LIMIT_ARRAY_PARAMS_SIZE);
|
||||
}, 'Array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](https://appwrite.io/docs/permissions). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 32 characters long.', false, ['project'])
|
||||
@@ -1090,7 +1091,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
|
||||
array_filter($roles, function ($role) {
|
||||
return !in_array($role, [Auth::USER_ROLE_APPS, Auth::USER_ROLE_GUESTS, Auth::USER_ROLE_USERS]);
|
||||
});
|
||||
return new ArrayList(new WhiteList($roles), APP_LIMIT_ARRAY_PARAMS_SIZE);
|
||||
return new ArrayList(new RoleValidator($roles), APP_LIMIT_ARRAY_PARAMS_SIZE);
|
||||
}
|
||||
return new ArrayList(new Key(), APP_LIMIT_ARRAY_PARAMS_SIZE);
|
||||
}, 'An array of strings. Use this param to set the user\'s roles in the team. A role can be any string. Learn more about [roles and permissions](https://appwrite.io/docs/permissions). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 32 characters long.', false, ['project'])
|
||||
|
||||
@@ -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;
|
||||
@@ -333,11 +334,23 @@ App::init()
|
||||
}
|
||||
|
||||
$scopes = []; // Reset scope if admin
|
||||
foreach ($adminRoles as $role) {
|
||||
$scopes = \array_merge($scopes, $roles[$role]['scopes']);
|
||||
$hasProjectSpecificPermissions = false;
|
||||
foreach ($adminRoles as $adminRole) {
|
||||
$adminRole = Role::parse($adminRole);
|
||||
if ($adminRole->getRole() === Roles::ROLE_PROJECT) {
|
||||
$hasProjectSpecificPermissions = true;
|
||||
$adminRole = $adminRole->getDimension();
|
||||
} else {
|
||||
$adminRole = $adminRole->getRole();
|
||||
}
|
||||
$scopes = \array_merge($scopes, $roles[$adminRole]['scopes'] ?? []);
|
||||
}
|
||||
|
||||
Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users.
|
||||
Authorization::setDefaultStatus($hasProjectSpecificPermissions); // Cancel security segmentation for admin users.
|
||||
|
||||
if (!$hasProjectSpecificPermissions) {
|
||||
$role = $adminRole;
|
||||
}
|
||||
}
|
||||
|
||||
$scopes = \array_unique($scopes);
|
||||
|
||||
+8
-2
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Generated
+124
-83
@@ -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": "de089be13fb56313c3554a681070b2a8",
|
||||
"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",
|
||||
@@ -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": "b00fcf1103164e0ace57e4e2fac07bcc9431b65e"
|
||||
},
|
||||
"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/b00fcf1103164e0ace57e4e2fac07bcc9431b65e",
|
||||
"reference": "b00fcf1103164e0ace57e4e2fac07bcc9431b65e",
|
||||
"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-26T05:35:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/detector",
|
||||
@@ -3943,16 +3974,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 +3991,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 +4025,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 +4087,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 +4140,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 +4157,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 +4194,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 +4239,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 +4281,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 +5142,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 +5181,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 +5685,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 +5705,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 +5737,7 @@
|
||||
"description": "An opinionated code formatter for PHP.",
|
||||
"homepage": "https://laravel.com",
|
||||
"keywords": [
|
||||
"dev",
|
||||
"format",
|
||||
"formatter",
|
||||
"lint",
|
||||
@@ -5716,7 +5748,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",
|
||||
@@ -8891,9 +8923,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 +8958,5 @@
|
||||
"platform-overrides": {
|
||||
"php": "8.3"
|
||||
},
|
||||
"plugin-api-version": "2.9.0"
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
||||
@@ -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'] ?? []));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Auth\Validator;
|
||||
|
||||
use Utopia\Database\Helpers\Role as DBRole;
|
||||
use Utopia\Database\Validator\Roles;
|
||||
use Utopia\Validator;
|
||||
|
||||
class Role extends Validator
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $roles;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* Sets the acceptable roles.
|
||||
*
|
||||
* @param array $list
|
||||
* @param string $type of $list items
|
||||
*/
|
||||
public function __construct(array $roles)
|
||||
{
|
||||
$this->roles = $roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Description
|
||||
*
|
||||
* Returns validator description
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Value must be one of (' . \implode(', ', $this->roles) . ' or of the format "project:<projectId>/<role>")';
|
||||
}
|
||||
|
||||
/**
|
||||
* Is array
|
||||
*
|
||||
* Function will return true if object is array.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isArray(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* Returns validator type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return self::TYPE_STRING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is valid
|
||||
*
|
||||
* Validation will pass if $value is in the white list array.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid(mixed $value): bool
|
||||
{
|
||||
if (!\is_string($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$role = DBRole::parse($value);
|
||||
$valid = $role->getRole() === Roles::ROLE_PROJECT ? !empty($role->getIdentifier()) && \in_array($role->getDimension(), $this->roles) : \in_array($role->getRole(), $this->roles);
|
||||
|
||||
return $valid;
|
||||
}
|
||||
}
|
||||
@@ -7,24 +7,28 @@ use Utopia\Database\Helpers\ID;
|
||||
|
||||
trait ProjectsBase
|
||||
{
|
||||
protected function setupProject(mixed $params): string
|
||||
protected function setupProject(mixed $params, string $teamId = null, bool $newTeam = true): string
|
||||
{
|
||||
$team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'teamId' => ID::unique(),
|
||||
'name' => 'Project Test',
|
||||
]);
|
||||
if ($newTeam) {
|
||||
$team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'teamId' => $teamId ?? ID::unique(),
|
||||
'name' => 'Project Test',
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $team['headers']['status-code'], 'Setup team failed with status code: ' . $team['headers']['status-code'] . ' and response: ' . json_encode($team['body'], JSON_PRETTY_PRINT));
|
||||
$this->assertEquals(201, $team['headers']['status-code'], 'Setup team failed with status code: ' . $team['headers']['status-code'] . ' and response: ' . json_encode($team['body'], JSON_PRETTY_PRINT));
|
||||
|
||||
$teamId = $team['body']['$id'];
|
||||
}
|
||||
|
||||
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
...$params,
|
||||
'teamId' => $team['body']['$id'],
|
||||
'teamId' => $teamId,
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $project['headers']['status-code'], 'Setup project failed with status code: ' . $project['headers']['status-code'] . ' and response: ' . json_encode($project['body'], JSON_PRETTY_PRINT));
|
||||
@@ -46,4 +50,93 @@ trait ProjectsBase
|
||||
'secret' => $devKey['body']['secret'],
|
||||
];
|
||||
}
|
||||
|
||||
protected function setupUserMembership(mixed $params): array
|
||||
{
|
||||
// Create membership
|
||||
$response = $this->client->call(Client::METHOD_POST, '/teams/' . $params['teamId'] . '/memberships', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'email' => $params['email'],
|
||||
'name' => $params['name'],
|
||||
'roles' => $params['roles'],
|
||||
'url' => 'http://localhost:5000/join-us#title'
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
$this->assertNotEmpty($response['body']['$id']);
|
||||
$this->assertNotEmpty($response['body']['userId']);
|
||||
$this->assertEquals($params['name'], $response['body']['userName']);
|
||||
$this->assertEquals($params['email'], $response['body']['userEmail']);
|
||||
$this->assertNotEmpty($response['body']['teamId']);
|
||||
$this->assertCount(count($params['roles']), $response['body']['roles']);
|
||||
$this->assertEquals(false, $response['body']['confirm']);
|
||||
|
||||
$userId = $response['body']['userId'];
|
||||
$membershipId = $response['body']['$id'];
|
||||
|
||||
|
||||
$lastEmail = $this->getLastEmail();
|
||||
$tokens = $this->extractQueryParamsFromEmailLink($lastEmail['html']);
|
||||
$userId = $tokens['userId'];
|
||||
$secret = $tokens['secret'];
|
||||
|
||||
// Confirm membership
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/teams/' . $params['teamId'] . '/memberships/' . $membershipId . '/status', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
]), [
|
||||
'userId' => $userId,
|
||||
'secret' => $secret,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertNotEmpty($response['body']['$id']);
|
||||
$this->assertNotEmpty($response['body']['userId']);
|
||||
$this->assertNotEmpty($response['body']['teamId']);
|
||||
$this->assertCount(count($params['roles']), $response['body']['roles']);
|
||||
$this->assertEquals(true, $response['body']['confirm']);
|
||||
|
||||
// Simulate password recovery flow to reset password for the created user (useful when creating session for this user)
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/recovery', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
]), [
|
||||
'email' => $params['email'],
|
||||
'url' => 'http://localhost/recovery',
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
$this->assertNotEmpty($response['body']['$id']);
|
||||
$this->assertEmpty($response['body']['secret']);
|
||||
|
||||
$lastEmail = $this->getLastEmail();
|
||||
$this->assertEquals($params['email'], $lastEmail['to'][0]['address']);
|
||||
$this->assertEquals($params['name'], $lastEmail['to'][0]['name']);
|
||||
$this->assertEquals('Password Reset for ' . $this->getProject()['name'], $lastEmail['subject']);
|
||||
$this->assertStringContainsStringIgnoringCase('Reset your ' . $this->getProject()['name'] . ' password using the link.', $lastEmail['text']);
|
||||
|
||||
$tokens = $this->extractQueryParamsFromEmailLink($lastEmail['html']);
|
||||
$secret = $tokens['secret'];
|
||||
|
||||
$response = $this->client->call(Client::METHOD_PUT, '/account/recovery', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
]), [
|
||||
'userId' => $userId,
|
||||
'secret' => $secret,
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
return [
|
||||
'userId' => $userId,
|
||||
'membershipId' => $membershipId,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use Tests\E2E\Scopes\SideClient;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\System\System;
|
||||
|
||||
@@ -5086,8 +5087,8 @@ class ProjectsConsoleClientTest extends Scope
|
||||
$this->assertEmpty($response['body']);
|
||||
|
||||
/**
|
||||
* Get rate limit trying to use the deleted key
|
||||
*/
|
||||
* Get rate limit trying to use the deleted key
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
@@ -5119,4 +5120,213 @@ class ProjectsConsoleClientTest extends Scope
|
||||
/**
|
||||
* Devkeys Tests ends here ------------------------------------------------
|
||||
*/
|
||||
public function testPerProjectPermissionsForListProjects(): void
|
||||
{
|
||||
$teamId = ID::unique();
|
||||
$projectIdA = $this->setupProject([
|
||||
'projectId' => ID::unique(),
|
||||
'name' => 'Project Test A',
|
||||
'region' => System::getEnv('_APP_REGION', 'default')
|
||||
], $teamId);
|
||||
$projectIdB = $this->setupProject([
|
||||
'projectId' => ID::unique(),
|
||||
'name' => 'Project Test B',
|
||||
'region' => System::getEnv('_APP_REGION', 'default')
|
||||
], $teamId, false);
|
||||
|
||||
$projectAUserEmail = 'projecta-' . ID::unique() . '-owner@localhost.test';
|
||||
$projectAUserName = 'Project A - owner';
|
||||
$projectBUserEmail = 'projectb-' . ID::unique() . '-owner@localhost.test';
|
||||
$projectBUserName = 'Project B - owner';
|
||||
$this->setupUserMembership([
|
||||
'teamId' => $teamId,
|
||||
'email' => $projectAUserEmail,
|
||||
'name' => $projectAUserName,
|
||||
'roles' => ['member', Role::project($projectIdA, 'owner')->toString()],
|
||||
]);
|
||||
$this->setupUserMembership([
|
||||
'teamId' => $teamId,
|
||||
'email' => $projectBUserEmail,
|
||||
'name' => $projectBUserName,
|
||||
'roles' => ['member', Role::project($projectIdB, 'owner')->toString()],
|
||||
]);
|
||||
|
||||
$users = [
|
||||
['email' => $projectAUserEmail, 'name' => $projectAUserName, 'role' => 'owner', 'projectId' => $projectIdA],
|
||||
['email' => $projectBUserEmail, 'name' => $projectBUserName, 'role' => 'owner', 'projectId' => $projectIdB],
|
||||
];
|
||||
|
||||
foreach ($users as $user) {
|
||||
$session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], [
|
||||
'email' => $user['email'],
|
||||
'password' => 'password',
|
||||
]);
|
||||
$token = $session['cookies']['a_session_' . $this->getProject()['$id']];
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $token,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertNotEmpty($response['body']);
|
||||
$this->assertCount(1, $response['body']['projects']);
|
||||
$this->assertEquals($user['projectId'], $response['body']['projects'][0]['$id']);
|
||||
}
|
||||
}
|
||||
|
||||
public function testPerProjectPermissionsForUpdateProject(): void
|
||||
{
|
||||
$teamId = ID::unique();
|
||||
$projectIdA = $this->setupProject([
|
||||
'projectId' => ID::unique(),
|
||||
'name' => 'Project Test A',
|
||||
'region' => System::getEnv('_APP_REGION', 'default')
|
||||
], $teamId);
|
||||
$projectIdB = $this->setupProject([
|
||||
'projectId' => ID::unique(),
|
||||
'name' => 'Project Test B',
|
||||
'region' => System::getEnv('_APP_REGION', 'default')
|
||||
], $teamId, false);
|
||||
|
||||
$projectAUserEmail = 'projecta-' . ID::unique() . '-owner@localhost.test';
|
||||
$projectAUserName = 'Project A - owner';
|
||||
$projectBUserEmail = 'projectb-' . ID::unique() . '-owner@localhost.test';
|
||||
$projectBUserName = 'Project B - owner';
|
||||
$this->setupUserMembership([
|
||||
'teamId' => $teamId,
|
||||
'email' => $projectAUserEmail,
|
||||
'name' => $projectAUserName,
|
||||
'roles' => ['member', Role::project($projectIdA, 'owner')->toString()],
|
||||
]);
|
||||
$this->setupUserMembership([
|
||||
'teamId' => $teamId,
|
||||
'email' => $projectBUserEmail,
|
||||
'name' => $projectBUserName,
|
||||
'roles' => ['member', Role::project($projectIdB, 'owner')->toString()],
|
||||
]);
|
||||
|
||||
$users = [
|
||||
['email' => $projectAUserEmail, 'name' => $projectAUserName, 'role' => 'owner', 'projectId' => $projectIdA],
|
||||
['email' => $projectBUserEmail, 'name' => $projectBUserName, 'role' => 'owner', 'projectId' => $projectIdB],
|
||||
];
|
||||
|
||||
foreach ($users as $user) {
|
||||
$session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], [
|
||||
'email' => $user['email'],
|
||||
'password' => 'password',
|
||||
]);
|
||||
$token = $session['cookies']['a_session_' . $this->getProject()['$id']];
|
||||
|
||||
$accessibleProjectId = $user['projectId'] === $projectIdA ? $projectIdA : $projectIdB;
|
||||
$inaccessibleProjectId = $user['projectId'] === $projectIdA ? $projectIdB : $projectIdA;
|
||||
|
||||
$updatedProjectName = 'Updated Project Name ' . ID::unique();
|
||||
|
||||
// Success: User should be able to update the project they have membership for.
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $accessibleProjectId, [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $token,
|
||||
], [
|
||||
'name' => $updatedProjectName,
|
||||
]);
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertNotEmpty($response['body']);
|
||||
$this->assertEquals($updatedProjectName, $response['body']['name']);
|
||||
|
||||
// Failure: User should not be able to update the project they do not have membership for.
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $inaccessibleProjectId, [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $token,
|
||||
], [
|
||||
'name' => $updatedProjectName,
|
||||
]);
|
||||
|
||||
$this->assertEquals(404, $response['headers']['status-code']);
|
||||
}
|
||||
}
|
||||
|
||||
public function testPerProjectPermissionsForDeleteProject(): void
|
||||
{
|
||||
$teamId = ID::unique();
|
||||
$projectIdA = $this->setupProject([
|
||||
'projectId' => ID::unique(),
|
||||
'name' => 'Project Test A',
|
||||
'region' => System::getEnv('_APP_REGION', 'default')
|
||||
], $teamId);
|
||||
$projectIdB = $this->setupProject([
|
||||
'projectId' => ID::unique(),
|
||||
'name' => 'Project Test B',
|
||||
'region' => System::getEnv('_APP_REGION', 'default')
|
||||
], $teamId, false);
|
||||
|
||||
$projectAUserEmail = 'projecta-' . ID::unique() . '-owner@localhost.test';
|
||||
$projectAUserName = 'Project A - owner';
|
||||
$projectBUserEmail = 'projectb-' . ID::unique() . '-owner@localhost.test';
|
||||
$projectBUserName = 'Project B - owner';
|
||||
$this->setupUserMembership([
|
||||
'teamId' => $teamId,
|
||||
'email' => $projectAUserEmail,
|
||||
'name' => $projectAUserName,
|
||||
'roles' => ['member', Role::project($projectIdA, 'owner')->toString()],
|
||||
]);
|
||||
$this->setupUserMembership([
|
||||
'teamId' => $teamId,
|
||||
'email' => $projectBUserEmail,
|
||||
'name' => $projectBUserName,
|
||||
'roles' => ['member', Role::project($projectIdB, 'owner')->toString()],
|
||||
]);
|
||||
|
||||
$users = [
|
||||
['email' => $projectAUserEmail, 'name' => $projectAUserName, 'role' => 'owner', 'projectId' => $projectIdA, 'otherProjectId' => $projectIdB],
|
||||
['email' => $projectBUserEmail, 'name' => $projectBUserName, 'role' => 'owner', 'projectId' => $projectIdB],
|
||||
];
|
||||
|
||||
foreach ($users as $user) {
|
||||
$session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], [
|
||||
'email' => $user['email'],
|
||||
'password' => 'password',
|
||||
]);
|
||||
$token = $session['cookies']['a_session_' . $this->getProject()['$id']];
|
||||
|
||||
// Success: User should be able to delete the project they have membership for.
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/projects/' . $user['projectId'], [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $token,
|
||||
]);
|
||||
$this->assertEquals(204, $response['headers']['status-code']);
|
||||
|
||||
if (!empty($user['otherProjectId'])) {
|
||||
// Failure: User should not be able to delete the project they do not have membership for.
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/projects/' . $user['otherProjectId'], [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $token,
|
||||
]);
|
||||
|
||||
$this->assertEquals(404, $response['headers']['status-code']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ use Tests\E2E\Client;
|
||||
use Tests\E2E\Scopes\ProjectConsole;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\SideClient;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
|
||||
class TeamsConsoleClientTest extends Scope
|
||||
{
|
||||
@@ -252,4 +254,110 @@ class TeamsConsoleClientTest extends Scope
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public function testPerProjectMembership()
|
||||
{
|
||||
// Create team.
|
||||
$team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'teamId' => ID::unique(),
|
||||
'name' => 'Arsenal',
|
||||
'roles' => ['player'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $team['headers']['status-code']);
|
||||
$this->assertNotEmpty($team['body']['$id']);
|
||||
$teamId = $team['body']['$id'];
|
||||
|
||||
// Create user.
|
||||
$user = $this->client->call(Client::METHOD_POST, '/account', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], [
|
||||
'userId' => 'unique()',
|
||||
'email' => uniqid() . 'friend@localhost.test',
|
||||
'password' => 'password',
|
||||
'name' => 'Friend User',
|
||||
], false);
|
||||
|
||||
$this->assertEquals(201, $user['headers']['status-code']);
|
||||
|
||||
// Create project.
|
||||
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'projectId' => ID::unique(),
|
||||
'name' => 'Test Project',
|
||||
'teamId' => $teamId
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $project['headers']['status-code']);
|
||||
$this->assertNotEmpty($project['body']['$id']);
|
||||
$projectId = $project['body']['$id'];
|
||||
|
||||
// Create per-project membership.
|
||||
$response = $this->client->call(Client::METHOD_POST, '/teams/' . $teamId . '/memberships', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'email' => uniqid() . 'friend@localhost.test',
|
||||
'name' => 'Friend User',
|
||||
'roles' => [Role::member('')->toString(), Role::project($projectId, 'owner')->toString()],
|
||||
'url' => 'http://localhost:5000/join-us#title'
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
$membershipId = $response['body']['$id'];
|
||||
$membershipRoles = $response['body']['roles'];
|
||||
$this->assertEquals(2, count($membershipRoles));
|
||||
$this->assertContains(Role::member('')->toString(), $membershipRoles);
|
||||
$this->assertContains(Role::project($projectId, 'owner')->toString(), $membershipRoles);
|
||||
|
||||
// Update the membership to team.
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/teams/' . $teamId . '/memberships/' . $membershipId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'roles' => ['owner'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$membershipRoles = $response['body']['roles'];
|
||||
$this->assertEquals(1, count($membershipRoles));
|
||||
$this->assertContains('owner', $membershipRoles);
|
||||
|
||||
// Again update the membership to project.
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/teams/' . $teamId . '/memberships/' . $membershipId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'roles' => [Role::project($projectId, 'owner')->toString()],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$membershipRoles = $response['body']['roles'];
|
||||
$this->assertEquals(1, count($membershipRoles));
|
||||
$this->assertContains(Role::project($projectId, 'owner')->toString(), $membershipRoles);
|
||||
|
||||
// Delete the membership.
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/teams/' . $teamId . '/memberships/' . $membershipId, array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(204, $response['headers']['status-code']);
|
||||
|
||||
// Cleanup team for other tests to work.
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/teams/' . $teamId, array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(204, $response['headers']['status-code']);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user