mirror of
https://github.com/appwrite/appwrite
synced 2026-05-23 08:58:35 +00:00
commit
aa43902d32
11 changed files with 542 additions and 56 deletions
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Attribute;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
|
|
@ -88,6 +89,9 @@ class Decrement extends Action
|
|||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $min, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, array $plan): void
|
||||
{
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
|
|
@ -100,7 +104,9 @@ class Decrement extends Action
|
|||
|
||||
// Handle transaction staging
|
||||
if ($transactionId !== null) {
|
||||
$transaction = $dbForProject->getDocument('transactions', $transactionId);
|
||||
$transaction = ($isAPIKey || $isPrivilegedUser)
|
||||
? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
|
||||
: $dbForProject->getDocument('transactions', $transactionId);
|
||||
if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Attribute;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
|
|
@ -88,6 +89,9 @@ class Increment extends Action
|
|||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, array $plan): void
|
||||
{
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
|
|
@ -100,7 +104,9 @@ class Increment extends Action
|
|||
|
||||
// Handle transaction staging
|
||||
if ($transactionId !== null) {
|
||||
$transaction = $dbForProject->getDocument('transactions', $transactionId);
|
||||
$transaction = ($isAPIKey || $isPrivilegedUser)
|
||||
? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
|
||||
: $dbForProject->getDocument('transactions', $transactionId);
|
||||
if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -367,7 +367,9 @@ class Create extends Action
|
|||
|
||||
// Handle transaction staging
|
||||
if ($transactionId !== null) {
|
||||
$transaction = $dbForProject->getDocument('transactions', $transactionId);
|
||||
$transaction = ($isAPIKey || $isPrivilegedUser)
|
||||
? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
|
||||
: $dbForProject->getDocument('transactions', $transactionId);
|
||||
if ($transaction->isEmpty()) {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,7 +129,9 @@ class Delete extends Action
|
|||
|
||||
// Handle transaction staging
|
||||
if ($transactionId !== null) {
|
||||
$transaction = $dbForProject->getDocument('transactions', $transactionId);
|
||||
$transaction = ($isAPIKey || $isPrivilegedUser)
|
||||
? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
|
||||
: $dbForProject->getDocument('transactions', $transactionId);
|
||||
if ($transaction->isEmpty()) {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -244,7 +244,9 @@ class Update extends Action
|
|||
|
||||
// Handle transaction staging
|
||||
if ($transactionId !== null) {
|
||||
$transaction = $dbForProject->getDocument('transactions', $transactionId);
|
||||
$transaction = ($isAPIKey || $isPrivilegedUser)
|
||||
? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
|
||||
: $dbForProject->getDocument('transactions', $transactionId);
|
||||
if ($transaction->isEmpty()) {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -253,7 +253,9 @@ class Upsert extends Action
|
|||
|
||||
// Handle transaction staging
|
||||
if ($transactionId !== null) {
|
||||
$transaction = $dbForProject->getDocument('transactions', $transactionId);
|
||||
$transaction = ($isAPIKey || $isPrivilegedUser)
|
||||
? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
|
||||
: $dbForProject->getDocument('transactions', $transactionId);
|
||||
if ($transaction->isEmpty()) {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ use Utopia\Database\Database;
|
|||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Validator\Range;
|
||||
|
|
@ -53,13 +54,28 @@ class Create extends Action
|
|||
->param('ttl', APP_DATABASE_TXN_TTL_DEFAULT, new Range(min: APP_DATABASE_TXN_TTL_MIN, max: APP_DATABASE_TXN_TTL_MAX), 'Seconds before the transaction expires.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(int $ttl, UtopiaResponse $response, Database $dbForProject): void
|
||||
public function action(int $ttl, UtopiaResponse $response, Database $dbForProject, Document $user): void
|
||||
{
|
||||
$permissions = [];
|
||||
if (!empty($user->getId())) {
|
||||
$allowedPermissions = [
|
||||
Database::PERMISSION_READ,
|
||||
Database::PERMISSION_UPDATE,
|
||||
Database::PERMISSION_DELETE,
|
||||
];
|
||||
|
||||
foreach ($allowedPermissions as $permission) {
|
||||
$permissions[] = (new Permission($permission, 'user', $user->getId()))->toString();
|
||||
}
|
||||
}
|
||||
|
||||
$transaction = Authorization::skip(fn () => $dbForProject->createDocument('transactions', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'$permissions' => $permissions,
|
||||
'status' => 'pending',
|
||||
'operations' => 0,
|
||||
'expiresAt' => DateTime::addSeconds(new \DateTime(), $ttl),
|
||||
|
|
|
|||
|
|
@ -72,8 +72,17 @@ class Create extends Action
|
|||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Operations array cannot be empty');
|
||||
}
|
||||
|
||||
$transaction = Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId));
|
||||
if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') {
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
|
||||
// API keys and admins can read any transaction, regular users need permissions
|
||||
$transaction = ($isAPIKey || $isPrivilegedUser)
|
||||
? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
|
||||
: $dbForProject->getDocument('transactions', $transactionId);
|
||||
if ($transaction->isEmpty()) {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND);
|
||||
}
|
||||
if ($transaction->getAttribute('status', '') !== 'pending') {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction');
|
||||
}
|
||||
|
||||
|
|
@ -93,9 +102,6 @@ class Create extends Action
|
|||
);
|
||||
}
|
||||
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
|
||||
$databases = $collections = $staged = $dependants = [];
|
||||
foreach ($operations as $operation) {
|
||||
if (!$isAPIKey && !$isPrivilegedUser && \in_array($operation['action'], [
|
||||
|
|
@ -146,54 +152,58 @@ class Create extends Action
|
|||
}
|
||||
}
|
||||
|
||||
$permissionType = match ($operation['action']) {
|
||||
'create', 'bulkCreate' => Database::PERMISSION_CREATE,
|
||||
'update', 'bulkUpdate', 'increment', 'decrement' => Database::PERMISSION_UPDATE,
|
||||
'delete', 'bulkDelete' => Database::PERMISSION_DELETE,
|
||||
'upsert', 'bulkUpsert' => ($document && !$document->isEmpty()) ? Database::PERMISSION_UPDATE : Database::PERMISSION_CREATE,
|
||||
default => throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid action: ' . $operation['action'])
|
||||
};
|
||||
// Bulk operations skip permission validation entirely (API key/admin only, already checked above)
|
||||
if (!\in_array($operation['action'], ['bulkCreate', 'bulkUpdate', 'bulkUpsert', 'bulkDelete'])) {
|
||||
$permissionType = match ($operation['action']) {
|
||||
'create' => Database::PERMISSION_CREATE,
|
||||
'update', 'increment', 'decrement' => Database::PERMISSION_UPDATE,
|
||||
'delete' => Database::PERMISSION_DELETE,
|
||||
'upsert' => ($document && !$document->isEmpty()) ? Database::PERMISSION_UPDATE : Database::PERMISSION_CREATE,
|
||||
default => throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid action: ' . $operation['action'])
|
||||
};
|
||||
|
||||
if (!$isAPIKey && !$isPrivilegedUser) {
|
||||
$documentSecurity = $collection->getAttribute('documentSecurity', false);
|
||||
$validator = new Authorization($permissionType);
|
||||
$collectionValid = $validator->isValid($collection->getPermissionsByType($permissionType));
|
||||
$documentValid = false;
|
||||
if ($document !== null && !$document->isEmpty() && $documentSecurity) {
|
||||
if ($permissionType === Database::PERMISSION_UPDATE) {
|
||||
$documentValid = $validator->isValid($document->getUpdate());
|
||||
} elseif ($permissionType === Database::PERMISSION_DELETE) {
|
||||
$documentValid = $validator->isValid($document->getDelete());
|
||||
// For individual operations, enforce permissions unless using API key/admin
|
||||
if (!$isAPIKey && !$isPrivilegedUser) {
|
||||
$documentSecurity = $collection->getAttribute('documentSecurity', false);
|
||||
$validator = new Authorization($permissionType);
|
||||
$collectionValid = $validator->isValid($collection->getPermissionsByType($permissionType));
|
||||
$documentValid = false;
|
||||
if ($document !== null && !$document->isEmpty() && $documentSecurity) {
|
||||
if ($permissionType === Database::PERMISSION_UPDATE) {
|
||||
$documentValid = $validator->isValid($document->getUpdate());
|
||||
} elseif ($permissionType === Database::PERMISSION_DELETE) {
|
||||
$documentValid = $validator->isValid($document->getDelete());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($permissionType === Database::PERMISSION_CREATE || !$documentSecurity) {
|
||||
if (!$collectionValid) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
if ($permissionType === Database::PERMISSION_CREATE || !$documentSecurity) {
|
||||
if (!$collectionValid) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
} else {
|
||||
if (!$collectionValid && !$documentValid) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!$collectionValid && !$documentValid) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
// Users can only set permissions for roles they have
|
||||
if (isset($operation['data']['$permissions'])) {
|
||||
$permissions = $operation['data']['$permissions'];
|
||||
$roles = Authorization::getRoles();
|
||||
foreach (Database::PERMISSIONS as $type) {
|
||||
foreach ($permissions as $permission) {
|
||||
$permission = Permission::parse($permission);
|
||||
if ($permission->getPermission() != $type) {
|
||||
continue;
|
||||
}
|
||||
$role = (new Role(
|
||||
$permission->getRole(),
|
||||
$permission->getIdentifier(),
|
||||
$permission->getDimension()
|
||||
))->toString();
|
||||
if (!Authorization::isRole($role)) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')');
|
||||
// Users can only set permissions for roles they have
|
||||
if (isset($operation['data']['$permissions'])) {
|
||||
$permissions = $operation['data']['$permissions'];
|
||||
$roles = Authorization::getRoles();
|
||||
foreach (Database::PERMISSIONS as $type) {
|
||||
foreach ($permissions as $permission) {
|
||||
$permission = Permission::parse($permission);
|
||||
if ($permission->getPermission() != $type) {
|
||||
continue;
|
||||
}
|
||||
$role = (new Role(
|
||||
$permission->getRole(),
|
||||
$permission->getIdentifier(),
|
||||
$permission->getDimension()
|
||||
))->toString();
|
||||
if (!Authorization::isRole($role)) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Databases\TransactionState;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
|
|
@ -110,7 +111,12 @@ class Update extends Action
|
|||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Cannot commit and rollback at the same time');
|
||||
}
|
||||
|
||||
$transaction = Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId));
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
|
||||
$transaction = ($isAPIKey || $isPrivilegedUser)
|
||||
? Authorization::skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
|
||||
: $dbForProject->getDocument('transactions', $transactionId);
|
||||
if ($transaction->isEmpty()) {
|
||||
throw new Exception(Exception::TRANSACTION_NOT_FOUND);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ class Create extends TransactionsCreate
|
|||
->param('ttl', APP_DATABASE_TXN_TTL_DEFAULT, new Range(min: APP_DATABASE_TXN_TTL_MIN, max: APP_DATABASE_TXN_TTL_MAX), 'Seconds before the transaction expires.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -780,4 +780,437 @@ trait PermissionsBase
|
|||
|
||||
$this->assertEquals(200, $rollback['headers']['status-code']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that one user cannot read another user's transaction
|
||||
*/
|
||||
public function testUserCannotReadAnotherUsersTransaction(): void
|
||||
{
|
||||
// Create user 1 (fresh) and their transaction
|
||||
$user1 = $this->getUser(true);
|
||||
$user1Headers = [
|
||||
'origin' => 'http://localhost',
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $user1['session'],
|
||||
];
|
||||
|
||||
$transaction1 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $user1Headers));
|
||||
|
||||
$this->assertEquals(201, $transaction1['headers']['status-code']);
|
||||
$transactionId1 = $transaction1['body']['$id'];
|
||||
|
||||
// Create user 2 (fresh)
|
||||
$user2 = $this->getUser(true); // Fresh user
|
||||
$user2Headers = [
|
||||
'origin' => 'http://localhost',
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $user2['session'],
|
||||
];
|
||||
|
||||
// User 2 tries to read User 1's transaction - should fail
|
||||
$readAttempt = $this->client->call(Client::METHOD_GET, '/tablesdb/transactions/' . $transactionId1, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $user2Headers));
|
||||
|
||||
// This should fail with 404 Not Found (transaction doesn't exist for this user)
|
||||
$this->assertEquals(404, $readAttempt['headers']['status-code']);
|
||||
|
||||
// Verify User 1 can still read their own transaction
|
||||
$readOwn = $this->client->call(Client::METHOD_GET, '/tablesdb/transactions/' . $transactionId1, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $user1Headers));
|
||||
|
||||
$this->assertEquals(200, $readOwn['headers']['status-code']);
|
||||
$this->assertEquals($transactionId1, $readOwn['body']['$id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that one user cannot list another user's transactions
|
||||
*/
|
||||
public function testUserCannotListAnotherUsersTransactions(): void
|
||||
{
|
||||
// Create user 1 (fresh) with transactions
|
||||
$user1 = $this->getUser(true);
|
||||
$user1Headers = [
|
||||
'origin' => 'http://localhost',
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $user1['session'],
|
||||
];
|
||||
|
||||
$transaction1 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $user1Headers));
|
||||
|
||||
$this->assertEquals(201, $transaction1['headers']['status-code']);
|
||||
|
||||
$transaction2 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $user1Headers));
|
||||
|
||||
$this->assertEquals(201, $transaction2['headers']['status-code']);
|
||||
|
||||
// Create user 2 (fresh) with their own transaction
|
||||
$user2 = $this->getUser(true); // Fresh user
|
||||
$user2Headers = [
|
||||
'origin' => 'http://localhost',
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $user2['session'],
|
||||
];
|
||||
|
||||
$transaction3 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $user2Headers));
|
||||
|
||||
$this->assertEquals(201, $transaction3['headers']['status-code']);
|
||||
|
||||
// User 2 lists transactions - should only see their own
|
||||
$listUser2 = $this->client->call(Client::METHOD_GET, '/tablesdb/transactions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $user2Headers));
|
||||
|
||||
$this->assertEquals(200, $listUser2['headers']['status-code']);
|
||||
$this->assertEquals(1, $listUser2['body']['total']);
|
||||
$this->assertEquals($transaction3['body']['$id'], $listUser2['body']['transactions'][0]['$id']);
|
||||
|
||||
// User 1 lists transactions - should only see their own (2 transactions)
|
||||
$listUser1 = $this->client->call(Client::METHOD_GET, '/tablesdb/transactions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $user1Headers));
|
||||
|
||||
$this->assertEquals(200, $listUser1['headers']['status-code']);
|
||||
$this->assertEquals(2, $listUser1['body']['total']);
|
||||
|
||||
// Verify neither of user1's transactions appear in user2's list
|
||||
$user2TransactionIds = array_column($listUser2['body']['transactions'], '$id');
|
||||
$this->assertNotContains($transaction1['body']['$id'], $user2TransactionIds);
|
||||
$this->assertNotContains($transaction2['body']['$id'], $user2TransactionIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that one user cannot update another user's transaction
|
||||
*/
|
||||
public function testUserCannotUpdateAnotherUsersTransaction(): void
|
||||
{
|
||||
// Create user 1 (fresh) and their transaction
|
||||
$user1 = $this->getUser(true);
|
||||
$user1Headers = [
|
||||
'origin' => 'http://localhost',
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $user1['session'],
|
||||
];
|
||||
|
||||
$transaction1 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $user1Headers));
|
||||
|
||||
$this->assertEquals(201, $transaction1['headers']['status-code']);
|
||||
$transactionId1 = $transaction1['body']['$id'];
|
||||
|
||||
// Create user 2 (fresh)
|
||||
$user2 = $this->getUser(true); // Fresh user
|
||||
$user2Headers = [
|
||||
'origin' => 'http://localhost',
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $user2['session'],
|
||||
];
|
||||
|
||||
// User 2 tries to commit User 1's transaction - should fail
|
||||
$commitAttempt = $this->client->call(Client::METHOD_PATCH, '/tablesdb/transactions/' . $transactionId1, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $user2Headers), [
|
||||
'commit' => true,
|
||||
]);
|
||||
|
||||
// This should fail with 404 Not Found
|
||||
$this->assertEquals(404, $commitAttempt['headers']['status-code']);
|
||||
|
||||
// User 2 tries to rollback User 1's transaction - should also fail
|
||||
$rollbackAttempt = $this->client->call(Client::METHOD_PATCH, '/tablesdb/transactions/' . $transactionId1, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $user2Headers), [
|
||||
'rollback' => true,
|
||||
]);
|
||||
|
||||
// This should also fail with 404 Not Found
|
||||
$this->assertEquals(404, $rollbackAttempt['headers']['status-code']);
|
||||
|
||||
// Verify User 1 can still commit their own transaction
|
||||
$commitOwn = $this->client->call(Client::METHOD_PATCH, '/tablesdb/transactions/' . $transactionId1, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $user1Headers), [
|
||||
'commit' => true,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $commitOwn['headers']['status-code']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that one user cannot delete another user's transaction
|
||||
*/
|
||||
public function testUserCannotDeleteAnotherUsersTransaction(): void
|
||||
{
|
||||
// Create user 1 (fresh) and their transaction
|
||||
$user1 = $this->getUser(true);
|
||||
$user1Headers = [
|
||||
'origin' => 'http://localhost',
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $user1['session'],
|
||||
];
|
||||
|
||||
$transaction1 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $user1Headers));
|
||||
|
||||
$this->assertEquals(201, $transaction1['headers']['status-code']);
|
||||
$transactionId1 = $transaction1['body']['$id'];
|
||||
|
||||
// Create user 2 (fresh)
|
||||
$user2 = $this->getUser(true); // Fresh user
|
||||
$user2Headers = [
|
||||
'origin' => 'http://localhost',
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $user2['session'],
|
||||
];
|
||||
|
||||
// User 2 tries to delete User 1's transaction - should fail
|
||||
$deleteAttempt = $this->client->call(Client::METHOD_DELETE, '/tablesdb/transactions/' . $transactionId1, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $user2Headers));
|
||||
|
||||
// This should fail with 404 Not Found
|
||||
$this->assertEquals(404, $deleteAttempt['headers']['status-code']);
|
||||
|
||||
// Verify User 1 can still access their transaction
|
||||
$readOwn = $this->client->call(Client::METHOD_GET, '/tablesdb/transactions/' . $transactionId1, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $user1Headers));
|
||||
|
||||
$this->assertEquals(200, $readOwn['headers']['status-code']);
|
||||
|
||||
// User 1 can delete their own transaction
|
||||
$deleteOwn = $this->client->call(Client::METHOD_DELETE, '/tablesdb/transactions/' . $transactionId1, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $user1Headers));
|
||||
|
||||
$this->assertEquals(204, $deleteOwn['headers']['status-code']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that one user cannot add operations to another user's transaction
|
||||
*/
|
||||
public function testUserCannotAddOperationsToAnotherUsersTransaction(): void
|
||||
{
|
||||
// Create a collection for testing
|
||||
$collection = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'tableId' => 'permTest11',
|
||||
'name' => 'Permission Test 11',
|
||||
'permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::create(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'rowSecurity' => false,
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $collection['headers']['status-code']);
|
||||
|
||||
$attribute = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $this->permissionsDatabase . '/tables/' . $collection['body']['$id'] . '/columns/string', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'key' => 'title',
|
||||
'size' => 255,
|
||||
'required' => true,
|
||||
]);
|
||||
|
||||
$this->assertEquals(202, $attribute['headers']['status-code']);
|
||||
sleep(2);
|
||||
|
||||
// Create user 1 (fresh) and their transaction
|
||||
$user1 = $this->getUser(true);
|
||||
$user1Headers = [
|
||||
'origin' => 'http://localhost',
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $user1['session'],
|
||||
];
|
||||
|
||||
$transaction1 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $user1Headers));
|
||||
|
||||
$this->assertEquals(201, $transaction1['headers']['status-code']);
|
||||
$transactionId1 = $transaction1['body']['$id'];
|
||||
|
||||
// Create user 2 (fresh)
|
||||
$user2 = $this->getUser(true); // Fresh user
|
||||
$user2Headers = [
|
||||
'origin' => 'http://localhost',
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $user2['session'],
|
||||
];
|
||||
|
||||
// User 2 tries to add operations to User 1's transaction - should fail
|
||||
$operationAttempt = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transactionId1 . '/operations', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $user2Headers), [
|
||||
'operations' => [[
|
||||
'action' => 'create',
|
||||
'databaseId' => $this->permissionsDatabase,
|
||||
'tableId' => $collection['body']['$id'],
|
||||
'rowId' => 'maliciousDoc',
|
||||
'data' => ['title' => 'Malicious Document'],
|
||||
]]
|
||||
]);
|
||||
|
||||
// This should fail with 404 Not Found
|
||||
$this->assertEquals(404, $operationAttempt['headers']['status-code']);
|
||||
|
||||
// Verify User 1 can still add operations to their own transaction
|
||||
$operationOwn = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions/' . $transactionId1 . '/operations', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $user1Headers), [
|
||||
'operations' => [[
|
||||
'action' => 'create',
|
||||
'databaseId' => $this->permissionsDatabase,
|
||||
'tableId' => $collection['body']['$id'],
|
||||
'rowId' => 'legitimateDoc',
|
||||
'data' => ['title' => 'Legitimate Document'],
|
||||
]]
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $operationOwn['headers']['status-code']);
|
||||
$this->assertEquals(1, $operationOwn['body']['operations']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that an authenticated user can successfully list their own transactions
|
||||
*/
|
||||
public function testAuthenticatedUserCanListTheirOwnTransactions(): void
|
||||
{
|
||||
// Create an authenticated user
|
||||
$user = $this->getUser();
|
||||
$userHeaders = [
|
||||
'origin' => 'http://localhost',
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $user['session'],
|
||||
];
|
||||
|
||||
// Create multiple transactions for this user
|
||||
$transactionIds = [];
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
$transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $userHeaders));
|
||||
|
||||
$this->assertEquals(201, $transaction['headers']['status-code']);
|
||||
$this->assertNotEmpty($transaction['body']['$id']);
|
||||
$transactionIds[] = $transaction['body']['$id'];
|
||||
}
|
||||
|
||||
// List transactions
|
||||
$list = $this->client->call(Client::METHOD_GET, '/tablesdb/transactions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $userHeaders));
|
||||
|
||||
$this->assertEquals(200, $list['headers']['status-code']);
|
||||
$this->assertGreaterThanOrEqual(3, $list['body']['total']);
|
||||
$this->assertIsArray($list['body']['transactions']);
|
||||
$this->assertGreaterThanOrEqual(3, count($list['body']['transactions']));
|
||||
|
||||
// Verify all created transactions are in the list
|
||||
$listedIds = array_column($list['body']['transactions'], '$id');
|
||||
foreach ($transactionIds as $transactionId) {
|
||||
$this->assertContains($transactionId, $listedIds);
|
||||
}
|
||||
|
||||
// Verify transaction structure
|
||||
foreach ($list['body']['transactions'] as $transaction) {
|
||||
$this->assertArrayHasKey('$id', $transaction);
|
||||
$this->assertArrayHasKey('$createdAt', $transaction);
|
||||
$this->assertArrayHasKey('$updatedAt', $transaction);
|
||||
$this->assertArrayHasKey('status', $transaction);
|
||||
$this->assertArrayHasKey('operations', $transaction);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that an authenticated user can successfully delete their own transaction
|
||||
*/
|
||||
public function testAuthenticatedUserCanDeleteTheirOwnTransaction(): void
|
||||
{
|
||||
// Create an authenticated user
|
||||
$user = $this->getUser();
|
||||
$userHeaders = [
|
||||
'origin' => 'http://localhost',
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $user['session'],
|
||||
];
|
||||
|
||||
// Create a transaction
|
||||
$transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $userHeaders));
|
||||
|
||||
$this->assertEquals(201, $transaction['headers']['status-code']);
|
||||
$transactionId = $transaction['body']['$id'];
|
||||
|
||||
// Verify transaction exists by reading it
|
||||
$read = $this->client->call(Client::METHOD_GET, '/tablesdb/transactions/' . $transactionId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $userHeaders));
|
||||
|
||||
$this->assertEquals(200, $read['headers']['status-code']);
|
||||
$this->assertEquals($transactionId, $read['body']['$id']);
|
||||
|
||||
// Delete the transaction
|
||||
$delete = $this->client->call(Client::METHOD_DELETE, '/tablesdb/transactions/' . $transactionId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $userHeaders));
|
||||
|
||||
$this->assertEquals(204, $delete['headers']['status-code']);
|
||||
|
||||
// Verify transaction is deleted by trying to read it again
|
||||
$readAfterDelete = $this->client->call(Client::METHOD_GET, '/tablesdb/transactions/' . $transactionId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $userHeaders));
|
||||
|
||||
$this->assertEquals(404, $readAfterDelete['headers']['status-code']);
|
||||
|
||||
// Create another transaction and verify it can also be deleted
|
||||
$transaction2 = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $userHeaders));
|
||||
|
||||
$this->assertEquals(201, $transaction2['headers']['status-code']);
|
||||
$transactionId2 = $transaction2['body']['$id'];
|
||||
|
||||
$delete2 = $this->client->call(Client::METHOD_DELETE, '/tablesdb/transactions/' . $transactionId2, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $userHeaders));
|
||||
|
||||
$this->assertEquals(204, $delete2['headers']['status-code']);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue