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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

## 2.0.1 under development

Bug #264: Fix bug when default roles were not checked in `Manager::userHasPermission()` (@KovYu, @arogachev)
- Bug #264: Fix bug when default roles were not checked in `Manager::userHasPermission()` (@KovYu, @arogachev)
- New #275: Add optional `$clock` parameter to `Manager` constructor to get current time (@vjik)

## 2.0.0 March 07, 2024

Expand Down
18 changes: 9 additions & 9 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,21 @@
"yiisoft/friendly-exception": "^1.1"
},
"require-dev": {
"ext-uopz": "*",
"maglnet/composer-require-checker": "^4.3",
"phpunit/phpunit": "^10.5.2",
"rector/rector": "^2.0.3",
"roave/infection-static-analysis-plugin": "^1.18",
"slope-it/clock-mock": "^0.4.0",
"spatie/phpunit-watcher": "^1.23",
"maglnet/composer-require-checker": "^4.7.1",
"phpunit/phpunit": "^10.5.45",
"psr/clock": "^1.0",
"rector/rector": "^2.0.10",
"roave/infection-static-analysis-plugin": "^1.35",
"spatie/phpunit-watcher": "^1.24",
"vimeo/psalm": "^5.26.1",
"yiisoft/di": "^1.2"
"yiisoft/di": "^1.3"
},
"suggest": {
"yiisoft/rbac-cycle-db": "For using Cycle as a storage",
"yiisoft/rbac-db": "For using Yii Database as a storage",
"yiisoft/rbac-php": "For using PHP files as a storage",
"yiisoft/rbac-rules-container": "To create rules via Yii Factory"
"yiisoft/rbac-rules-container": "To create rules via Yii Factory",
"psr/clock": "For using custom clock"
},
"autoload": {
"psr-4": {
Expand Down
19 changes: 15 additions & 4 deletions src/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Closure;
use InvalidArgumentException;
use Psr\Clock\ClockInterface;
use RuntimeException;
use Stringable;
use Yiisoft\Access\AccessCheckerInterface;
Expand All @@ -30,18 +31,21 @@ final class Manager implements ManagerInterface
/**
* @param ItemsStorageInterface $itemsStorage Items storage.
* @param AssignmentsStorageInterface $assignmentsStorage Assignments storage.
* @param RuleFactoryInterface $ruleFactory Rule factory.
* @param RuleFactoryInterface|null $ruleFactory Rule factory.
* @param bool $enableDirectPermissions Whether to enable assigning permissions directly to user. Prefer assigning
* roles only.
* @param bool $includeRolesInAccessChecks Whether to include roles (in addition to permissions) during access
* checks in {@see Manager::userHasPermission()}.
* @param ClockInterface|null $clock Instance of `ClockInterface` implementation to be used for getting current
* time.
*/
public function __construct(
private readonly ItemsStorageInterface $itemsStorage,
private readonly AssignmentsStorageInterface $assignmentsStorage,
?RuleFactoryInterface $ruleFactory = null,
private readonly bool $enableDirectPermissions = false,
private readonly bool $includeRolesInAccessChecks = false,
private readonly ?ClockInterface $clock = null,
) {
$this->ruleFactory = $ruleFactory ?? new SimpleRuleFactory();
}
Expand Down Expand Up @@ -70,7 +74,7 @@ public function userHasPermission(
}

$hierarchy = $this->itemsStorage->getHierarchy($item->getName());
$itemNames = array_map(static fn (array $treeItem): string => $treeItem['item']->getName(), $hierarchy);
$itemNames = array_map(static fn(array $treeItem): string => $treeItem['item']->getName(), $hierarchy);
$userItemNames = $guestRole !== null
? [$guestRole->getName()]
: $this->filterUserItemNames((string) $userId, $itemNames);
Expand Down Expand Up @@ -172,7 +176,7 @@ public function assign(string $itemName, int|Stringable|string $userId, ?int $cr
return $this;
}

$assignment = new Assignment($userId, $itemName, $createdAt ?? time());
$assignment = new Assignment($userId, $itemName, $createdAt ?? $this->getCurrentTimestamp());
$this->assignmentsStorage->add($assignment);

return $this;
Expand Down Expand Up @@ -391,7 +395,7 @@ private function addItem(Permission|Role $item): void
throw new ItemAlreadyExistsException($item);
}

$time = time();
$time = $this->getCurrentTimestamp();
if (!$item->hasCreatedAt()) {
$item = $item->withCreatedAt($time);
}
Expand Down Expand Up @@ -528,4 +532,11 @@ private function filterUserItemNames(string $userId, array $itemNames): array
$this->defaultRoleNames,
);
}

