Add new InheritedPermissionFlusher extension, CacheFlusher service

This commit is contained in:
Aaron Carlino 2017-12-04 13:12:13 +13:00 committed by Damian Mooyman
parent 71c80d3762
commit eecb9f64d3
7 changed files with 340 additions and 2 deletions

9
_config/extensions.yml Normal file
View File

@ -0,0 +1,9 @@
---
Name: coreextensions
---
SilverStripe\Security\Member:
extensions:
- SilverStripe\Security\InheritedPermissionFlusher
SilverStripe\Security\Group:
extensions:
- SilverStripe\Security\InheritedPermissionFlusher

View File

@ -0,0 +1,16 @@
<?php
namespace SilverStripe\Core\Cache;
/**
* Defines a service that can flush its cache for a list of members
* @package SilverStripe\Core\Cache
*/
interface CacheFlusher
{
/**
* @param null $ids
* @return mixed
*/
public function flushCache($ids = null);
}

View File

@ -327,9 +327,16 @@ class Group extends DataObject
$query->removeFilterOn('Group_Members');
});
}
// Now set all children groups as a new foreign key
$groups = Group::get()->byIDs($this->collateFamilyIDs());
$result = $result->forForeignID($groups->column('ID'))->where($filter);
$familyIDs = $this->collateFamilyIDs();
if (!empty($familyIDs)) {
$groups = Group::get()->byIDs($familyIDs);
$groupIDs = $groups->column('ID');
if (!empty($groupIDs)) {
$result = $result->forForeignID($groupIDs)->where($filter);
}
}
return $result;
}

View File

@ -0,0 +1,104 @@
<?php
namespace SilverStripe\Security;
use Psr\Log\InvalidArgumentException;
use SilverStripe\Core\Flushable;
use SilverStripe\ORM\DataExtension;
use SilverStripe\ORM\DataObject;
use SilverStripe\Core\Cache\CacheFlusher;
class InheritedPermissionFlusher extends DataExtension implements Flushable
{
/**
* @var CacheFlusher[]
*/
protected $services = [];
/**
* Flush all CacheFlusher services
*/
public static function flush()
{
singleton(__CLASS__)->flushCache();
}
/**
* @param DataObject $owner
*/
public function setOwner($owner)
{
if (!$owner instanceof Member && !$owner instanceof Group) {
throw new InvalidArgumentException(sprintf(
'%s can only be applied to %s or %s',
__CLASS__,
Member::class,
Group::class
));
}
parent::setOwner($owner);
}
/**
* @param CacheFlusher[]
*/
public function setServices($services)
{
foreach ($services as $service) {
if (!$service instanceof CacheFlusher) {
throw new InvalidArgumentException(sprintf(
'%s.services must contain only %s instances. %s provided.',
__CLASS__,
CacheFlusher::class,
get_class($service)
));
}
}
$this->services = $services;
}
/**
* @return CacheFlusher[]
*/
public function getServices()
{
return $this->services;
}
/**
* Flushes all registered CacheFlusher services
*/
public function flushCache()
{
$ids = $this->getMemberIDList();
foreach ($this->services as $service) {
$service->flushCache($ids);
}
}
/**
* Get a list of member IDs that need their permissions flushed
*
* @return array|null
*/
protected function getMemberIDList()
{
if (!$this->owner) {
return null;
}
if (!$this->owner->exists()) {
return null;
}
if ($this->owner instanceof Group) {
return $this->owner->Members()->exists()
? $this->owner->Members()->column('ID')
: null;
}
return [$this->owner->ID];
}
}

View File

