diff --git a/src/Database/Validator/Operator.php b/src/Database/Validator/Operator.php index 082895af4..6ada79777 100644 --- a/src/Database/Validator/Operator.php +++ b/src/Database/Validator/Operator.php @@ -233,6 +233,13 @@ private function validateOperatorForAttribute( $type = $attribute->type; $isArray = $attribute->array; + // Operators run on the stored value, which for an encrypted attribute is the ciphertext + if (\in_array('encrypt', $attribute->filters, true)) { + $this->message = "Cannot apply {$methodName} operator to encrypted attribute '{$operator->getAttribute()}'"; + + return false; + } + // Array operators that carry a caller-supplied value list are capped to guard against // memory exhaustion. Enforced here so every adapter rejects an oversized list the same way. // The payload may be spread across $values or wrapped in $values[0] (the same shape the diff --git a/tests/e2e/Adapter/Scopes/OperatorTests.php b/tests/e2e/Adapter/Scopes/OperatorTests.php index 0bf19d41a..86d616fd3 100644 --- a/tests/e2e/Adapter/Scopes/OperatorTests.php +++ b/tests/e2e/Adapter/Scopes/OperatorTests.php @@ -694,6 +694,44 @@ public function testOperatorArrayErrorHandling(): void $database->deleteCollection($collectionId); } + public function testOperatorEncryptedAttribute(): void + { + /** @var Database $database */ + $database = $this->getDatabase(); + + if (!$database->getAdapter()->supports(Capability::Operators)) { + $this->expectNotToPerformAssertions(); + return; + } + + $database->addFilter( + 'encrypt', + fn (mixed $value): ?string => \is_string($value) ? (string) \json_encode(['data' => \base64_encode($value)]) : null, + function (mixed $value): ?string { + $decoded = \is_string($value) ? \json_decode($value, true) : null; + + return \is_array($decoded) && \is_string($decoded['data'] ?? null) ? (string) \base64_decode($decoded['data']) : null; + }, + ); + + $collectionId = 'test_operator_encrypted'; + $database->createCollection(Collection::create(id: $collectionId)); + $database->createAttribute($collectionId, Attribute::string(key: 'secret', size: 128, filters: ['encrypt'])); + + $database->createDocument($collectionId, new Document([ + '$id' => 'doc1', + '$permissions' => [Permission::read(Role::any()), Permission::update(Role::any())], + 'secret' => 'banana', + ])); + + $this->expectException(StructureException::class); + $this->expectExceptionMessage("Cannot apply stringReplace operator to encrypted attribute 'secret'"); + + $database->updateDocument($collectionId, 'doc1', new Document([ + 'secret' => Operator::stringReplace('a', 'o'), + ])); + } + public function testOperatorInsertErrorHandling(): void { /** @var Database $database */