Process permissions before txn logging

This commit is contained in:
Jake Barnby 2025-08-15 00:34:20 +12:00
parent 2235848262
commit 3d1ae4feda
No known key found for this signature in database
GPG key ID: C437A8CC85B96E9C
7 changed files with 141 additions and 141 deletions

View file

@ -13,10 +13,12 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Conflict as ConflictException;
use Utopia\Database\Exception\Limit as LimitException;
use Utopia\Database\Exception\NotFound as NotFoundException;
use Utopia\Database\Exception\Type as TypeException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
@ -85,12 +87,12 @@ 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
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
$collection = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
if ($collection->isEmpty()) {
throw new Exception($this->getParentNotFoundException());
}
@ -142,7 +144,7 @@ class Decrement extends Action
'$id' => $documentId,
'$collectionId' => $collectionId,
'$databaseId' => $databaseId,
$attribute => $value, // Mock response - actual value would be computed during commit
$attribute => $value,
]);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_OK)

View file

@ -13,10 +13,12 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Conflict as ConflictException;
use Utopia\Database\Exception\Limit as LimitException;
use Utopia\Database\Exception\NotFound as NotFoundException;
use Utopia\Database\Exception\Type as TypeException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
@ -142,7 +144,7 @@ class Increment extends Action
'$id' => $documentId,
'$collectionId' => $collectionId,
'$databaseId' => $databaseId,
$attribute => $value, // Mock response - actual value would be computed during commit
$attribute => $value,
]);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_OK)

View file

@ -133,7 +133,6 @@ class Delete extends Action
'databaseInternalId' => $database->getSequence(),
'collectionInternalId' => $collection->getSequence(),
'transactionInternalId' => $transaction->getSequence(),
'documentId' => null, // Bulk operation doesn't have specific document ID
'action' => 'bulkDelete',
'data' => [
'queries' => $queries,
@ -146,7 +145,6 @@ class Delete extends Action
'transactions',
$transactionId,
'operations',
1
);
});

View file

@ -122,6 +122,13 @@ class Update extends Action
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
if ($data['$permissions']) {
$validator = new Permissions();
if (!$validator->isValid($data['$permissions'])) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, $validator->getDescription());
}
}
// Handle transaction staging
if ($transactionId !== null) {
$transaction = $dbForProject->getDocument('transactions', $transactionId);
@ -145,7 +152,6 @@ class Update extends Action
'databaseInternalId' => $database->getSequence(),
'collectionInternalId' => $collection->getSequence(),
'transactionInternalId' => $transaction->getSequence(),
'documentId' => null, // Bulk operation doesn't have specific document ID
'action' => 'bulkUpdate',
'data' => [
'data' => $data,
@ -171,13 +177,6 @@ class Update extends Action
return;
}
if ($data['$permissions']) {
$validator = new Permissions();
if (!$validator->isValid($data['$permissions'])) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, $validator->getDescription());
}
}
$documents = [];
try {

View file

@ -101,6 +101,10 @@ class Upsert extends Action
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk upsert is not supported for ' . $this->getSdkNamespace() . ' with relationship attributes');
}
foreach ($documents as $key => $document) {
$documents[$key] = new Document($document);
}
// Handle transaction staging
if ($transactionId !== null) {
$transaction = $dbForProject->getDocument('transactions', $transactionId);
@ -111,7 +115,7 @@ class Upsert extends Action
// Enforce max operations per transaction
$maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION;
$existing = $transaction->getAttribute('operations', 0);
if (($existing + \count($documents)) > $maxBatch) {
if (($existing + 1) > $maxBatch) {
throw new Exception(
Exception::TRANSACTION_LIMIT_EXCEEDED,
'Transaction already has ' . $existing . ' operations, adding ' . \count($documents) . ' would exceed the maximum of ' . $maxBatch
@ -119,21 +123,17 @@ class Upsert extends Action
}
// Stage the operations in transaction logs
$staged = [];
foreach ($documents as $document) {
$staged[] = new Document([
'$id' => ID::unique(),
'databaseInternalId' => $database->getSequence(),
'collectionInternalId' => $collection->getSequence(),
'transactionInternalId' => $transaction->getSequence(),
'documentId' => $document['$id'] ?? ID::unique(),
'action' => 'upsert',
'data' => $document,
]);
}
$staged = new Document([
'$id' => ID::unique(),
'databaseInternalId' => $database->getSequence(),
'collectionInternalId' => $collection->getSequence(),
'transactionInternalId' => $transaction->getSequence(),
'action' => 'bulkUpsert',
'data' => $documents,
]);
$dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) {
$dbForProject->createDocuments('transactionLogs', $staged);
$dbForProject->createDocument('transactionLogs', $staged);
$dbForProject->increaseDocumentAttribute(
'transactions',
$transactionId,
@ -147,11 +147,8 @@ class Upsert extends Action
$this->getSdkGroup() => [],
'total' => \count($documents),
]), $this->getResponseModel());
return;
}
foreach ($documents as $key => $document) {
$documents[$key] = new Document($document);
return;
}
$upserted = [];

