diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index c52915946..136ebac0f 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -2460,6 +2460,18 @@ protected function getInternalKeyForAttribute(string $attribute): string }; } + /** + * @return list + */ + private function permissionStrings(string $type): array + { + $permissions = []; + foreach ($this->authorization->getRoles() as $role) { + $permissions[] = $type . '("' . $role . '")'; + } + + return $permissions; + } /** * Find Documents @@ -2497,8 +2509,7 @@ public function find(Document $collection, array $queries = [], ?int $limit = 25 // permissions if ($this->authorization->getStatus()) { - $roles = \implode('|', $this->authorization->getRoles()); - $filters['_permissions']['$in'] = [new Regex("{$forPermission}\\(\"(?:{$roles})\"\\)", 'i')]; + $filters['_permissions']['$in'] = $this->permissionStrings($forPermission); } $options = []; @@ -2750,8 +2761,7 @@ public function count(Document $collection, array $queries = [], ?int $max = nul // Add permissions filter if authorization is enabled if ($this->authorization->getStatus()) { - $roles = \implode('|', $this->authorization->getRoles()); - $filters['_permissions']['$in'] = [new Regex("read\\(\"(?:{$roles})\"\\)", 'i')]; + $filters['_permissions']['$in'] = $this->permissionStrings(Database::PERMISSION_READ); } /** @@ -2850,8 +2860,7 @@ public function sum(Document $collection, string $attribute, array $queries = [] // permissions if ($this->authorization->getStatus()) { // skip if authorization is disabled - $roles = \implode('|', $this->authorization->getRoles()); - $filters['_permissions']['$in'] = [new Regex("read\\(\"(?:{$roles})\"\\)", 'i')]; + $filters['_permissions']['$in'] = $this->permissionStrings(Database::PERMISSION_READ); } // using aggregation to get sum an attribute as described in diff --git a/tests/e2e/Adapter/Scopes/PermissionTests.php b/tests/e2e/Adapter/Scopes/PermissionTests.php index c3af74495..a97f9691c 100644 --- a/tests/e2e/Adapter/Scopes/PermissionTests.php +++ b/tests/e2e/Adapter/Scopes/PermissionTests.php @@ -1312,4 +1312,79 @@ public function testCreateRelationDocumentWithoutUpdatePermission(): void $database->deleteCollection('childRelationTest'); } + public function testDocumentPermissionRolesAreMatchedExactly(): void + { + /** @var Database $database */ + $database = $this->getDatabase(); + $authorization = $database->getAuthorization(); + $collection = 'perm_exact_' . uniqid(); + + $database->createCollection($collection, permissions: [ + Permission::create(Role::any()), + ], documentSecurity: true); + $database->createAttribute($collection, 'amount', Database::VAR_INTEGER, 0, true); + + $authorization->skip(function () use ($database, $collection): void { + $database->createDocument($collection, new Document([ + '$id' => 'alice0', + '$permissions' => [Permission::read(Role::user('alice0'))], + 'amount' => 10, + ])); + $database->createDocument($collection, new Document([ + '$id' => 'alice9', + '$permissions' => [Permission::read(Role::user('alice9'))], + 'amount' => 20, + ])); + $database->createDocument($collection, new Document([ + '$id' => 'mass', + '$permissions' => [Permission::read(Role::user('a3f9c1e0b2d4a6f8c1e0'))], + 'amount' => 30, + ])); + $database->createDocument($collection, new Document([ + '$id' => 'literal', + '$permissions' => [Permission::read(Role::user('alice.'))], + 'amount' => 40, + ])); + }); + + $authorization->cleanRoles(); + $authorization->addRole(Role::user('alice.')->toString()); + + $this->assertSame(['literal'], $this->documentIds($database->find($collection))); + $this->assertSame(1, $database->count($collection)); + $this->assertSame(40, (int) $database->sum($collection, 'amount')); + $this->assertTrue($database->getDocument($collection, 'alice0')->isEmpty()); + $this->assertSame('literal', $database->getDocument($collection, 'literal')->getId()); + + $authorization->cleanRoles(); + $authorization->addRole(Role::user('a' . \str_repeat('.', 19))->toString()); + + $this->assertSame([], $this->documentIds($database->find($collection))); + $this->assertSame(0, $database->count($collection)); + $this->assertSame(0, (int) $database->sum($collection, 'amount')); + $this->assertTrue($database->getDocument($collection, 'mass')->isEmpty()); + + $authorization->cleanRoles(); + $authorization->addRole(Role::user('alice0')->toString()); + + $this->assertSame(['alice0'], $this->documentIds($database->find($collection))); + $this->assertSame(1, $database->count($collection)); + $this->assertSame(10, (int) $database->sum($collection, 'amount')); + $this->assertSame('alice0', $database->getDocument($collection, 'alice0')->getId()); + + $database->deleteCollection($collection); + } + + /** + * @param array $documents + * @return list + */ + private function documentIds(array $documents): array + { + return \array_values(\array_map( + static fn (Document $document): string => $document->getId(), + $documents, + )); + } + } diff --git a/tests/unit/MongoPermissionStringsTest.php b/tests/unit/MongoPermissionStringsTest.php new file mode 100644 index 000000000..25cf69da9 --- /dev/null +++ b/tests/unit/MongoPermissionStringsTest.php @@ -0,0 +1,85 @@ +assertSame( + ['read("user:alice.")'], + $this->permissionStrings(['user:alice.'], Database::PERMISSION_READ) + ); + } + + public function testMassReadDotPaddingStaysExact(): void + { + $role = 'user:a' . \str_repeat('.', 19); + + $this->assertSame( + ['read("' . $role . '")'], + $this->permissionStrings([$role], Database::PERMISSION_READ) + ); + } + + public function testMatchingIsCaseSensitiveAndUsesRequestedType(): void + { + $this->assertSame( + ['update("user:alice")'], + $this->permissionStrings(['user:alice'], Database::PERMISSION_UPDATE) + ); + } + + public function testMultipleRolesMapToExactPermissionStrings(): void + { + $this->assertSame( + ['read("user:alice")', 'read("users")'], + $this->permissionStrings(['user:alice', 'users'], Database::PERMISSION_READ) + ); + } + + public function testEmptyRolesProduceEmptyList(): void + { + $this->assertSame([], $this->permissionStrings([], Database::PERMISSION_READ)); + } + + public function testValuesAreStringsNotRegex(): void + { + foreach ($this->permissionStrings(['user:alice.'], Database::PERMISSION_READ) as $value) { + $this->assertIsString($value); + $this->assertStringStartsWith('read("', $value); + $this->assertStringEndsWith('")', $value); + } + } + + /** + * @param list $roles + * @return list + */ + private function permissionStrings(array $roles, string $type): array + { + $authorization = new Authorization(); + $authorization->enable(); + $authorization->cleanRoles(); + foreach ($roles as $role) { + $authorization->addRole($role); + } + + $adapter = (new ReflectionClass(Mongo::class))->newInstanceWithoutConstructor(); + $adapter->setAuthorization($authorization); + + $method = new ReflectionMethod(Mongo::class, 'permissionStrings'); + + /** @var list $values */ + $values = $method->invoke($adapter, $type); + + return $values; + } +}