Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions src/Database/Adapter/Mongo.php
Original file line number Diff line number Diff line change
Expand Up @@ -2460,6 +2460,18 @@ protected function getInternalKeyForAttribute(string $attribute): string
};
}

/**
* @return list<string>
*/
private function permissionStrings(string $type): array
{
$permissions = [];
foreach ($this->authorization->getRoles() as $role) {
$permissions[] = $type . '("' . $role . '")';
}

return $permissions;
}

/**
* Find Documents
Expand Down Expand Up @@ -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 = [];
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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
Expand Down
75 changes: 75 additions & 0 deletions tests/e2e/Adapter/Scopes/PermissionTests.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<Document> $documents
* @return list<string>
*/
private function documentIds(array $documents): array
{
return \array_values(\array_map(
static fn (Document $document): string => $document->getId(),
$documents,
));
}

}
85 changes: 85 additions & 0 deletions tests/unit/MongoPermissionStringsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

namespace Tests\Unit;

use PHPUnit\Framework\TestCase;
use ReflectionClass;
use ReflectionMethod;
use Utopia\Database\Adapter\Mongo;
use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;

class MongoPermissionStringsTest extends TestCase
{
public function testPeriodInRoleIsLiteralNotRegexWildcard(): void
{
$this->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<string> $roles
* @return list<string>
*/
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<string> $values */
$values = $method->invoke($adapter, $type);

return $values;
}
}
Loading