Base implementation

This commit is contained in:
Hemachandar
2026-01-22 13:58:22 +05:30
parent cbb5963b29
commit bb7126ef0a
9 changed files with 529 additions and 41 deletions
+14
View File
@@ -175,11 +175,18 @@ App::post('/v1/projects')
$project = $dbForPlatform->createDocument('projects', new Document([
'$id' => $projectId,
'$permissions' => [
// Team wide 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')),
// Project-specific permissions
Permission::read(Role::team(ID::custom($teamId), "project-$projectId")),
Permission::update(Role::team(ID::custom($teamId), "project-$projectId-owner")),
Permission::update(Role::team(ID::custom($teamId), "project-$projectId-developer")),
Permission::delete(Role::team(ID::custom($teamId), "project-$projectId-owner")),
Permission::delete(Role::team(ID::custom($teamId), "project-$projectId-developer")),
],
'name' => $name,
'teamInternalId' => $team->getSequence(),
@@ -428,11 +435,18 @@ App::patch('/v1/projects/:projectId/team')
}
$permissions = [
// Team wide 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')),
// Project-specific permissions
Permission::read(Role::team(ID::custom($teamId), "project-$projectId")),
Permission::update(Role::team(ID::custom($teamId), "project-$projectId-owner")),
Permission::update(Role::team(ID::custom($teamId), "project-$projectId-developer")),
Permission::delete(Role::team(ID::custom($teamId), "project-$projectId-owner")),
Permission::delete(Role::team(ID::custom($teamId), "project-$projectId-developer")),
];
$project
+4 -2
View File
@@ -2,6 +2,7 @@
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;
@@ -100,6 +101,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')),
],
@@ -483,7 +485,7 @@ App::post('/v1/teams/:teamId/memberships')
$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);
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'])
@@ -1094,7 +1096,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
$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);
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'])
+23 -2
View File
@@ -244,8 +244,29 @@ App::init()
}
$scopes = []; // Reset scope if admin
foreach ($adminRoles as $role) {
$scopes = \array_merge($scopes, $roles[$role]['scopes']);
// Populate scopes from team wide roles
$teamWideRoles = \array_filter($adminRoles, fn ($role) => !str_starts_with($role, "project-"));
if (!empty($teamWideRoles)) {
foreach ($teamWideRoles as $role) {
$scopes = \array_merge($scopes, $roles[$role]['scopes']);
}
} else {
$scopes = \array_merge($scopes, $roles[User::ROLE_USERS]['scopes']);
}
// Populate scopes from project-specific roles
if ($project->getId() !== 'console') {
$projectId = $project->getId();
$projectRoles = \array_filter($adminRoles, fn ($role) => str_starts_with($role, "project-{$projectId}"));
foreach ($projectRoles as $role) {
$parts = \explode('-', $role);
if (\count($parts) === 3) {
$role = $parts[2];
$scopes = \array_merge($scopes, $roles[$role]['scopes']);
}
}
}
$authorization->setDefaultStatus(false); // Cancel security segmentation for admin users.
+8 -2
View File
@@ -52,7 +52,7 @@
"utopia-php/cache": "0.13.*",
"utopia-php/cli": "0.15.*",
"utopia-php/config": "1.*",
"utopia-php/database": "4.*",
"utopia-php/database": "dev-ser-541-tag-4.5.2 as 4.0.99",
"utopia-php/detector": "0.2.*",
"utopia-php/domains": "0.11.*",
"utopia-php/emails": "0.6.*",
@@ -108,5 +108,11 @@
"php-http/discovery": true,
"tbachert/spi": true
}
}
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/utopia-php/database"
}
]
}
Generated
+58 -18
View File
@@ -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": "33da844fdf5648d1d1a027dfb6ae42bc",
"content-hash": "6df869889de657693cbd12e2cf4cf781",
"packages": [
{
"name": "adhocore/jwt",
@@ -3899,16 +3899,16 @@
},
{
"name": "utopia-php/database",
"version": "4.5.2",
"version": "dev-ser-541-tag-4.5.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "8e6a033d4da09a2f2ac1f79fd85fcfa2da018d23"
"reference": "a074126df965d3c828c0234fa4f38cb8dab836bd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/8e6a033d4da09a2f2ac1f79fd85fcfa2da018d23",
"reference": "8e6a033d4da09a2f2ac1f79fd85fcfa2da018d23",
"url": "https://api.github.com/repos/utopia-php/database/zipball/a074126df965d3c828c0234fa4f38cb8dab836bd",
"reference": "a074126df965d3c828c0234fa4f38cb8dab836bd",
"shasum": ""
},
"require": {
@@ -3937,7 +3937,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"
],
@@ -3950,10 +3981,10 @@
"utopia"
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/4.5.2"
"source": "https://github.com/utopia-php/database/tree/ser-541-tag-4.5.2",
"issues": "https://github.com/utopia-php/database/issues"
},
"time": "2026-01-15T04:23:30+00:00"
"time": "2026-01-20T08:22:15+00:00"
},
{
"name": "utopia-php/detector",
@@ -4516,16 +4547,16 @@
},
{
"name": "utopia-php/migration",
"version": "1.4.4",
"version": "1.4.5",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/migration.git",
"reference": "3fe751902012d09d323420cd3523be1ed855e868"
"reference": "ed2b8f3778add4728c518c2a41be4ff2bd68f018"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/3fe751902012d09d323420cd3523be1ed855e868",
"reference": "3fe751902012d09d323420cd3523be1ed855e868",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/ed2b8f3778add4728c518c2a41be4ff2bd68f018",
"reference": "ed2b8f3778add4728c518c2a41be4ff2bd68f018",
"shasum": ""
},
"require": {
@@ -4565,9 +4596,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/migration/issues",
"source": "https://github.com/utopia-php/migration/tree/1.4.4"
"source": "https://github.com/utopia-php/migration/tree/1.4.5"
},
"time": "2026-01-16T10:00:07+00:00"
"time": "2026-01-20T06:17:26+00:00"
},
{
"name": "utopia-php/mongo",
@@ -8986,9 +9017,18 @@
"time": "2024-03-07T20:33:40+00:00"
}
],
"aliases": [],
"aliases": [
{
"package": "utopia-php/database",
"version": "dev-ser-541-tag-4.5.2",
"alias": "4.0.99",
"alias_normalized": "4.0.99.0"
}
],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {
"utopia-php/database": 20
},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
@@ -9012,5 +9052,5 @@
"platform-overrides": {
"php": "8.3"
},
"plugin-api-version": "2.2.0"
"plugin-api-version": "2.6.0"
}
+84
View File
@@ -0,0 +1,84 @@
<?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;
}
// TODO: Implement role validation
return true;
}
}
@@ -60,17 +60,35 @@ class User extends Document
}
foreach ($this->getAttribute('memberships', []) as $node) {
if (!isset($node['confirm']) || !$node['confirm']) {
if (!isset($node['confirm']) || !$node['confirm'] || !isset($node['$id']) || !isset($node['teamId'])) {
continue;
}
if (isset($node['$id']) && isset($node['teamId'])) {
$roles[] = Role::team($node['teamId'])->toString();
$roles[] = Role::member($node['$id'])->toString();
$nodeRoles = $node['roles'] ?? [];
if (isset($node['roles'])) {
foreach ($node['roles'] as $nodeRole) { // Set all team roles
$roles[] = Role::team($node['teamId'], $nodeRole)->toString();
// Add role for this membership.
$roles[] = Role::member($node['$id'])->toString();
// Add all roles in the team.
$allRoles = \array_map(fn ($role) => Role::team($node['teamId'], $role)->toString(), $nodeRoles);
$roles = \array_merge($roles, $allRoles);
// Add base team-wide role.
$teamWideRoles = \array_filter($nodeRoles, fn ($role) => !str_starts_with($role, "project-"));
if (!empty($teamWideRoles)) {
$roles[] = Role::team($node['teamId'])->toString();
} else {
$roles[] = Role::team($node['teamId'], self::ROLE_MEMBER)->toString();
}
// Add base project-wide roles.
$projectRoles = \array_filter($nodeRoles, fn ($role) => str_starts_with($role, "project-"));
if (!empty($projectRoles)) {
foreach ($projectRoles as $role) {
$parts = \explode('-', $role);
if (\count($parts) === 3) {
$projectId = $parts[1];
$roles[] = Role::team($node['teamId'], "project-{$projectId}")->toString(); // Add base project-wide role
}
}
}
+103 -10
View File
@@ -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,
];
}
}
@@ -5594,4 +5594,214 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(204, $response['headers']['status-code']);
}
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']);
}
}
}
}