private function getCurrentTimestamp(): int
{
return $this->clock === null
? time()
: $this->clock->now()->getTimestamp();
}
}
12 changes: 1 addition & 11 deletions tests/Common/AssignmentsStorageTestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

namespace Yiisoft\Rbac\Tests\Common;

use DateTime;
use SlopeIt\ClockMock\ClockMock;
use Yiisoft\Rbac\Assignment;
use Yiisoft\Rbac\AssignmentsStorageInterface;
use Yiisoft\Rbac\Item;
Expand All @@ -22,20 +20,12 @@ trait AssignmentsStorageTestTrait

protected function setUp(): void
{
if ($this->name() === 'testAddWithCurrentTimestamp') {
ClockMock::freeze(new DateTime('2023-05-10 08:24:39'));
}

$this->populateItemsStorage();
$this->populateAssignmentsStorage();
}

protected function tearDown(): void
{
if ($this->name() === 'testAddWithCurrentTimestamp') {
ClockMock::reset();
}

$this->getItemsStorage()->clear();
$this->getAssignmentsStorage()->clear();
}
Expand Down Expand Up @@ -263,7 +253,7 @@ public function testAddWithCurrentTimestamp(): void
{
$testStorage = $this->getAssignmentsStorageForModificationAssertions();
$actionStorage = $this->getAssignmentsStorage();
$actionStorage->add(new Assignment(userId: 'john', itemName: 'Operator', createdAt: time()));
$actionStorage->add(new Assignment(userId: 'john', itemName: 'Operator', createdAt: 1_683_707_079));

$this->assertEquals(
new Assignment(userId: 'john', itemName: 'Operator', createdAt: 1_683_707_079),
Expand Down
22 changes: 4 additions & 18 deletions tests/Common/ItemsStorageTestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace Yiisoft\Rbac\Tests\Common;

use DateTime;
use SlopeIt\ClockMock\ClockMock;
use PHPUnit\Framework\Attributes\DataProvider;
use Yiisoft\Rbac\Item;
use Yiisoft\Rbac\ItemsStorageInterface;
use Yiisoft\Rbac\Permission;
Expand All @@ -25,23 +25,11 @@ trait ItemsStorageTestTrait

protected function setUp(): void
{
if ($this->name() === 'testAddWithCurrentTimestamps') {
ClockMock::freeze(new DateTime('2023-05-10 08:24:39'));
}

if ($this->name() === 'testGetHierarchy') {
ClockMock::freeze(new DateTime('2023-12-24 17:51:18'));
}

$this->populateItemsStorage();
}

protected function tearDown(): void
{
if (in_array($this->name(), ['testAddWithCurrentTimestamps', 'testGetHierarchy'], strict: true)) {
ClockMock::reset();
}

$this->getItemsStorage()->clear();
}

Expand Down Expand Up @@ -489,9 +477,7 @@ public static function dataGetHierarchy(): array
];
}

