mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Implement project-specific permissions
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -58,7 +59,6 @@ use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Assoc;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
App::post('/v1/teams')
|
||||
->desc('Create team')
|
||||
@@ -483,7 +483,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 +1094,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'])
|
||||
|
||||
@@ -91,6 +91,7 @@ App::init()
|
||||
->inject('authorization')
|
||||
->action(function (App $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
|
||||
$route = $utopia->getRoute();
|
||||
$path = $route->getPath();
|
||||
|
||||
/**
|
||||
* Handle user authentication and session validation.
|
||||
@@ -243,9 +244,24 @@ App::init()
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
$scopes = []; // Reset scope if admin
|
||||
foreach ($adminRoles as $role) {
|
||||
$scopes = \array_merge($scopes, $roles[$role]['scopes']);
|
||||
$teamWideRoles = \array_filter($adminRoles, fn ($role) => !str_starts_with($role, "project-"));
|
||||
foreach ($teamWideRoles as $teamRole) {
|
||||
$scopes = \array_merge($scopes, $roles[$teamRole]['scopes']);
|
||||
}
|
||||
|
||||
$projectId = $project->getId();
|
||||
if ($projectId === 'console') {
|
||||
// Find the true project-id
|
||||
if (str_starts_with($path, "/v1/projects/:projectId")) {
|
||||
$uri = $request->getURI();
|
||||
$projectId = explode('/', $uri)[3];
|
||||
}
|
||||
}
|
||||
|
||||
$projectSpecificRoles = \array_filter($adminRoles, fn ($role) => str_starts_with($role, "project-$projectId"));
|
||||
foreach ($projectSpecificRoles as $projectRole) {
|
||||
$actualRole = explode('-', $projectRole)[2];
|
||||
$scopes = \array_merge($scopes, $roles[$actualRole]['scopes']);
|
||||
}
|
||||
|
||||
$authorization->setDefaultStatus(false); // Cancel security segmentation for admin users.
|
||||
@@ -254,7 +270,7 @@ App::init()
|
||||
$scopes = \array_unique($scopes);
|
||||
|
||||
$authorization->addRole($role);
|
||||
foreach ($user->getRoles($authorization) as $authRole) {
|
||||
foreach ($user->getRoles($authorization, $project->getId(), $path) as $authRole) {
|
||||
$authorization->addRole($authRole);
|
||||
}
|
||||
|
||||
|
||||
+8
-2
@@ -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
+51
-11
@@ -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": "71c0b9c44f7b4d47b3d7517f592374743eb604d6"
|
||||
},
|
||||
"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/71c0b9c44f7b4d47b3d7517f592374743eb604d6",
|
||||
"reference": "71c0b9c44f7b4d47b3d7517f592374743eb604d6",
|
||||
"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-25T15:56:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/detector",
|
||||
@@ -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": {
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Auth\Validator;
|
||||
|
||||
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) in 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 = $value;
|
||||
|
||||
if (str_starts_with($value, "project-")) {
|
||||
$parts = explode("-", $value);
|
||||
if (\count($parts) !== 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$role = $parts[2];
|
||||
}
|
||||
|
||||
if (!\in_array($role, $this->roles)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use Utopia\Auth\Proofs\Token;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Roles;
|
||||
|
||||
class User extends Document
|
||||
@@ -35,7 +36,7 @@ class User extends Document
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getRoles($authorization): array
|
||||
public function getRoles(Authorization $authorization, string $projectId = '', string $path = ''): array
|
||||
{
|
||||
$roles = [];
|
||||
|
||||
@@ -60,19 +61,34 @@ 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();
|
||||
// Role for this membership id.
|
||||
$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();
|
||||
}
|
||||
$nodeRoles = $node['roles'] ?? [];
|
||||
|
||||
if ($projectId !== 'console') {
|
||||
$roles[] = Role::team($node['teamId'])->toString(); // Populate team-wide base role.
|
||||
} else {
|
||||
$teamWideRoles = \array_filter($nodeRoles, fn ($role) => !str_starts_with($role, "project-"));
|
||||
$populateTeamWideRole = !str_starts_with($path, "/v1/projects") || !empty($teamWideRoles);
|
||||
|
||||
if ($populateTeamWideRole) {
|
||||
$roles[] = Role::team($node['teamId'])->toString(); // Populate team-wide base role.
|
||||
}
|
||||
|
||||
$projectSpecificRoles = \array_filter($nodeRoles, fn ($role) => str_starts_with($role, "project-"));
|
||||
foreach ($projectSpecificRoles as $projectRole) {
|
||||
$parts = explode("-", $projectRole);
|
||||
$roles[] = Role::team($node['teamId'], "$parts[0]-$parts[1]")->toString(); // Populate project-wide base role.
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($nodeRoles as $nodeRole) {
|
||||
$roles[] = Role::team($node['teamId'], $nodeRole)->toString(); // Set all team roles
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 testProjectSpecificPermissionsForListProjects(): 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' => ["project-$projectIdA-owner"],
|
||||
]);
|
||||
$this->setupUserMembership([
|
||||
'teamId' => $teamId,
|
||||
'email' => $projectBUserEmail,
|
||||
'name' => $projectBUserName,
|
||||
'roles' => ["project-$projectIdB-owner"],
|
||||
]);
|
||||
|
||||
$users = [
|
||||
['email' => $projectAUserEmail, 'name' => $projectAUserName, 'role' => "project-$projectIdA-owner", 'projectId' => $projectIdA],
|
||||
['email' => $projectBUserEmail, 'name' => $projectBUserName, 'role' => "project-$projectIdB-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 testProjectSpecificPermissionsForUpdateProject(): 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' => ["project-$projectIdA-owner"],
|
||||
]);
|
||||
$this->setupUserMembership([
|
||||
'teamId' => $teamId,
|
||||
'email' => $projectBUserEmail,
|
||||
'name' => $projectBUserName,
|
||||
'roles' => ["project-$projectIdB-owner"],
|
||||
]);
|
||||
|
||||
$users = [
|
||||
['email' => $projectAUserEmail, 'name' => $projectAUserName, 'role' => "project-$projectIdA-owner", 'projectId' => $projectIdA],
|
||||
['email' => $projectBUserEmail, 'name' => $projectBUserName, 'role' => "project-$projectIdB-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(401, $response['headers']['status-code']);
|
||||
}
|
||||
}
|
||||
|
||||
public function testProjectSpecificPermissionsForDeleteProject(): 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' => ["project-$projectIdA-owner"],
|
||||
]);
|
||||
$this->setupUserMembership([
|
||||
'teamId' => $teamId,
|
||||
'email' => $projectBUserEmail,
|
||||
'name' => $projectBUserName,
|
||||
'roles' => ["project-$projectIdB-owner"],
|
||||
]);
|
||||
|
||||
$users = [
|
||||
['email' => $projectAUserEmail, 'name' => $projectAUserName, 'role' => "project-$projectIdA-owner", 'projectId' => $projectIdA, 'otherProjectId' => $projectIdB],
|
||||
['email' => $projectBUserEmail, 'name' => $projectBUserName, 'role' => "project-$projectIdB-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(401, $response['headers']['status-code']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user