View file

@ -130,61 +130,6 @@ class Update extends Action
throw new Exception($this->getNotFoundException());
}
// Handle transaction staging
if ($transactionId !== null) {
$transaction = $dbForProject->getDocument('transactions', $transactionId);
if ($transaction->isEmpty()) {
throw new Exception(Exception::TRANSACTION_NOT_FOUND);
}
if ($transaction->getAttribute('status', '') !== 'pending') {
throw new Exception(Exception::TRANSACTION_NOT_READY);
}
// Enforce max operations per transaction
$maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION;
$existing = $transaction->getAttribute('operations', 0);
if (($existing + 1) > $maxBatch) {
throw new Exception(
Exception::TRANSACTION_LIMIT_EXCEEDED,
'Transaction already has ' . $existing . ' operations, adding 1 would exceed the maximum of ' . $maxBatch
);
}
// Stage the operation in transaction logs
$staged = new Document([
'$id' => ID::unique(),
'databaseInternalId' => $database->getSequence(),
'collectionInternalId' => $collection->getSequence(),
'transactionInternalId' => $transaction->getSequence(),
'documentId' => $documentId,
'action' => 'update',
'data' => $data,
]);
$dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) {
$dbForProject->createDocument('transactionLogs', $staged);
$dbForProject->increaseDocumentAttribute(
'transactions',
$transactionId,
'operations',
1
);
});
// Return successful response without actually updating document
$mockDocument = new Document([
'$id' => $documentId,
'$collectionId' => $collectionId,
'$databaseId' => $databaseId,
...$document->getArrayCopy(),
...$data
]);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
->dynamic($mockDocument, $this->getResponseModel());
return;
}
// Map aggregate permissions into the multiple permissions they represent.
$permissions = Permission::aggregate($permissions, [
Database::PERMISSION_READ,
@ -298,6 +243,63 @@ class Update extends Action
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, max($operations, 1))
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), $operations);
// Handle transaction staging
if ($transactionId !== null) {
$transaction = $dbForProject->getDocument('transactions', $transactionId);
if ($transaction->isEmpty()) {
throw new Exception(Exception::TRANSACTION_NOT_FOUND);
}
if ($transaction->getAttribute('status', '') !== 'pending') {
throw new Exception(Exception::TRANSACTION_NOT_READY);
}
// Enforce max operations per transaction
$maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION;
$existing = $transaction->getAttribute('operations', 0);
if (($existing + 1) > $maxBatch) {
throw new Exception(
Exception::TRANSACTION_LIMIT_EXCEEDED,
'Transaction already has ' . $existing . ' operations, adding 1 would exceed the maximum of ' . $maxBatch
);
}
// Stage the operation in transaction logs
$staged = new Document([
'$id' => ID::unique(),
'databaseInternalId' => $database->getSequence(),
'collectionInternalId' => $collection->getSequence(),
'transactionInternalId' => $transaction->getSequence(),
'documentId' => $documentId,
'action' => 'update',
'data' => $data,
]);
$dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) {
$dbForProject->createDocument('transactionLogs', $staged);
$dbForProject->increaseDocumentAttribute(
'transactions',
$transactionId,
'operations',
1
);
});
// Return successful response without actually updating document
$mockDocument = new Document([
'$id' => $documentId,
'$collectionId' => $collectionId,
'$databaseId' => $databaseId,
...$document->getArrayCopy(),
...$data
]);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
->dynamic($mockDocument, $this->getResponseModel());
return;
}
try {
$document = $dbForProject->withRequestTimestamp(
$requestTimestamp,

View file

@ -112,60 +112,6 @@ class Upsert extends Action
throw new Exception($this->getParentNotFoundException());
}
// Handle transaction staging
if ($transactionId !== null) {
$transaction = $dbForProject->getDocument('transactions', $transactionId);
if ($transaction->isEmpty()) {
throw new Exception(Exception::TRANSACTION_NOT_FOUND);
}
if ($transaction->getAttribute('status', '') !== 'pending') {
throw new Exception(Exception::TRANSACTION_NOT_READY);
}
// Enforce max operations per transaction
$maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION;
$existing = $transaction->getAttribute('operations', 0);
if (($existing + 1) > $maxBatch) {
throw new Exception(
Exception::TRANSACTION_LIMIT_EXCEEDED,
'Transaction already has ' . $existing . ' operations, adding 1 would exceed the maximum of ' . $maxBatch
);
}
// Stage the operation in transaction logs
$staged = new Document([
'$id' => ID::unique(),
'databaseInternalId' => $database->getSequence(),
'collectionInternalId' => $collection->getSequence(),
'transactionInternalId' => $transaction->getSequence(),
'documentId' => $documentId,
'action' => 'upsert',
'data' => $data,
]);
$dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) {
$dbForProject->createDocument('transactionLogs', $staged);
$dbForProject->increaseDocumentAttribute(
'transactions',
$transactionId,
'operations',
1
);
});
// Return successful response without actually upserting document
$mockDocument = new Document([
'$id' => $documentId,
'$collectionId' => $collectionId,
'$databaseId' => $databaseId,
...$data
]);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_CREATED)
->dynamic($mockDocument, $this->getResponseModel());
return;
}
$allowedPermissions = [
Database::PERMISSION_READ,
Database::PERMISSION_UPDATE,
@ -300,6 +246,60 @@ class Upsert extends Action
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $operations))
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $operations));
// Handle transaction staging
if ($transactionId !== null) {
$transaction = $dbForProject->getDocument('transactions', $transactionId);
if ($transaction->isEmpty()) {
throw new Exception(Exception::TRANSACTION_NOT_FOUND);
}
if ($transaction->getAttribute('status', '') !== 'pending') {
throw new Exception(Exception::TRANSACTION_NOT_READY);
}
// Enforce max operations per transaction
$maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION;
$existing = $transaction->getAttribute('operations', 0);
if (($existing + 1) > $maxBatch) {
throw new Exception(
Exception::TRANSACTION_LIMIT_EXCEEDED,
'Transaction already has ' . $existing . ' operations, adding 1 would exceed the maximum of ' . $maxBatch
);
}
// Stage the operation in transaction logs
$staged = new Document([
'$id' => ID::unique(),
'databaseInternalId' => $database->getSequence(),
'collectionInternalId' => $collection->getSequence(),
'transactionInternalId' => $transaction->getSequence(),
'documentId' => $documentId,
'action' => 'upsert',
'data' => $data,
]);
$dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) {
$dbForProject->createDocument('transactionLogs', $staged);
$dbForProject->increaseDocumentAttribute(
'transactions',
$transactionId,
'operations',
1
);
});
// Return successful response without actually upserting document
$mockDocument = new Document([
'$id' => $documentId,
'$collectionId' => $collectionId,
'$databaseId' => $databaseId,
...$data
]);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_CREATED)
->dynamic($mockDocument, $this->getResponseModel());
return;
}
$upserted = [];
try {
$dbForProject->withPreserveDates(function () use (&$upserted, $dbForProject, $database, $collection, $newDocument) {