Compare commits

...
Author SHA1 Message Date
Hemachandar 8c63755e05 revert utopia version hardcode 2025-11-27 13:24:38 +05:30
Hemachandar 3ee91fd7a3 test fix 2025-11-27 13:15:26 +05:30
Hemachandar 5d78051fb5 lint 2025-11-27 11:33:02 +05:30
Hemachandar 83820a7b0e use 0.33.30 utopia-php/framework.. some issue with 0.33.31 2025-11-26 11:32:29 +05:30
Hemachandar 6cc60ac680 Merge branch '1.8.x' into ser-539 2025-11-26 11:10:52 +05:30
Hemachandar bd6edaa3f2 team member 2025-11-25 12:32:09 +05:30
Hemachandar 4fbf79d815 tests for memberships API 2025-11-23 20:41:25 +05:30
Hemachandar af6ed25e38 simplify tests 2025-11-21 13:15:41 +05:30
Hemachandar 561761b406 tests 2025-11-21 13:15:39 +05:30
Hemachandar 3d00c579a4 backend changes for project-specific permissions 2025-11-21 13:15:13 +05:30
11 changed files with 705 additions and 122 deletions
+4
View File
@@ -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'],
+32 -10
View File
@@ -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
+4 -3
View File
@@ -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'])
+16 -3
View File
@@ -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
View File
@@ -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
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": "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"
}
+9 -9
View File
@@ -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'] ?? []));
}
}
+85
View File
@@ -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;
}
}
+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,
];
}
}
@@ -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']);
}
}