List policies API

This commit is contained in:
Matej Bačo
2026-04-23 13:24:39 +02:00
parent c1dfeae323
commit cef7a5197f
15 changed files with 718 additions and 0 deletions
+20
View File
@@ -112,6 +112,16 @@ use Appwrite\Utopia\Response\Model\PlatformLinux;
use Appwrite\Utopia\Response\Model\PlatformList;
use Appwrite\Utopia\Response\Model\PlatformWeb;
use Appwrite\Utopia\Response\Model\PlatformWindows;
use Appwrite\Utopia\Response\Model\PolicyList;
use Appwrite\Utopia\Response\Model\PolicyMembershipPrivacy;
use Appwrite\Utopia\Response\Model\PolicyPasswordDictionary;
use Appwrite\Utopia\Response\Model\PolicyPasswordHistory;
use Appwrite\Utopia\Response\Model\PolicyPasswordPersonalData;
use Appwrite\Utopia\Response\Model\PolicySessionAlert;
use Appwrite\Utopia\Response\Model\PolicySessionDuration;
use Appwrite\Utopia\Response\Model\PolicySessionInvalidation;
use Appwrite\Utopia\Response\Model\PolicySessionLimit;
use Appwrite\Utopia\Response\Model\PolicyUserLimit;
use Appwrite\Utopia\Response\Model\Preferences;
use Appwrite\Utopia\Response\Model\Project;
use Appwrite\Utopia\Response\Model\Provider;
@@ -211,6 +221,7 @@ Response::setModel(new BaseList('Phones List', Response::MODEL_PHONE_LIST, 'phon
Response::setModel(new BaseList('Metric List', Response::MODEL_METRIC_LIST, 'metrics', Response::MODEL_METRIC, true, false));
Response::setModel(new BaseList('Variables List', Response::MODEL_VARIABLE_LIST, 'variables', Response::MODEL_VARIABLE));
Response::setModel(new BaseList('Mock Numbers List', Response::MODEL_MOCK_NUMBER_LIST, 'mockNumbers', Response::MODEL_MOCK_NUMBER));
Response::setModel(new PolicyList());
Response::setModel(new BaseList('Email Templates List', Response::MODEL_EMAIL_TEMPLATE_LIST, 'templates', Response::MODEL_EMAIL_TEMPLATE));
Response::setModel(new BaseList('Status List', Response::MODEL_HEALTH_STATUS_LIST, 'statuses', Response::MODEL_HEALTH_STATUS));
Response::setModel(new BaseList('Rule List', Response::MODEL_PROXY_RULE_LIST, 'rules', Response::MODEL_PROXY_RULE));
@@ -339,6 +350,15 @@ Response::setModel(new Webhook());
Response::setModel(new Key());
Response::setModel(new DevKey());
Response::setModel(new MockNumber());
Response::setModel(new PolicyPasswordDictionary());
Response::setModel(new PolicyPasswordHistory());
Response::setModel(new PolicyPasswordPersonalData());
Response::setModel(new PolicySessionAlert());
Response::setModel(new PolicySessionDuration());
Response::setModel(new PolicySessionInvalidation());
Response::setModel(new PolicySessionLimit());
Response::setModel(new PolicyUserLimit());
Response::setModel(new PolicyMembershipPrivacy());
Response::setModel(new AuthProvider());
Response::setModel(new PlatformWeb());
Response::setModel(new PlatformApple());
@@ -0,0 +1,132 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Policies;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
class XList extends Action
{
use HTTP;
public static function getName()
{
return 'listPolicies';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/project/policies')
->desc('List project policies')
->groups(['api', 'project'])
->label('scope', 'policies.read')
->label('sdk', new Method(
namespace: 'project',
group: 'policies',
name: 'listPolicies',
description: <<<EOT
Get a list of all project policies and their current configuration.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_POLICY_LIST,
)
]
))
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('project')
->callback($this->action(...));
}
/**
* @param array<string> $queries
*/
public function action(
array $queries,
bool $includeTotal,
Response $response,
Document $project,
) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$auths = $project->getAttribute('auths', []);
$policies = [
new Document([
'$id' => 'password-dictionary',
'enabled' => $auths['passwordDictionary'] ?? false,
]),
new Document([
'$id' => 'password-history',
'total' => $auths['passwordHistory'] ?? 0,
]),
new Document([
'$id' => 'password-personal-data',
'enabled' => $auths['personalDataCheck'] ?? false,
]),
new Document([
'$id' => 'session-alert',
'enabled' => $auths['sessionAlerts'] ?? false,
]),
new Document([
'$id' => 'session-duration',
'duration' => $auths['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG,
]),
new Document([
'$id' => 'session-invalidation',
'enabled' => $auths['invalidateSessions'] ?? true,
]),
new Document([
'$id' => 'session-limit',
'total' => $auths['maxSessions'] ?? 0,
]),
new Document([
'$id' => 'user-limit',
'total' => $auths['limit'] ?? 0,
]),
new Document([
'$id' => 'membership-privacy',
'userId' => $auths['membershipsUserId'] ?? false,
'userEmail' => $auths['membershipsUserEmail'] ?? false,
'userPhone' => $auths['membershipsUserPhone'] ?? false,
'userName' => $auths['membershipsUserName'] ?? false,
'userMFA' => $auths['membershipsMfa'] ?? false,
]),
];
$total = $includeTotal ? \count($policies) : 0;
$grouped = Query::groupByType($queries);
$offset = $grouped['offset'] ?? 0;
$limit = $grouped['limit'] ?? null;
$policies = \array_slice($policies, $offset, $limit);
$response->dynamic(new Document([
'policies' => $policies,
'total' => $total,
]), Response::MODEL_POLICY_LIST);
}
}
+10
View File
@@ -255,6 +255,16 @@ class Response extends SwooleResponse
public const MODEL_DEV_KEY_LIST = 'devKeyList';
public const MODEL_MOCK_NUMBER = 'mockNumber';
public const MODEL_MOCK_NUMBER_LIST = 'mockNumberList';
public const MODEL_POLICY_LIST = 'policyList';
public const MODEL_POLICY_PASSWORD_DICTIONARY = 'policyPasswordDictionary';
public const MODEL_POLICY_PASSWORD_HISTORY = 'policyPasswordHistory';
public const MODEL_POLICY_PASSWORD_PERSONAL_DATA = 'policyPasswordPersonalData';
public const MODEL_POLICY_SESSION_ALERT = 'policySessionAlert';
public const MODEL_POLICY_SESSION_DURATION = 'policySessionDuration';
public const MODEL_POLICY_SESSION_INVALIDATION = 'policySessionInvalidation';
public const MODEL_POLICY_SESSION_LIMIT = 'policySessionLimit';
public const MODEL_POLICY_USER_LIMIT = 'policyUserLimit';
public const MODEL_POLICY_MEMBERSHIP_PRIVACY = 'policyMembershipPrivacy';
public const MODEL_AUTH_PROVIDER = 'authProvider';
public const MODEL_AUTH_PROVIDER_LIST = 'authProviderList';
public const MODEL_PLATFORM_APPLE = 'platformApple';
@@ -0,0 +1,19 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response\Model;
abstract class PolicyBase extends Model
{
public function __construct()
{
$this
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'Policy ID.',
'default' => '',
'example' => 'password-dictionary',
]);
}
}
@@ -0,0 +1,46 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class PolicyList extends Model
{
public function __construct()
{
$this
->addRule('total', [
'type' => self::TYPE_INTEGER,
'description' => 'Total number of policies in the given project.',
'default' => 0,
'example' => 9,
])
->addRule('policies', [
'type' => [
Response::MODEL_POLICY_PASSWORD_DICTIONARY,
Response::MODEL_POLICY_PASSWORD_HISTORY,
Response::MODEL_POLICY_PASSWORD_PERSONAL_DATA,
Response::MODEL_POLICY_SESSION_ALERT,
Response::MODEL_POLICY_SESSION_DURATION,
Response::MODEL_POLICY_SESSION_INVALIDATION,
Response::MODEL_POLICY_SESSION_LIMIT,
Response::MODEL_POLICY_USER_LIMIT,
Response::MODEL_POLICY_MEMBERSHIP_PRIVACY,
],
'description' => 'List of policies.',
'default' => [],
'array' => true,
]);
}
public function getName(): string
{
return 'Policies List';
}
public function getType(): string
{
return Response::MODEL_POLICY_LIST;
}
}
@@ -0,0 +1,59 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class PolicyMembershipPrivacy extends PolicyBase
{
public array $conditions = [
'$id' => 'membership-privacy',
];
public function __construct()
{
parent::__construct();
$this
->addRule('userId', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Whether user ID is visible in memberships.',
'default' => false,
'example' => true,
])
->addRule('userEmail', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Whether user email is visible in memberships.',
'default' => false,
'example' => true,
])
->addRule('userPhone', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Whether user phone is visible in memberships.',
'default' => false,
'example' => true,
])
->addRule('userName', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Whether user name is visible in memberships.',
'default' => false,
'example' => true,
])
->addRule('userMFA', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Whether user MFA status is visible in memberships.',
'default' => false,
'example' => true,
]);
}
public function getName(): string
{
return 'Policy Membership Privacy';
}
public function getType(): string
{
return Response::MODEL_POLICY_MEMBERSHIP_PRIVACY;
}
}
@@ -0,0 +1,34 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class PolicyPasswordDictionary extends PolicyBase
{
public array $conditions = [
'$id' => 'password-dictionary',
];
public function __construct()
{
parent::__construct();
$this->addRule('enabled', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Whether password dictionary policy is enabled.',
'default' => false,
'example' => true,
]);
}
public function getName(): string
{
return 'Policy Password Dictionary';
}
public function getType(): string
{
return Response::MODEL_POLICY_PASSWORD_DICTIONARY;
}
}
@@ -0,0 +1,34 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class PolicyPasswordHistory extends PolicyBase
{
public array $conditions = [
'$id' => 'password-history',
];
public function __construct()
{
parent::__construct();
$this->addRule('total', [
'type' => self::TYPE_INTEGER,
'description' => 'Password history length. A value of 0 means the policy is disabled.',
'default' => 0,
'example' => 5,
]);
}
public function getName(): string
{
return 'Policy Password History';
}
public function getType(): string
{
return Response::MODEL_POLICY_PASSWORD_HISTORY;
}
}
@@ -0,0 +1,34 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class PolicyPasswordPersonalData extends PolicyBase
{
public array $conditions = [
'$id' => 'password-personal-data',
];
public function __construct()
{
parent::__construct();
$this->addRule('enabled', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Whether password personal data policy is enabled.',
'default' => false,
'example' => true,
]);
}
public function getName(): string
{
return 'Policy Password Personal Data';
}
public function getType(): string
{
return Response::MODEL_POLICY_PASSWORD_PERSONAL_DATA;
}
}
@@ -0,0 +1,34 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class PolicySessionAlert extends PolicyBase
{
public array $conditions = [
'$id' => 'session-alert',
];
public function __construct()
{
parent::__construct();
$this->addRule('enabled', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Whether session alert policy is enabled.',
'default' => false,
'example' => true,
]);
}
public function getName(): string
{
return 'Policy Session Alert';
}
public function getType(): string
{
return Response::MODEL_POLICY_SESSION_ALERT;
}
}
@@ -0,0 +1,34 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class PolicySessionDuration extends PolicyBase
{
public array $conditions = [
'$id' => 'session-duration',
];
public function __construct()
{
parent::__construct();
$this->addRule('duration', [
'type' => self::TYPE_INTEGER,
'description' => 'Session duration in seconds.',
'default' => TOKEN_EXPIRATION_LOGIN_LONG,
'example' => 3600,
]);
}
public function getName(): string
{
return 'Policy Session Duration';
}
public function getType(): string
{
return Response::MODEL_POLICY_SESSION_DURATION;
}
}
@@ -0,0 +1,34 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class PolicySessionInvalidation extends PolicyBase
{
public array $conditions = [
'$id' => 'session-invalidation',
];
public function __construct()
{
parent::__construct();
$this->addRule('enabled', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Whether session invalidation policy is enabled.',
'default' => true,
'example' => true,
]);
}
public function getName(): string
{
return 'Policy Session Invalidation';
}
public function getType(): string
{
return Response::MODEL_POLICY_SESSION_INVALIDATION;
}
}
@@ -0,0 +1,34 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class PolicySessionLimit extends PolicyBase
{
public array $conditions = [
'$id' => 'session-limit',
];
public function __construct()
{
parent::__construct();
$this->addRule('total', [
'type' => self::TYPE_INTEGER,
'description' => 'Maximum number of sessions allowed per user. A value of 0 means the policy is disabled.',
'default' => 0,
'example' => 10,
]);
}
public function getName(): string
{
return 'Policy Session Limit';
}
public function getType(): string
{
return Response::MODEL_POLICY_SESSION_LIMIT;
}
}
@@ -0,0 +1,34 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class PolicyUserLimit extends PolicyBase
{
public array $conditions = [
'$id' => 'user-limit',
];
public function __construct()
{
parent::__construct();
$this->addRule('total', [
'type' => self::TYPE_INTEGER,
'description' => 'Maximum number of users allowed in the project. A value of 0 means the policy is disabled.',
'default' => 0,
'example' => 100,
]);
}
public function getName(): string
{
return 'Policy User Limit';
}
public function getType(): string
{
return Response::MODEL_POLICY_USER_LIMIT;
}
}
+160
View File
@@ -3,9 +3,154 @@
namespace Tests\E2E\Services\Project;
use Tests\E2E\Client;
use Utopia\Database\Query;
trait PoliciesBase
{
// =========================================================================
// List Policies
// =========================================================================
public function testListPolicies(): void
{
$response = $this->listPolicies();
$this->assertSame(200, $response['headers']['status-code']);
$this->assertArrayHasKey('policies', $response['body']);
$this->assertArrayHasKey('total', $response['body']);
$this->assertIsArray($response['body']['policies']);
$this->assertIsInt($response['body']['total']);
$this->assertSame(9, $response['body']['total']);
$this->assertCount(9, $response['body']['policies']);
$policyIds = \array_column($response['body']['policies'], '$id');
$this->assertContains('password-dictionary', $policyIds);
$this->assertContains('password-history', $policyIds);
$this->assertContains('password-personal-data', $policyIds);
$this->assertContains('session-alert', $policyIds);
$this->assertContains('session-duration', $policyIds);
$this->assertContains('session-invalidation', $policyIds);
$this->assertContains('session-limit', $policyIds);
$this->assertContains('user-limit', $policyIds);
$this->assertContains('membership-privacy', $policyIds);
}
public function testListPoliciesResponseModel(): void
{
$response = $this->listPolicies();
$this->assertSame(200, $response['headers']['status-code']);
foreach ($response['body']['policies'] as $policy) {
$this->assertArrayHasKey('$id', $policy);
}
$byId = [];
foreach ($response['body']['policies'] as $policy) {
$byId[$policy['$id']] = $policy;
}
$this->assertArrayHasKey('enabled', $byId['password-dictionary']);
$this->assertArrayHasKey('total', $byId['password-history']);
$this->assertArrayHasKey('enabled', $byId['password-personal-data']);
$this->assertArrayHasKey('enabled', $byId['session-alert']);
$this->assertArrayHasKey('duration', $byId['session-duration']);
$this->assertArrayHasKey('enabled', $byId['session-invalidation']);
$this->assertArrayHasKey('total', $byId['session-limit']);
$this->assertArrayHasKey('total', $byId['user-limit']);
$this->assertArrayHasKey('userId', $byId['membership-privacy']);
$this->assertArrayHasKey('userEmail', $byId['membership-privacy']);
$this->assertArrayHasKey('userPhone', $byId['membership-privacy']);
$this->assertArrayHasKey('userName', $byId['membership-privacy']);
$this->assertArrayHasKey('userMFA', $byId['membership-privacy']);
}
public function testListPoliciesReflectsUpdates(): void
{
$this->updatePasswordDictionaryPolicy(true);
$this->updatePasswordHistoryPolicy(5);
$this->updateSessionDurationPolicy(3600);
$this->updateMembershipPrivacyPolicy([
'userId' => true,
'userEmail' => true,
'userPhone' => false,
'userName' => true,
'userMFA' => true,
]);
$response = $this->listPolicies();
$this->assertSame(200, $response['headers']['status-code']);
$byId = [];
foreach ($response['body']['policies'] as $policy) {
$byId[$policy['$id']] = $policy;
}
$this->assertSame(true, $byId['password-dictionary']['enabled']);
$this->assertSame(5, $byId['password-history']['total']);
$this->assertSame(3600, $byId['session-duration']['duration']);
$this->assertSame(true, $byId['membership-privacy']['userId']);
$this->assertSame(true, $byId['membership-privacy']['userEmail']);
$this->assertSame(false, $byId['membership-privacy']['userPhone']);
$this->assertSame(true, $byId['membership-privacy']['userName']);
$this->assertSame(true, $byId['membership-privacy']['userMFA']);
// Cleanup
$this->updatePasswordDictionaryPolicy(false);
$this->updatePasswordHistoryPolicy(null);
$this->updateSessionDurationPolicy(31536000);
$this->updateMembershipPrivacyPolicy([
'userId' => false,
'userEmail' => false,
'userPhone' => false,
'userName' => false,
'userMFA' => false,
]);
}
public function testListPoliciesTotalFalse(): void
{
$response = $this->listPolicies(total: false);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertSame(0, $response['body']['total']);
$this->assertCount(9, $response['body']['policies']);
}
public function testListPoliciesWithLimit(): void
{
$response = $this->listPolicies([
Query::limit(1)->toString(),
]);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertCount(1, $response['body']['policies']);
$this->assertSame(9, $response['body']['total']);
}
public function testListPoliciesWithOffset(): void
{
$listAll = $this->listPolicies();
$this->assertSame(200, $listAll['headers']['status-code']);
$listOffset = $this->listPolicies([
Query::offset(1)->toString(),
]);
$this->assertSame(200, $listOffset['headers']['status-code']);
$this->assertCount(\count($listAll['body']['policies']) - 1, $listOffset['body']['policies']);
$this->assertSame($listAll['body']['total'], $listOffset['body']['total']);
}
public function testListPoliciesWithoutAuthentication(): void
{
$response = $this->listPolicies(authenticated: false);
$this->assertSame(401, $response['headers']['status-code']);
}
// =========================================================================
// Password Dictionary Policy
// =========================================================================
@@ -842,6 +987,21 @@ trait PoliciesBase
]);
}
protected function listPolicies(?array $queries = null, ?bool $total = null, bool $authenticated = true): mixed
{
$params = [];
if ($queries !== null) {
$params['queries'] = $queries;
}
if ($total !== null) {
$params['total'] = $total;
}
return $this->client->call(Client::METHOD_GET, '/project/policies', $this->buildHeaders($authenticated), $params);
}
protected function updatePasswordDictionaryPolicy(bool $enabled, bool $authenticated = true): mixed
{
return $this->client->call(Client::METHOD_PATCH, '/project/policies/password-dictionary', $this->buildHeaders($authenticated), [