@ -0,0 +1,100 @@
<?php
namespace SilverStripe\Security\Tests;
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Core\Cache\CacheFactory;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Security\InheritedPermissionFlusher;
use SilverStripe\Security\Member;
use SilverStripe\Security\Group;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Security\Tests\InheritedPermissionsFlusherTest\TestCacheFlusher;
use SilverStripe\Core\Config\Config;
class InheritedPermissionsFlusherTest extends SapphireTest
{
protected static $fixture_file = 'InheritedPermissionsFlusherTest.yml';
public function setUp()
{
parent::setUp();
// Set up a mock cache service
Injector::inst()->load([
CacheInterface::class . '.TestFlusherCache' => [
'factory' => CacheFactory::class,
'constructor' => ['namespace' => 'TestFlusherCache']
]
]);
}
public function testMemberFlushesPermissions()
{
$cache = Injector::inst()->create(CacheInterface::class . '.TestFlusherCache');
$flusher = new TestCacheFlusher($cache);
$extension = new InheritedPermissionFlusher();
$extension->setServices([$flusher]);
Injector::inst()->registerService($extension, InheritedPermissionFlusher::class);
$editor = $this->objFromFixture(Member::class, 'editor');
$admin = $this->objFromFixture(Member::class, 'admin');
$editorKey = $flusher->generateCacheKey(TestCacheFlusher::$categories[0], $editor->ID);
$adminKey = $flusher->generateCacheKey(TestCacheFlusher::$categories[0], $admin->ID);
$cache->set($editorKey, 'uncle');
$cache->set($adminKey, 'cheese');
$editor->flushCache();
$this->assertNull($cache->get($editorKey));
$this->assertEquals('cheese', $cache->get($adminKey));
$admin->flushCache();
$this->assertNull($cache->get($editorKey));
$this->assertNull($cache->get($adminKey));
}
public function testGroupFlushesPermissions()
{
$cache = Injector::inst()->create(CacheInterface::class . '.TestFlusherCache');
$flusher = new TestCacheFlusher($cache);
$extension = new InheritedPermissionFlusher();
$extension->setServices([$flusher]);
Injector::inst()->registerService($extension, InheritedPermissionFlusher::class);
$editors = $this->objFromFixture(Group::class, 'editors');
$admins = $this->objFromFixture(Group::class, 'admins');
// Populate the cache for all members in each group
foreach ($editors->Members() as $editor) {
$editorKey = $flusher->generateCacheKey(TestCacheFlusher::$categories[0], $editor->ID);
$cache->set($editorKey, 'uncle');
}
foreach ($admins->Members() as $admin) {
$adminKey = $flusher->generateCacheKey(TestCacheFlusher::$categories[0], $admin->ID);
$cache->set($adminKey, 'cheese');
}
// Clear the cache for all members in the editors group
$editors->flushCache();
foreach ($editors->Members() as $editor) {
$editorKey = $flusher->generateCacheKey(TestCacheFlusher::$categories[0], $editor->ID);
$this->assertNull($cache->get($editorKey));
}
// Admins group should be unaffected
foreach ($admins->Members() as $admin) {
$adminKey = $flusher->generateCacheKey(TestCacheFlusher::$categories[0], $admin->ID);
$this->assertEquals('cheese', $cache->get($adminKey));
}
$admins->flushCache();
// Admins now affected
foreach ($admins->Members() as $admin) {
$adminKey = $flusher->generateCacheKey(TestCacheFlusher::$categories[0], $admin->ID);
$this->assertNull($cache->get($adminKey));
}
foreach ($editors->Members() as $editor) {
$editorKey = $flusher->generateCacheKey(TestCacheFlusher::$categories[0], $editor->ID);
$this->assertNull($cache->get($editorKey));
}
}
}

View File

@ -0,0 +1,35 @@
SilverStripe\Security\Group:
editors:
Title: Editors
admins:
Title: Administrators
allsections:
Title: All Section Editors
securityadmins:
Title: Security Admins
SilverStripe\Security\Permission:
admins:
Code: ADMIN
Group: =>SilverStripe\Security\Group.admins
editors:
Code: CMS_ACCESS_CMSMain
Group: =>SilverStripe\Security\Group.editors
testpermission:
Code: TEST_NODE_ACCESS
Group: =>SilverStripe\Security\Group.editors
SilverStripe\Security\Member:
editor:
FirstName: Test
Surname: Editor
Groups: =>SilverStripe\Security\Group.editors
admin:
FirstName: Test
Surname: Administrator
Groups: =>SilverStripe\Security\Group.admins
allsections:
Groups: =>SilverStripe\Security\Group.allsections
securityadmin:
Groups: =>SilverStripe\Security\Group.securityadmins

View File

@ -0,0 +1,67 @@
<?php
namespace SilverStripe\Security\Tests\InheritedPermissionsFlusherTest;
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Core\Cache\CacheFlusher;
class TestCacheFlusher implements CacheFlusher
{
/**
* @var array
*/
public static $categories = [
'apples',
'pears',
'bananas',
];
/**
* @var CacheInterface
*/
public $cache;
/**
* TestCacheFlusher constructor.
* @param CacheInterface $cache
*/
public function __construct(CacheInterface $cache)
{
$this->cache = $cache;
}
/**
* Clear the cache for this instance only
* @param array $ids A list of member IDs
*/
public function flushCache($ids = null)
{
if (!$this->cache) {
return;
}
// Hard flush, e.g. flush=1
if (!$ids) {
$this->cache->clear();
}
if ($ids && is_array($ids)) {
foreach (self::$categories as $category) {
foreach ($ids as $memberID) {
$key = $this->generateCacheKey($category, $memberID);
$this->cache->delete($key);
}
}
}
}
/**
* @param $category
* @param $memberID
* @return string
*/
public function generateCacheKey($category, $memberID)
{
return "{$category}__{$memberID}";
}
}