silverstripe-framework/tests/php/Security/InheritedPermissionsTest.php

450 lines
19 KiB
PHP

<?php
namespace SilverStripe\Security\Tests;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Security\Group;
use SilverStripe\Security\InheritedPermissions;
use SilverStripe\Security\Member;
use SilverStripe\Security\PermissionChecker;
use SilverStripe\Security\Tests\InheritedPermissionsTest\TestPermissionNode;
use SilverStripe\Security\Tests\InheritedPermissionsTest\TestDefaultPermissionChecker;
use SilverStripe\Security\Tests\InheritedPermissionsTest\UnstagedNode;
use SilverStripe\Versioned\Versioned;
use Psr\SimpleCache\CacheInterface;
use ReflectionClass;
class InheritedPermissionsTest extends SapphireTest
{
protected static $fixture_file = 'InheritedPermissionsTest.yml';
protected static $extra_dataobjects = [
TestPermissionNode::class,
UnstagedNode::class,
];
/**
* @var TestDefaultPermissionChecker
*/
protected $rootPermissions = null;
protected function setUp(): void
{
$this->rootPermissions = new TestDefaultPermissionChecker();
// Register root permissions
$permission1 = InheritedPermissions::create(TestPermissionNode::class)
->setGlobalEditPermissions(['TEST_NODE_ACCESS'])
->setDefaultPermissions($this->rootPermissions);
Injector::inst()->registerService(
$permission1,
PermissionChecker::class . '.testpermissions'
);
// Reset root permission
$permission2 = InheritedPermissions::create(UnstagedNode::class)
->setGlobalEditPermissions(['TEST_NODE_ACCESS'])
->setDefaultPermissions($this->rootPermissions);
Injector::inst()->registerService(
$permission2,
PermissionChecker::class . '.unstagedpermissions'
);
parent::setUp();
$permission1->clearCache();
$permission2->clearCache();
}
protected function tearDown(): void
{
Injector::inst()->unregisterNamedObject(PermissionChecker::class . '.testpermissions');
Injector::inst()->unregisterNamedObject(PermissionChecker::class . '.unstagedpermissions');
$this->rootPermissions = null;
parent::tearDown();
}
public function testEditPermissions()
{
$editor = $this->objFromFixture(Member::class, 'editor');
$about = $this->objFromFixture(TestPermissionNode::class, 'about');
$aboutStaff = $this->objFromFixture(TestPermissionNode::class, 'about-staff');
$history = $this->objFromFixture(TestPermissionNode::class, 'history');
$products = $this->objFromFixture(TestPermissionNode::class, 'products');
$product1 = $this->objFromFixture(TestPermissionNode::class, 'products-product1');
$product4 = $this->objFromFixture(TestPermissionNode::class, 'products-product4');
// Test logged out users cannot edit
Member::actAs(null, function () use ($aboutStaff) {
$this->assertFalse($aboutStaff->canEdit());
});
// Can't edit a page that is locked to admins
$this->assertFalse($about->canEdit($editor));
// Can edit a page that is locked to editors
$this->assertTrue($products->canEdit($editor));
// Can edit a child of that page that inherits
$this->assertTrue($product1->canEdit($editor));
// Can't edit a child of that page that has its permissions overridden
$this->assertFalse($product4->canEdit($editor));
// Test that root node respects root permissions
$this->assertTrue($history->canEdit($editor));
TestPermissionNode::getInheritedPermissions()->clearCache();
$this->rootPermissions->setCanEdit(false);
// With root edit false, permissions are now denied for CanEditType = Inherit
$this->assertFalse($history->canEdit($editor));
}
public function testDeletePermissions()
{
$editor = $this->objFromFixture(Member::class, 'editor');
$about = $this->objFromFixture(TestPermissionNode::class, 'about');
$aboutStaff = $this->objFromFixture(TestPermissionNode::class, 'about-staff');
$history = $this->objFromFixture(TestPermissionNode::class, 'history');
$products = $this->objFromFixture(TestPermissionNode::class, 'products');
$product1 = $this->objFromFixture(TestPermissionNode::class, 'products-product1');
$product4 = $this->objFromFixture(TestPermissionNode::class, 'products-product4');
// Test logged out users cannot edit
Member::actAs(null, function () use ($aboutStaff) {
$this->assertFalse($aboutStaff->canDelete());
});
// Can't edit a page that is locked to admins
$this->assertFalse($about->canDelete($editor));
// Can't delete a page if a child (product4) is un-deletable
$this->assertFalse($products->canDelete($editor));
// Can edit a child of that page that inherits
$this->assertTrue($product1->canDelete($editor));
// Can't edit a child of that page that has its permissions overridden
$this->assertFalse($product4->canDelete($editor));
// Test that root node respects root permissions
$this->assertTrue($history->canDelete($editor));
TestPermissionNode::getInheritedPermissions()->clearCache();
$this->rootPermissions->setCanEdit(false);
// With root edit false, permissions are now denied for CanEditType = Inherit
$this->assertFalse($history->canDelete($editor));
}
public function testViewPermissions()
{
$history = $this->objFromFixture(TestPermissionNode::class, 'history');
$contact = $this->objFromFixture(TestPermissionNode::class, 'contact');
$contactForm = $this->objFromFixture(TestPermissionNode::class, 'contact-form');
$secret = $this->objFromFixture(TestPermissionNode::class, 'secret');
$secretNested = $this->objFromFixture(TestPermissionNode::class, 'secret-nested');
$protected = $this->objFromFixture(TestPermissionNode::class, 'protected');
$protectedChild = $this->objFromFixture(TestPermissionNode::class, 'protected-child');
$editor = $this->objFromFixture(Member::class, 'editor');
$restricted = $this->objFromFixture(TestPermissionNode::class, 'restricted-page');
$admin = $this->objFromFixture(Member::class, 'admin');
// Not logged in user can only access Inherit or Anyone pages
Member::actAs(
null,
function () use ($protectedChild, $secretNested, $protected, $secret, $history, $contact, $contactForm) {
$this->assertTrue($history->canView());
$this->assertTrue($contact->canView());
$this->assertTrue($contactForm->canView());
// Protected
$this->assertFalse($secret->canView());
$this->assertFalse($secretNested->canView());
$this->assertFalse($protected->canView());
$this->assertFalse($protectedChild->canView());
}
);
// Editor can view pages restricted to logged in users
$this->assertTrue($secret->canView($editor));
$this->assertTrue($secretNested->canView($editor));
// Cannot read admin-only pages
$this->assertFalse($protected->canView($editor));
$this->assertFalse($protectedChild->canView($editor));
// Check root permissions
$this->assertTrue($history->canView($editor));
TestPermissionNode::getInheritedPermissions()->clearCache();
$this->rootPermissions->setCanView(false);
$this->assertFalse($history->canView($editor));
// Ensure admins can view everything, even if only a certain group is allowed to view it
$this->assertTrue($restricted->canView($admin));
}
public function testUnstagedViewPermissions()
{
$history = $this->objFromFixture(UnstagedNode::class, 'history');
$contact = $this->objFromFixture(UnstagedNode::class, 'contact');
$contactForm = $this->objFromFixture(UnstagedNode::class, 'contact-form');
$secret = $this->objFromFixture(UnstagedNode::class, 'secret');
$secretNested = $this->objFromFixture(UnstagedNode::class, 'secret-nested');
$protected = $this->objFromFixture(UnstagedNode::class, 'protected');
$protectedChild = $this->objFromFixture(UnstagedNode::class, 'protected-child');
$editor = $this->objFromFixture(Member::class, 'editor');
// Not logged in user can only access Inherit or Anyone pages
Member::actAs(
null,
function () use ($protectedChild, $secretNested, $protected, $secret, $history, $contact, $contactForm) {
$this->assertTrue($history->canView());
$this->assertTrue($contact->canView());
$this->assertTrue($contactForm->canView());
// Protected
$this->assertFalse($secret->canView());
$this->assertFalse($secretNested->canView());
$this->assertFalse($protected->canView());
$this->assertFalse($protectedChild->canView());
}
);
// Editor can view pages restricted to logged in users
$this->assertTrue($secret->canView($editor));
$this->assertTrue($secretNested->canView($editor));
// Cannot read admin-only pages
$this->assertFalse($protected->canView($editor));
$this->assertFalse($protectedChild->canView($editor));
// Check root permissions
$this->assertTrue($history->canView($editor));
UnstagedNode::getInheritedPermissions()->clearCache();
$this->rootPermissions->setCanView(false);
$this->assertFalse($history->canView($editor));
}
/**
* Test that draft permissions deny unrestricted live permissions
*/
public function testRestrictedDraftUnrestrictedLive()
{
Versioned::set_stage(Versioned::DRAFT);
// Should be editable by non-admin editor
/** @var TestPermissionNode $products */
$products = $this->objFromFixture(TestPermissionNode::class, 'products');
/** @var TestPermissionNode $products1 */
$products1 = $this->objFromFixture(TestPermissionNode::class, 'products-product1');
$editor = $this->objFromFixture(Member::class, 'editor');
// Ensure the editor can edit
$this->assertTrue($products->canEdit($editor));
$this->assertTrue($products1->canEdit($editor));
// Write current version to live
$products->writeToStage(Versioned::LIVE);
$products1->writeToStage(Versioned::LIVE);
// Draft version restrict to admins
$products->EditorGroups()->setByIDList([
$this->idFromFixture(Group::class, 'admins')
]);
$products->write();
// Ensure editor can no longer edit
TestPermissionNode::getInheritedPermissions()->clearCache();
$this->assertFalse($products->canEdit($editor));
$this->assertFalse($products1->canEdit($editor));
}
/**
* Test that draft permissions permit access over live permissions
*/
public function testUnrestrictedDraftOverridesLive()
{
Versioned::set_stage(Versioned::DRAFT);
// Should be editable by non-admin editor
/** @var TestPermissionNode $about */
$about = $this->objFromFixture(TestPermissionNode::class, 'about');
/** @var TestPermissionNode $aboutStaff */
$aboutStaff = $this->objFromFixture(TestPermissionNode::class, 'about-staff');
$editor = $this->objFromFixture(Member::class, 'editor');
// Ensure the editor can't edit
$this->assertFalse($about->canEdit($editor));
$this->assertFalse($aboutStaff->canEdit($editor));
// Write current version to live
$about->writeToStage(Versioned::LIVE);
$aboutStaff->writeToStage(Versioned::LIVE);
// Unrestrict draft
$about->CanEditType = InheritedPermissions::LOGGED_IN_USERS;
$about->write();
// Ensure editor can no longer edit
TestPermissionNode::getInheritedPermissions()->clearCache();
$this->assertTrue($about->canEdit($editor));
$this->assertTrue($aboutStaff->canEdit($editor));
}
/**
* Ensure that flipping parent / child relationship on live doesn't
* cause infinite loop
*/
public function testMobiusHierarchy()
{
Versioned::set_stage(Versioned::DRAFT);
/** @var TestPermissionNode $history */
$history = $this->objFromFixture(TestPermissionNode::class, 'history');
/** @var TestPermissionNode $historyGallery */
$historyGallery = $this->objFromFixture(TestPermissionNode::class, 'history-gallery');
// Publish current state to live
$history->writeToStage(Versioned::LIVE);
$historyGallery->writeToStage(Versioned::LIVE);
// Flip relation
$historyGallery->ParentID = 0;
$historyGallery->write();
$history->ParentID = $historyGallery->ID;
$history->write();
// Test viewability (not logged in users)
Member::actAs(null, function () use ($history, $historyGallery) {
$this->assertTrue($history->canView());
$this->assertTrue($historyGallery->canView());
});
// Change permission on draft root and ensure it affects both
$historyGallery->CanViewType = InheritedPermissions::LOGGED_IN_USERS;
$historyGallery->write();
TestPermissionNode::getInheritedPermissions()->clearCache();
// Test viewability (not logged in users)
Member::actAs(null, function () use ($history, $historyGallery) {
$this->assertFalse($historyGallery->canView());
$this->assertFalse($history->canView());
});
}
public function testPermissionsPersistCache()
{
/* @var CacheInterface $cache */
$cache = Injector::inst()->create(CacheInterface::class . '.InheritedPermissions');
$cache->clear();
$member = $this->objFromFixture(Member::class, 'editor');
/** @var TestPermissionNode $history */
$history = $this->objFromFixture(TestPermissionNode::class, 'history');
/** @var TestPermissionNode $historyGallery */
$historyGallery = $this->objFromFixture(TestPermissionNode::class, 'history-gallery');
$permissionChecker = new InheritedPermissions(TestPermissionNode::class, $cache);
$viewKey = $this->generateCacheKey($permissionChecker, InheritedPermissions::VIEW, $member->ID);
$editKey = $this->generateCacheKey($permissionChecker, InheritedPermissions::EDIT, $member->ID);
$this->assertNull($cache->get($editKey));
$this->assertNull($cache->get($viewKey));
$permissionChecker->canEditMultiple([$history->ID, $historyGallery->ID], $member);
$this->assertNull($cache->get($editKey));
$this->assertNull($cache->get($viewKey));
unset($permissionChecker);
$this->assertTrue(is_array($cache->get($editKey)));
$this->assertNull($cache->get($viewKey));
$this->assertArrayHasKey($history->ID, $cache->get($editKey));
$this->assertArrayHasKey($historyGallery->ID, $cache->get($editKey));
$permissionChecker = new InheritedPermissions(TestPermissionNode::class, $cache);
$permissionChecker->canViewMultiple([$history->ID], $member);
$this->assertNotNull($cache->get($editKey));
$this->assertNull($cache->get($viewKey));
unset($permissionChecker);
$this->assertTrue(is_array($cache->get($viewKey)));
$this->assertTrue(is_array($cache->get($editKey)));
$this->assertArrayHasKey($history->ID, $cache->get($viewKey));
$this->assertArrayNotHasKey($historyGallery->ID, $cache->get($viewKey));
}
public function testPermissionsFlushCache()
{
/* @var CacheInterface $cache */
$cache = Injector::inst()->create(CacheInterface::class . '.InheritedPermissions');
$cache->clear();
$permissionChecker = new InheritedPermissions(TestPermissionNode::class, $cache);
$member1 = $this->objFromFixture(Member::class, 'editor');
$member2 = $this->objFromFixture(Member::class, 'admin');
$editKey1 = $this->generateCacheKey($permissionChecker, InheritedPermissions::EDIT, $member1->ID);
$editKey2 = $this->generateCacheKey($permissionChecker, InheritedPermissions::EDIT, $member2->ID);
$viewKey1 = $this->generateCacheKey($permissionChecker, InheritedPermissions::VIEW, $member1->ID);
$viewKey2 = $this->generateCacheKey($permissionChecker, InheritedPermissions::VIEW, $member2->ID);
foreach ([$editKey1, $editKey2, $viewKey1, $viewKey2] as $key) {
$this->assertNull($cache->get($key));
}
/** @var TestPermissionNode $history */
$history = $this->objFromFixture(TestPermissionNode::class, 'history');
/** @var TestPermissionNode $historyGallery */
$historyGallery = $this->objFromFixture(TestPermissionNode::class, 'history-gallery');
$permissionChecker->canEditMultiple([$history->ID, $historyGallery->ID], $member1);
$permissionChecker->canViewMultiple([$history->ID, $historyGallery->ID], $member1);
$permissionChecker->canEditMultiple([$history->ID, $historyGallery->ID], $member2);
$permissionChecker->canViewMultiple([$history->ID, $historyGallery->ID], $member2);
unset($permissionChecker);
foreach ([$editKey1, $editKey2, $viewKey1, $viewKey2] as $key) {
$this->assertNotNull($cache->get($key));
}
$permissionChecker = new InheritedPermissions(TestPermissionNode::class, $cache);
// Non existent ID
$permissionChecker->flushMemberCache('dummy');
foreach ([$editKey1, $editKey2, $viewKey1, $viewKey2] as $key) {
$this->assertNotNull($cache->get($key));
}
// Precision strike
$permissionChecker->flushMemberCache([$member1->ID]);
// Member1 should be clear
$this->assertNull($cache->get($editKey1));
$this->assertNull($cache->get($viewKey1));
// Member 2 is unaffected
$this->assertNotNull($cache->get($editKey2));
$this->assertNotNull($cache->get($viewKey2));
// Nuclear
$permissionChecker->flushMemberCache();
foreach ([$editKey1, $editKey2, $viewKey1, $viewKey2] as $key) {
$this->assertNull($cache->get($key));
}
}
protected function generateCacheKey(InheritedPermissions $inst, $type, $memberID)
{
$reflection = new ReflectionClass(InheritedPermissions::class);
$method = $reflection->getMethod('generateCacheKey');
$method->setAccessible(true);
return $method->invokeArgs($inst, [$type, $memberID]);
}
}