Skip to content

Commit 1550238

Browse files
authored
Merge pull request #940 from utopia-php/fix/mongo-permission-exact-match
fix: match Mongo document permissions with exact strings
2 parents 4f50112 + d46283d commit 1550238

3 files changed

Lines changed: 175 additions & 6 deletions

File tree

‎src/Database/Adapter/Mongo.php‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2460,6 +2460,18 @@ protected function getInternalKeyForAttribute(string $attribute): string
24602460
};
24612461
}
24622462

2463+
/**
2464+
* @return list<string>
2465+
*/
2466+
private function permissionStrings(string $type): array
2467+
{
2468+
$permissions = [];
2469+
foreach ($this->authorization->getRoles() as $role) {
2470+
$permissions[] = $type . '("' . $role . '")';
2471+
}
2472+
2473+
return $permissions;
2474+
}
24632475

24642476
/**
24652477
* Find Documents
@@ -2497,8 +2509,7 @@ public function find(Document $collection, array $queries = [], ?int $limit = 25
24972509

24982510
// permissions
24992511
if ($this->authorization->getStatus()) {
2500-
$roles = \implode('|', $this->authorization->getRoles());
2501-
$filters['_permissions']['$in'] = [new Regex("{$forPermission}\\(\"(?:{$roles})\"\\)", 'i')];
2512+
$filters['_permissions']['$in'] = $this->permissionStrings($forPermission);
25022513
}
25032514

25042515
$options = [];
@@ -2750,8 +2761,7 @@ public function count(Document $collection, array $queries = [], ?int $max = nul
27502761

27512762
// Add permissions filter if authorization is enabled
27522763
if ($this->authorization->getStatus()) {
2753-
$roles = \implode('|', $this->authorization->getRoles());
2754-
$filters['_permissions']['$in'] = [new Regex("read\\(\"(?:{$roles})\"\\)", 'i')];
2764+
$filters['_permissions']['$in'] = $this->permissionStrings(Database::PERMISSION_READ);
27552765
}
27562766

27572767
/**
@@ -2850,8 +2860,7 @@ public function sum(Document $collection, string $attribute, array $queries = []
28502860

28512861
// permissions
28522862
if ($this->authorization->getStatus()) { // skip if authorization is disabled
2853-
$roles = \implode('|', $this->authorization->getRoles());
2854-
$filters['_permissions']['$in'] = [new Regex("read\\(\"(?:{$roles})\"\\)", 'i')];
2863+
$filters['_permissions']['$in'] = $this->permissionStrings(Database::PERMISSION_READ);
28552864
}
28562865

28572866
// using aggregation to get sum an attribute as described in

‎tests/e2e/Adapter/Scopes/PermissionTests.php‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1312,4 +1312,79 @@ public function testCreateRelationDocumentWithoutUpdatePermission(): void
13121312
$database->deleteCollection('childRelationTest');
13131313
}
13141314

1315+
public function testDocumentPermissionRolesAreMatchedExactly(): void
1316+
{
1317+
/** @var Database $database */
1318+
$database = $this->getDatabase();
1319+
$authorization = $database->getAuthorization();
1320+
$collection = 'perm_exact_' . uniqid();
1321+
1322+
$database->createCollection($collection, permissions: [
1323+
Permission::create(Role::any()),
1324+
], documentSecurity: true);
1325+
$database->createAttribute($collection, 'amount', Database::VAR_INTEGER, 0, true);
1326+
1327+
$authorization->skip(function () use ($database, $collection): void {
1328+
$database->createDocument($collection, new Document([
1329+
'$id' => 'alice0',
1330+
'$permissions' => [Permission::read(Role::user('alice0'))],
1331+
'amount' => 10,
1332+
]));
1333+
$database->createDocument($collection, new Document([
1334+
'$id' => 'alice9',
1335+
'$permissions' => [Permission::read(Role::user('alice9'))],
1336+
'amount' => 20,
1337+
]));
1338+
$database->createDocument($collection, new Document([
1339+
'$id' => 'mass',
1340+
'$permissions' => [Permission::read(Role::user('a3f9c1e0b2d4a6f8c1e0'))],
1341+
'amount' => 30,
1342+
]));
1343+
$database->createDocument($collection, new Document([
1344+
'$id' => 'literal',
1345+
'$permissions' => [Permission::read(Role::user('alice.'))],
1346+
'amount' => 40,
1347+
]));
1348+
});
1349+
1350+
$authorization->cleanRoles();
1351+
$authorization->addRole(Role::user('alice.')->toString());
1352+
1353+
$this->assertSame(['literal'], $this->documentIds($database->find($collection)));
1354+
$this->assertSame(1, $database->count($collection));
1355+
$this->assertSame(40, (int) $database->sum($collection, 'amount'));
1356+
$this->assertTrue($database->getDocument($collection, 'alice0')->isEmpty());
1357+
$this->assertSame('literal', $database->getDocument($collection, 'literal')->getId());
1358+
1359+
$authorization->cleanRoles();
1360+
$authorization->addRole(Role::user('a' . \str_repeat('.', 19))->toString());
1361+
1362+
$this->assertSame([], $this->documentIds($database->find($collection)));
1363+
$this->assertSame(0, $database->count($collection));
1364+
$this->assertSame(0, (int) $database->sum($collection, 'amount'));
1365+
$this->assertTrue($database->getDocument($collection, 'mass')->isEmpty());
1366+
1367+
$authorization->cleanRoles();
1368+
$authorization->addRole(Role::user('alice0')->toString());
1369+
1370+
$this->assertSame(['alice0'], $this->documentIds($database->find($collection)));
1371+
$this->assertSame(1, $database->count($collection));
1372+
$this->assertSame(10, (int) $database->sum($collection, 'amount'));
1373+
$this->assertSame('alice0', $database->getDocument($collection, 'alice0')->getId());
1374+
1375+
$database->deleteCollection($collection);
1376+
}
1377+
1378+
/**
1379+
* @param array<Document> $documents
1380+
* @return list<string>
1381+
*/
1382+
private function documentIds(array $documents): array
1383+
{
1384+
return \array_values(\array_map(
1385+
static fn (Document $document): string => $document->getId(),
1386+
$documents,
1387+
));
1388+
}
1389+
13151390
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
namespace Tests\Unit;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use ReflectionClass;
7+
use ReflectionMethod;
8+
use Utopia\Database\Adapter\Mongo;
9+
use Utopia\Database\Database;
10+
use Utopia\Database\Validator\Authorization;
11+
12+
class MongoPermissionStringsTest extends TestCase
13+
{
14+
public function testPeriodInRoleIsLiteralNotRegexWildcard(): void
15+
{
16+
$this->assertSame(
17+
['read("user:alice.")'],
18+
$this->permissionStrings(['user:alice.'], Database::PERMISSION_READ)
19+
);
20+
}
21+
22+
public function testMassReadDotPaddingStaysExact(): void
23+
{
24+
$role = 'user:a' . \str_repeat('.', 19);
25+
26+
$this->assertSame(
27+
['read("' . $role . '")'],
28+
$this->permissionStrings([$role], Database::PERMISSION_READ)
29+
);
30+
}
31+
32+
public function testMatchingIsCaseSensitiveAndUsesRequestedType(): void
33+
{
34+
$this->assertSame(
35+
['update("user:alice")'],
36+
$this->permissionStrings(['user:alice'], Database::PERMISSION_UPDATE)
37+
);
38+
}
39+
40+
public function testMultipleRolesMapToExactPermissionStrings(): void
41+
{
42+
$this->assertSame(
43+
['read("user:alice")', 'read("users")'],
44+
$this->permissionStrings(['user:alice', 'users'], Database::PERMISSION_READ)
45+
);
46+
}
47+
48+
public function testEmptyRolesProduceEmptyList(): void
49+
{
50+
$this->assertSame([], $this->permissionStrings([], Database::PERMISSION_READ));
51+
}
52+
53+
public function testValuesAreStringsNotRegex(): void
54+
{
55+
foreach ($this->permissionStrings(['user:alice.'], Database::PERMISSION_READ) as $value) {
56+
$this->assertIsString($value);
57+
$this->assertStringStartsWith('read("', $value);
58+
$this->assertStringEndsWith('")', $value);
59+
}
60+
}
61+
62+
/**
63+
* @param list<string> $roles
64+
* @return list<string>
65+
*/
66+
private function permissionStrings(array $roles, string $type): array
67+
{
68+
$authorization = new Authorization();
69+
$authorization->enable();
70+
$authorization->cleanRoles();
71+
foreach ($roles as $role) {
72+
$authorization->addRole($role);
73+
}
74+
75+
$adapter = (new ReflectionClass(Mongo::class))->newInstanceWithoutConstructor();
76+
$adapter->setAuthorization($authorization);
77+
78+
$method = new ReflectionMethod(Mongo::class, 'permissionStrings');
79+
80+
/** @var list<string> $values */
81+
$values = $method->invoke($adapter, $type);
82+
83+
return $values;
84+
}
85+
}

0 commit comments

Comments
 (0)