Merge pull request #10819 from andrewandante/FEAT_add_only_individual_users_inherited_permission

NEW add OnlyTheseMembers Inherited Permission type
This commit is contained in:
Guy Sartorelli 2023-07-07 09:37:59 +12:00 committed by GitHub
commit 62bd560d0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 146 additions and 19 deletions

View File

@ -248,17 +248,20 @@ class ListboxField extends MultiSelectField
} }
$canary = reset($validValues); $canary = reset($validValues);
$targetType = gettype($canary);
if (is_array($value) && count($value) > 0) { if (is_array($value) && count($value) > 0) {
$first = reset($value); $first = reset($value);
// sanity check the values - make sure strings get strings, ints get ints etc // sanity check the values - make sure strings get strings, ints get ints etc
if (gettype($canary) !== gettype($first)) { if ($targetType !== gettype($first)) {
$replaced = []; $replaced = [];
foreach ($value as $item) { foreach ($value as $item) {
if (!is_array($item)) { if (!is_array($item)) {
$item = json_decode($item, true); $item = json_decode($item, true);
} }
if (isset($item['Value'])) { if ($targetType === gettype($item)) {
$replaced[] = $item;
} elseif (isset($item['Value'])) {
$replaced[] = $item['Value']; $replaced[] = $item['Value'];
} }
} }

View File

@ -10,6 +10,7 @@ use SilverStripe\ORM\Hierarchy\Hierarchy;
use SilverStripe\Versioned\Versioned; use SilverStripe\Versioned\Versioned;
use Psr\SimpleCache\CacheInterface; use Psr\SimpleCache\CacheInterface;
use SilverStripe\Core\Cache\MemberCacheFlusher; use SilverStripe\Core\Cache\MemberCacheFlusher;
use SilverStripe\Dev\Deprecation;
/** /**
* Calculates batch permissions for nested objects for: * Calculates batch permissions for nested objects for:
@ -25,37 +26,42 @@ class InheritedPermissions implements PermissionChecker, MemberCacheFlusher
/** /**
* Delete permission * Delete permission
*/ */
const DELETE = 'delete'; public const DELETE = 'delete';
/** /**
* View permission * View permission
*/ */
const VIEW = 'view'; public const VIEW = 'view';
/** /**
* Edit permission * Edit permission
*/ */
const EDIT = 'edit'; public const EDIT = 'edit';
/** /**
* Anyone canView permission * Anyone canView permission
*/ */
const ANYONE = 'Anyone'; public const ANYONE = 'Anyone';
/** /**
* Restrict to logged in users * Restrict to logged in users
*/ */
const LOGGED_IN_USERS = 'LoggedInUsers'; public const LOGGED_IN_USERS = 'LoggedInUsers';
/** /**
* Restrict to specific groups * Restrict to specific groups
*/ */
const ONLY_THESE_USERS = 'OnlyTheseUsers'; public const ONLY_THESE_USERS = 'OnlyTheseUsers';
/**
* Restrict to specific members
*/
public const ONLY_THESE_MEMBERS = 'OnlyTheseMembers';
/** /**
* Inherit from parent * Inherit from parent
*/ */
const INHERIT = 'Inherit'; public const INHERIT = 'Inherit';
/** /**
* Class name * Class name
@ -359,19 +365,27 @@ class InheritedPermissions implements PermissionChecker, MemberCacheFlusher
if ($member && $member->ID) { if ($member && $member->ID) {
if (!Permission::checkMember($member, 'ADMIN')) { if (!Permission::checkMember($member, 'ADMIN')) {
// Determine if this member matches any of the group or other rules // Determine if this member matches any of the group or other rules
$groupJoinTable = $this->getJoinTable($type); $groupJoinTable = $this->getGroupJoinTable($type);
$memberJoinTable = $this->getMemberJoinTable($type);
$uninheritedPermissions = $stageRecords $uninheritedPermissions = $stageRecords
->where([ ->where([
"(\"$typeField\" IN (?, ?) OR " . "(\"$typeField\" = ? AND \"$groupJoinTable\".\"{$baseTable}ID\" IS NOT NULL))" "(\"$typeField\" IN (?, ?)"
. " OR (\"$typeField\" = ? AND \"$groupJoinTable\".\"{$baseTable}ID\" IS NOT NULL)"
. " OR (\"$typeField\" = ? AND \"$memberJoinTable\".\"{$baseTable}ID\" IS NOT NULL)"
. ")"
=> [ => [
self::ANYONE, self::ANYONE,
self::LOGGED_IN_USERS, self::LOGGED_IN_USERS,
self::ONLY_THESE_USERS self::ONLY_THESE_USERS,
self::ONLY_THESE_MEMBERS,
] ]
]) ])
->leftJoin( ->leftJoin(
$groupJoinTable, $groupJoinTable,
"\"$groupJoinTable\".\"{$baseTable}ID\" = \"{$baseTable}\".\"ID\" AND " . "\"$groupJoinTable\".\"GroupID\" IN ($groupIDsSQLList)" "\"$groupJoinTable\".\"{$baseTable}ID\" = \"{$baseTable}\".\"ID\" AND " . "\"$groupJoinTable\".\"GroupID\" IN ($groupIDsSQLList)"
)->leftJoin(
$memberJoinTable,
"\"$memberJoinTable\".\"{$baseTable}ID\" = \"{$baseTable}\".\"ID\" AND " . "\"$memberJoinTable\".\"MemberID\" = {$member->ID}"
)->column('ID'); )->column('ID');
} else { } else {
$uninheritedPermissions = $stageRecords->column('ID'); $uninheritedPermissions = $stageRecords->column('ID');
@ -628,10 +642,24 @@ class InheritedPermissions implements PermissionChecker, MemberCacheFlusher
* Get join table for type * Get join table for type
* Defaults to those provided by {@see InheritedPermissionsExtension) * Defaults to those provided by {@see InheritedPermissionsExtension)
* *
* @deprecated 5.1.0 Use getGroupJoinTable() instead
* @param string $type * @param string $type
* @return string * @return string
*/ */
protected function getJoinTable($type) protected function getJoinTable($type)
{
Deprecation::notice('5.1.0', 'Use getGroupJoinTable() instead');
return $this->getGroupJoinTable($type);
}
/**
* Get group join table for type
* Defaults to those provided by {@see InheritedPermissionsExtension)
*
* @param string $type
* @return string
*/
protected function getGroupJoinTable($type)
{ {
switch ($type) { switch ($type) {
case self::DELETE: case self::DELETE:
@ -645,6 +673,27 @@ class InheritedPermissions implements PermissionChecker, MemberCacheFlusher
} }
} }
/**
* Get member join table for type
* Defaults to those provided by {@see InheritedPermissionsExtension)
*
* @param string $type
* @return string
*/
protected function getMemberJoinTable($type)
{
switch ($type) {
case self::DELETE:
// Delete uses edit type - Drop through
case self::EDIT:
return $this->getEditorMembersTable();
case self::VIEW:
return $this->getViewerMembersTable();
default:
throw new InvalidArgumentException("Invalid argument type $type");
}
}
/** /**
* Determine default permission for a givion check * Determine default permission for a givion check
* *
@ -716,6 +765,28 @@ class InheritedPermissions implements PermissionChecker, MemberCacheFlusher
return "{$table}_ViewerGroups"; return "{$table}_ViewerGroups";
} }
/**
* Get table to use for editor members relation
*
* @return string
*/
protected function getEditorMembersTable()
{
$table = DataObject::getSchema()->tableName($this->baseClass);
return "{$table}_EditorMembers";
}
/**
* Get table to use for viewer members relation
*
* @return string
*/
protected function getViewerMembersTable()
{
$table = DataObject::getSchema()->tableName($this->baseClass);
return "{$table}_ViewerMembers";
}
/** /**
* Gets the permission from cache * Gets the permission from cache
* *

View File

@ -12,17 +12,21 @@ use SilverStripe\ORM\ManyManyList;
* @property string $CanEditType * @property string $CanEditType
* @method ManyManyList ViewerGroups() * @method ManyManyList ViewerGroups()
* @method ManyManyList EditorGroups() * @method ManyManyList EditorGroups()
* @method ManyManyList ViewerMembers()
* @method ManyManyList EditorMembers()
*/ */
class InheritedPermissionsExtension extends DataExtension class InheritedPermissionsExtension extends DataExtension
{ {
private static array $db = [ private static array $db = [
'CanViewType' => "Enum('Anyone, LoggedInUsers, OnlyTheseUsers, Inherit', 'Inherit')", 'CanViewType' => "Enum('Anyone, LoggedInUsers, OnlyTheseUsers, OnlyTheseMembers, Inherit', 'Inherit')",
'CanEditType' => "Enum('LoggedInUsers, OnlyTheseUsers, Inherit', 'Inherit')", 'CanEditType' => "Enum('LoggedInUsers, OnlyTheseUsers, OnlyTheseMembers, Inherit', 'Inherit')",
]; ];
private static array $many_many = [ private static array $many_many = [
'ViewerGroups' => Group::class, 'ViewerGroups' => Group::class,
'EditorGroups' => Group::class, 'EditorGroups' => Group::class,
'ViewerMembers' => Member::class,
'EditorMembers' => Member::class,
]; ];
private static array $defaults = [ private static array $defaults = [
@ -33,5 +37,7 @@ class InheritedPermissionsExtension extends DataExtension
private static array $cascade_duplicates = [ private static array $cascade_duplicates = [
'ViewerGroups', 'ViewerGroups',
'EditorGroups', 'EditorGroups',
'ViewerMembers',
'EditorMembers',
]; ];
} }

View File

@ -68,6 +68,7 @@ class InheritedPermissionsTest extends SapphireTest
public function testEditPermissions() public function testEditPermissions()
{ {
$editor = $this->objFromFixture(Member::class, 'editor'); $editor = $this->objFromFixture(Member::class, 'editor');
$freddie = $this->objFromFixture(Member::class, 'oneFileFreddie');
$about = $this->objFromFixture(TestPermissionNode::class, 'about'); $about = $this->objFromFixture(TestPermissionNode::class, 'about');
$aboutStaff = $this->objFromFixture(TestPermissionNode::class, 'about-staff'); $aboutStaff = $this->objFromFixture(TestPermissionNode::class, 'about-staff');
@ -75,10 +76,12 @@ class InheritedPermissionsTest extends SapphireTest
$products = $this->objFromFixture(TestPermissionNode::class, 'products'); $products = $this->objFromFixture(TestPermissionNode::class, 'products');
$product1 = $this->objFromFixture(TestPermissionNode::class, 'products-product1'); $product1 = $this->objFromFixture(TestPermissionNode::class, 'products-product1');
$product4 = $this->objFromFixture(TestPermissionNode::class, 'products-product4'); $product4 = $this->objFromFixture(TestPermissionNode::class, 'products-product4');
$freddiesFile = $this->objFromFixture(TestPermissionNode::class, 'freddies-file');
// Test logged out users cannot edit // Test logged out users cannot edit
Member::actAs(null, function () use ($aboutStaff) { Member::actAs(null, function () use ($aboutStaff, $freddiesFile) {
$this->assertFalse($aboutStaff->canEdit()); $this->assertFalse($aboutStaff->canEdit());
$this->assertFalse($freddiesFile->canEdit());
}); });
// Can't edit a page that is locked to admins // Can't edit a page that is locked to admins
@ -96,6 +99,10 @@ class InheritedPermissionsTest extends SapphireTest
// Test that root node respects root permissions // Test that root node respects root permissions
$this->assertTrue($history->canEdit($editor)); $this->assertTrue($history->canEdit($editor));
// Test that only Freddie can edit Freddie's file
$this->assertFalse($freddiesFile->canEdit($editor));
$this->assertTrue($freddiesFile->canEdit($freddie));
TestPermissionNode::getInheritedPermissions()->clearCache(); TestPermissionNode::getInheritedPermissions()->clearCache();
$this->rootPermissions->setCanEdit(false); $this->rootPermissions->setCanEdit(false);
@ -106,6 +113,7 @@ class InheritedPermissionsTest extends SapphireTest
public function testDeletePermissions() public function testDeletePermissions()
{ {
$editor = $this->objFromFixture(Member::class, 'editor'); $editor = $this->objFromFixture(Member::class, 'editor');
$freddie = $this->objFromFixture(Member::class, 'oneFileFreddie');
$about = $this->objFromFixture(TestPermissionNode::class, 'about'); $about = $this->objFromFixture(TestPermissionNode::class, 'about');
$aboutStaff = $this->objFromFixture(TestPermissionNode::class, 'about-staff'); $aboutStaff = $this->objFromFixture(TestPermissionNode::class, 'about-staff');
@ -113,10 +121,12 @@ class InheritedPermissionsTest extends SapphireTest
$products = $this->objFromFixture(TestPermissionNode::class, 'products'); $products = $this->objFromFixture(TestPermissionNode::class, 'products');
$product1 = $this->objFromFixture(TestPermissionNode::class, 'products-product1'); $product1 = $this->objFromFixture(TestPermissionNode::class, 'products-product1');
$product4 = $this->objFromFixture(TestPermissionNode::class, 'products-product4'); $product4 = $this->objFromFixture(TestPermissionNode::class, 'products-product4');
$freddiesFile = $this->objFromFixture(TestPermissionNode::class, 'freddies-file');
// Test logged out users cannot edit // Test logged out users cannot edit
Member::actAs(null, function () use ($aboutStaff) { Member::actAs(null, function () use ($aboutStaff, $freddiesFile) {
$this->assertFalse($aboutStaff->canDelete()); $this->assertFalse($aboutStaff->canDelete());
$this->assertFalse($freddiesFile->canDelete());
}); });
// Can't edit a page that is locked to admins // Can't edit a page that is locked to admins
@ -134,6 +144,10 @@ class InheritedPermissionsTest extends SapphireTest
// Test that root node respects root permissions // Test that root node respects root permissions
$this->assertTrue($history->canDelete($editor)); $this->assertTrue($history->canDelete($editor));
// Test that only Freddie can delete Freddie's file
$this->assertFalse($freddiesFile->canDelete($editor));
$this->assertTrue($freddiesFile->canDelete($freddie));
TestPermissionNode::getInheritedPermissions()->clearCache(); TestPermissionNode::getInheritedPermissions()->clearCache();
$this->rootPermissions->setCanEdit(false); $this->rootPermissions->setCanEdit(false);
@ -150,14 +164,17 @@ class InheritedPermissionsTest extends SapphireTest
$secretNested = $this->objFromFixture(TestPermissionNode::class, 'secret-nested'); $secretNested = $this->objFromFixture(TestPermissionNode::class, 'secret-nested');
$protected = $this->objFromFixture(TestPermissionNode::class, 'protected'); $protected = $this->objFromFixture(TestPermissionNode::class, 'protected');
$protectedChild = $this->objFromFixture(TestPermissionNode::class, 'protected-child'); $protectedChild = $this->objFromFixture(TestPermissionNode::class, 'protected-child');
$editor = $this->objFromFixture(Member::class, 'editor');
$restricted = $this->objFromFixture(TestPermissionNode::class, 'restricted-page'); $restricted = $this->objFromFixture(TestPermissionNode::class, 'restricted-page');
$freddiesFile = $this->objFromFixture(TestPermissionNode::class, 'freddies-file');
$editor = $this->objFromFixture(Member::class, 'editor');
$admin = $this->objFromFixture(Member::class, 'admin'); $admin = $this->objFromFixture(Member::class, 'admin');
$freddie = $this->objFromFixture(Member::class, 'oneFileFreddie');
// Not logged in user can only access Inherit or Anyone pages // Not logged in user can only access Inherit or Anyone pages
Member::actAs( Member::actAs(
null, null,
function () use ($protectedChild, $secretNested, $protected, $secret, $history, $contact, $contactForm) { function () use ($protectedChild, $secretNested, $protected, $secret, $history, $contact, $contactForm, $freddiesFile) {
$this->assertTrue($history->canView()); $this->assertTrue($history->canView());
$this->assertTrue($contact->canView()); $this->assertTrue($contact->canView());
$this->assertTrue($contactForm->canView()); $this->assertTrue($contactForm->canView());
@ -166,6 +183,7 @@ class InheritedPermissionsTest extends SapphireTest
$this->assertFalse($secretNested->canView()); $this->assertFalse($secretNested->canView());
$this->assertFalse($protected->canView()); $this->assertFalse($protected->canView());
$this->assertFalse($protectedChild->canView()); $this->assertFalse($protectedChild->canView());
$this->assertFalse($freddiesFile->canView());
} }
); );
@ -180,6 +198,10 @@ class InheritedPermissionsTest extends SapphireTest
// Check root permissions // Check root permissions
$this->assertTrue($history->canView($editor)); $this->assertTrue($history->canView($editor));
// Test that only Freddie can view Freddie's file
$this->assertFalse($freddiesFile->canView($editor));
$this->assertTrue($freddiesFile->canView($freddie));
TestPermissionNode::getInheritedPermissions()->clearCache(); TestPermissionNode::getInheritedPermissions()->clearCache();
$this->rootPermissions->setCanView(false); $this->rootPermissions->setCanView(false);
@ -198,12 +220,16 @@ class InheritedPermissionsTest extends SapphireTest
$secretNested = $this->objFromFixture(UnstagedNode::class, 'secret-nested'); $secretNested = $this->objFromFixture(UnstagedNode::class, 'secret-nested');
$protected = $this->objFromFixture(UnstagedNode::class, 'protected'); $protected = $this->objFromFixture(UnstagedNode::class, 'protected');
$protectedChild = $this->objFromFixture(UnstagedNode::class, 'protected-child'); $protectedChild = $this->objFromFixture(UnstagedNode::class, 'protected-child');
$freddiesFile = $this->objFromFixture(UnstagedNode::class, 'freddies-file');
$editor = $this->objFromFixture(Member::class, 'editor'); $editor = $this->objFromFixture(Member::class, 'editor');
$freddie = $this->objFromFixture(Member::class, 'oneFileFreddie');
// Not logged in user can only access Inherit or Anyone pages // Not logged in user can only access Inherit or Anyone pages
Member::actAs( Member::actAs(
null, null,
function () use ($protectedChild, $secretNested, $protected, $secret, $history, $contact, $contactForm) { function () use ($protectedChild, $secretNested, $protected, $secret, $history, $contact, $contactForm, $freddiesFile) {
$this->assertTrue($history->canView()); $this->assertTrue($history->canView());
$this->assertTrue($contact->canView()); $this->assertTrue($contact->canView());
$this->assertTrue($contactForm->canView()); $this->assertTrue($contactForm->canView());
@ -212,6 +238,7 @@ class InheritedPermissionsTest extends SapphireTest
$this->assertFalse($secretNested->canView()); $this->assertFalse($secretNested->canView());
$this->assertFalse($protected->canView()); $this->assertFalse($protected->canView());
$this->assertFalse($protectedChild->canView()); $this->assertFalse($protectedChild->canView());
$this->assertFalse($freddiesFile->canView());
} }
); );
@ -226,6 +253,10 @@ class InheritedPermissionsTest extends SapphireTest
// Check root permissions // Check root permissions
$this->assertTrue($history->canView($editor)); $this->assertTrue($history->canView($editor));
// Test that only Freddie can view Freddie's file
$this->assertFalse($freddiesFile->canView($editor));
$this->assertTrue($freddiesFile->canView($freddie));
UnstagedNode::getInheritedPermissions()->clearCache(); UnstagedNode::getInheritedPermissions()->clearCache();
$this->rootPermissions->setCanView(false); $this->rootPermissions->setCanView(false);

View File

@ -33,6 +33,10 @@ SilverStripe\Security\Member:
Groups: =>SilverStripe\Security\Group.allsections Groups: =>SilverStripe\Security\Group.allsections
securityadmin: securityadmin:
Groups: =>SilverStripe\Security\Group.securityadmins Groups: =>SilverStripe\Security\Group.securityadmins
oneFileFreddie:
FirstName: Freddie
Surname: Fantastic
Groups: =>SilverStripe\Security\Group.editors
SilverStripe\Security\Tests\InheritedPermissionsTest\TestPermissionNode: SilverStripe\Security\Tests\InheritedPermissionsTest\TestPermissionNode:
about: about:
@ -104,6 +108,12 @@ SilverStripe\Security\Tests\InheritedPermissionsTest\TestPermissionNode:
Title: Restricted Page Title: Restricted Page
CanViewType: OnlyTheseUsers CanViewType: OnlyTheseUsers
ViewerGroups: =>SilverStripe\Security\Group.allsections ViewerGroups: =>SilverStripe\Security\Group.allsections
freddies-file:
Title: Freddies File
CanViewType: OnlyTheseMembers
CanEditType: OnlyTheseMembers
ViewerMembers: =>SilverStripe\Security\Member.oneFileFreddie
EditorMembers: =>SilverStripe\Security\Member.oneFileFreddie
SilverStripe\Security\Tests\InheritedPermissionsTest\UnstagedNode: SilverStripe\Security\Tests\InheritedPermissionsTest\UnstagedNode:
about: about:
@ -175,3 +185,9 @@ SilverStripe\Security\Tests\InheritedPermissionsTest\UnstagedNode:
Title: Restricted Page Title: Restricted Page
CanViewType: OnlyTheseUsers CanViewType: OnlyTheseUsers
ViewerGroups: =>SilverStripe\Security\Group.allsections ViewerGroups: =>SilverStripe\Security\Group.allsections
freddies-file:
Title: Freddies File
CanViewType: OnlyTheseMembers
CanEditType: OnlyTheseMembers
ViewerMembers: =>SilverStripe\Security\Member.oneFileFreddie
EditorMembers: =>SilverStripe\Security\Member.oneFileFreddie