/**
* @dataProvider dataGetHierarchy
*/
#[DataProvider('dataGetHierarchy')]
public function testGetHierarchy(string $name, array $expectedHierarchy): void
{
$this->assertEquals($expectedHierarchy, $this->getItemsStorage()->getHierarchy($name));
Expand Down Expand Up @@ -531,7 +517,7 @@ public function testAddWithCurrentTimestamps(): void
{
$testStorage = $this->getItemsStorageForModificationAssertions();

$time = time();
$time = 1_683_707_079;
$newItem = (new Permission('Delete post'))->withCreatedAt($time)->withUpdatedAt($time);

$actionStorage = $this->getItemsStorage();
Expand Down Expand Up @@ -765,7 +751,7 @@ protected function getFixtures(): array
'posts.update' => Item::TYPE_PERMISSION,
'posts.delete' => Item::TYPE_PERMISSION,
];
$time = time();
$time = 1703440278;

$items = [];
foreach ($itemsMap as $name => $type) {
Expand Down
15 changes: 15 additions & 0 deletions tests/Common/ManagerConfigurationTestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace Yiisoft\Rbac\Tests\Common;

use DateTimeImmutable;
use Psr\Clock\ClockInterface;
use Yiisoft\Rbac\AssignmentsStorageInterface;
use Yiisoft\Rbac\ItemsStorageInterface;
use Yiisoft\Rbac\Manager;
Expand All @@ -21,10 +23,23 @@ protected function createManager(
?AssignmentsStorageInterface $assignmentsStorage = null,
?bool $enableDirectPermissions = null,
?bool $includeRolesInAccessChecks = null,
?DateTimeImmutable $currentDateTime = null,
): ManagerInterface {
$arguments = [
'itemsStorage' => $itemsStorage ?? $this->createItemsStorage(),
'assignmentsStorage' => $assignmentsStorage ?? $this->createAssignmentsStorage(),
'clock' => $currentDateTime === null
? null
: new class ($currentDateTime) implements ClockInterface {
public function __construct(private readonly DateTimeImmutable $dateTime)
{
}

public function now(): DateTimeImmutable
{
return $this->dateTime;
}
},
];
if ($enableDirectPermissions !== null) {
$arguments['enableDirectPermissions'] = $enableDirectPermissions;
Expand Down
45 changes: 16 additions & 29 deletions tests/Common/ManagerLogicTestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
namespace Yiisoft\Rbac\Tests\Common;

use Closure;
use DateTime;
use DateTimeImmutable;
use InvalidArgumentException;
use RuntimeException;
use SlopeIt\ClockMock\ClockMock;
use Yiisoft\Rbac\Assignment;
use Yiisoft\Rbac\Exception\DefaultRolesNotFoundException;
use Yiisoft\Rbac\Exception\ItemAlreadyExistsException;
Expand All @@ -30,22 +29,6 @@

trait ManagerLogicTestTrait
{
private static array $frozenTimeTests = ['testAssign', 'testDataPersistency'];

protected function setUp(): void
{
if (in_array($this->name(), self::$frozenTimeTests, strict: true)) {
ClockMock::freeze(new DateTime('2023-05-10 08:24:39'));
}
}

protected function tearDown(): void
{
if (in_array($this->name(), self::$frozenTimeTests, strict: true)) {
ClockMock::reset();
}
}

public static function dataUserHasPermissionGeneric(): array
{
return [
Expand Down Expand Up @@ -543,15 +526,15 @@ public function testHasChild(): void
$this->assertFalse($manager->hasChild('reader', 'createPost'));
}

/**
* Relies on {@see ClockMock} for testing timestamp. When using with other PHPUnit classes / traits, make sure to
* call {@see setUp} and {@see tearDown} methods explicitly.
*/
public function testAssign(): void
{
$itemsStorage = $this->createItemsStorage();
$assignmentsStorage = $this->createAssignmentsStorage();
$manager = $this->createManager($itemsStorage, $assignmentsStorage)
$manager = $this->createManager(
$itemsStorage,
$assignmentsStorage,
currentDateTime: new DateTimeImmutable('2023-05-10 08:24:39')
)
->addRole(new Role('author'))
->addRole(new Role('reader'))
->addRole(new Role('writer'))
Expand Down Expand Up @@ -895,12 +878,12 @@ public static function dataSetDefaultRoleNamesException(): array
return [
[['test1', 2, 'test3'], InvalidArgumentException::class, 'Each role name must be a string.'],
[
static fn (): string => 'test',
static fn(): string => 'test',
InvalidArgumentException::class,
'Default role names closure must return an array.',
],
[
static fn (): array => ['test1', 2, 'test3'],
static fn(): array => ['test1', 2, 'test3'],
InvalidArgumentException::class,
'Each role name must be a string.',
],
Expand All @@ -925,10 +908,10 @@ public static function dataSetDefaultRoleNames(): array
return [
[['defaultRole1'], ['defaultRole1']],
[['defaultRole1', 'defaultRole2'], ['defaultRole1', 'defaultRole2']],
[static fn (): array => ['defaultRole1'], ['defaultRole1']],
[static fn (): array => ['defaultRole1', 'defaultRole2'], ['defaultRole1', 'defaultRole2']],
[static fn(): array => ['defaultRole1'], ['defaultRole1']],
[static fn(): array => ['defaultRole1', 'defaultRole2'], ['defaultRole1', 'defaultRole2']],
[[], []],
[static fn (): array => [], []],
[static fn(): array => [], []],
];
}

Expand Down Expand Up @@ -1111,7 +1094,11 @@ public function testDataPersistency(): void
{
$itemsStorage = new FakeItemsStorage();
$assignmentsStorage = new FakeAssignmentsStorage();
$manager = $this->createManager($itemsStorage, $assignmentsStorage);
$manager = $this->createManager(
$itemsStorage,
$assignmentsStorage,
currentDateTime: new DateTimeImmutable('2023-05-10 08:24:39'),
);
$manager
->addRole((new Role('role1'))->withCreatedAt(1_694_502_936)->withUpdatedAt(1_694_502_936))
->addRole((new Role('role2'))->withCreatedAt(1_694_502_976)->withUpdatedAt(1_694_502_976))
Expand Down