API CHANGE Changed SiteTree->Viewers to SiteTree->CanViewType, Changed SiteTree->Editors to SiteTree->CanEditType (see #2847)

API CHANGE Changed SiteTree->ViewersGroup has_one relationship to SiteTree->ViewerGroups has_many relationship (see #2847)
API CHANGE Changed SiteTree->EditorsGroup has_one relationship to SiteTree->EditorGroups has_many relationship (see #2847)
ENHANCEMENT Added 'Inherit' flag to SiteTree->CanViewType and SiteTree->CanEditType (see #2419)
ENHANCEMENT Added unit tests for SiteTree permissions
BUGFIX Checking recursively for permissions on children with SiteTree->canDelete()
BUGFIX Disallow SiteTree->canEdit() if SiteTree->canView() is not granted
Note: Use dev/tasks/UpgradeSiteTreePermissionSchemaTask/run to migrate legacy data to the new schema as outlined above

git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@65150 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Ingo Schommer 2008-11-03 14:52:35 +00:00
parent ab3ef0196d
commit ca6d92341e
3 changed files with 388 additions and 91 deletions

View File

@ -80,10 +80,8 @@ class SiteTree extends DataObject {
"ReportClass" => "Varchar", "ReportClass" => "Varchar",
"Priority" => "Float", "Priority" => "Float",
"Viewers" => "Enum('Anyone, LoggedInUsers, OnlyTheseUsers', 'Anyone')", "CanViewType" => "Enum('Anyone, LoggedInUsers, OnlyTheseUsers, Inherit', 'Anyone')",
"Editors" => "Enum('LoggedInUsers, OnlyTheseUsers', 'LoggedInUsers')", "CanEditType" => "Enum('LoggedInUsers, OnlyTheseUsers, Inherit', 'LoggedInUsers')",
"ViewersGroup" => "Int",
"EditorsGroup" => "Int",
// Simple task tracking // Simple task tracking
"ToDo" => "Text", "ToDo" => "Text",
@ -101,7 +99,9 @@ class SiteTree extends DataObject {
static $many_many = array( static $many_many = array(
"LinkTracking" => "SiteTree", "LinkTracking" => "SiteTree",
"ImageTracking" => "File" "ImageTracking" => "File",
"ViewerGroups" => "Group",
"EditorGroups" => "Group",
); );
static $belongs_many_many = array( static $belongs_many_many = array(
@ -123,9 +123,8 @@ class SiteTree extends DataObject {
"ShowInMenus" => 1, "ShowInMenus" => 1,
"ShowInSearch" => 1, "ShowInSearch" => 1,
"Status" => "New page", "Status" => "New page",
"CanCreateChildren" => array(10), "CanViewType" => "Anyone",
"Viewers" => "Anyone", "CanEditType" => "LoggedInUsers"
"Editors" => "LoggedInUsers"
); );
static $has_one = array( static $has_one = array(
@ -545,11 +544,18 @@ class SiteTree extends DataObject {
/** /**
* This function should return true if the current user can add children * This function should return true if the current user can add children
* to this page. * to this page. It can be overloaded to customise the security model for an
*
* It can be overloaded to customise the security model for an
* application. * application.
* *
* Denies permission if any of the following conditions is TRUE:
* - alternateCanAddChildren() on a decorator returns FALSE
* - canEdit() is not granted
* - There are no classes defined in {@link $allowed_children}
*
* @uses alternateCanAddChildren()
* @uses canEdit()
* @uses $allowed_children
*
* @return boolean True if the current user can add children. * @return boolean True if the current user can add children.
*/ */
public function canAddChildren($member = null) { public function canAddChildren($member = null) {
@ -570,77 +576,116 @@ class SiteTree extends DataObject {
/** /**
* This function should return true if the current user can view this * This function should return true if the current user can view this
* page. * page. It can be overloaded to customise the security model for an
*
* It can be overloaded to customise the security model for an
* application. * application.
* *
* Denies permission if any of the following conditions is TRUE:
* - alternateCanView() on any decorator returns FALSE
* - "CanViewType" directive is set to "Inherit" and any parent page return false for canView()
* - "CanViewType" directive is set to "LoggedInUsers" and no user is logged in
* - "CanViewType" directive is set to "OnlyTheseUsers" and user is not in the given groups
*
* @uses alternateCanView()
* @uses ViewerGroups()
*
* @return boolean True if the current user can view this page. * @return boolean True if the current user can view this page.
*/ */
public function canView($member = null) { public function canView($member = null) {
if(!isset($member)) { if(!isset($member)) $member = Member::currentUser();
$member = Member::currentUser();
}
if($member && $member->isAdmin()) {
return true;
}
// admin override
if($member && $member->isAdmin()) return true;
// decorated access checks
$args = array($member, true); $args = array($member, true);
$this->extend('alternateCanView', $args); $this->extend('alternateCanView', $args);
if($args[1] == false) return false; if($args[1] == false) return false;
if(((!$this->Viewers) || ($this->Viewers == 'Anyone') || // check for empty spec
($this->Viewers == 'LoggedInUsers' && $member) || if(
($this->Viewers == 'OnlyTheseUsers' && $member && !$this->CanViewType || $this->CanViewType == 'Anyone'
$member->inGroup($this->ViewersGroup))) == false) ) return true;
return false;
return true; // check for inherit
if(
$this->CanViewType == 'Inherit' && $this->Parent()
) return $this->Parent()->canView($member);
// check for any logged-in users
if(
$this->CanViewType == 'LoggedInUsers'
&& Member::currentUser()
) return true;
// check for specific groups
if(
$this->CanViewType == 'OnlyTheseUsers'
&& $member
&& $member->inGroups($this->ViewerGroups())
) return true;
return false;
} }
/** /**
* This function should return true if the current user can delete this * This function should return true if the current user can delete this
* page. * page. It can be overloaded to customise the security model for an
*
* It can be overloaded to customise the security model for an
* application. * application.
* *
* Denies permission if any of the following conditions is TRUE:
* - alternateCanDelete() returns FALSE on any decorator
* - canEdit() returns FALSE
* - any descendant page returns FALSE for canDelete()
*
* @todo Check if all children can be deleted as well
* @uses alternateCanDelete()
* @uses canEdit()
*
* @param Member $member * @param Member $member
* @return boolean True if the current user can delete this page. * @return boolean True if the current user can delete this page.
*/ */
public function canDelete($member = null) { public function canDelete($member = null) {
if(!isset($member)) { if(!isset($member)) $member = Member::currentUser();
$member = Member::currentUser();
} if($member && $member->isAdmin()) return true;
if($member && $member->isAdmin()) {
return true;
}
$args = array($member, true); $args = array($member, true);
$this->extend('alternateCanDelete', $args); $this->extend('alternateCanDelete', $args);
if($args[1] == false) return false; if($args[1] == false) return false;
// if page can't be edited, don't grant delete permissions
if(!$this->canEdit()) return false;
$children = $this->AllChildren();
if($children) foreach($children as $child) {
if(!$child->canDelete()) return false;
}
return $this->stat('can_create') != false; return $this->stat('can_create') != false;
} }
/** /**
* This function should return true if the current user can create new * This function should return true if the current user can create new
* pages of this class. * pages of this class. It can be overloaded to customise the security model for an
*
* It can be overloaded to customise the security model for an
* application. * application.
* *
* Denies permission if any of the following conditions is TRUE:
* - alternateCanCreate() returns FALSE on any decorator
* - $can_create is set to FALSE and the site is not in "dev mode"
*
* Use {@link canAddChildren()} to control behaviour of creating children under this page.
*
* @uses alternateCanCreate()
* @uses $can_create
*
* @param Member $member * @param Member $member
* @return boolean True if the current user can create pages on this * @return boolean True if the current user can create pages on this class.
* class.
*/ */
public function canCreate($member = null) { public function canCreate($member = null) {
if(!isset($member)) { if(!isset($member)) $member = Member::currentUser();
$member = Member::currentUser();
} if($member && $member->isAdmin()) return true;
if($member && $member->isAdmin()) {
return true;
}
$args = array($member, true); $args = array($member, true);
$this->extend('alternateCanCreate', $args); $this->extend('alternateCanCreate', $args);
@ -652,52 +697,81 @@ class SiteTree extends DataObject {
/** /**
* This function should return true if the current user can edit this * This function should return true if the current user can edit this
* page. * page. It can be overloaded to customise the security model for an
*
* It can be overloaded to customise the security model for an
* application. * application.
* *
* Denies permission if any of the following conditions is TRUE:
* - alternateCanEdit() on any decorator returns FALSE
* - canView() return false
* - "CanEditType" directive is set to "Inherit" and any parent page return false for canEdit()
* - "CanEditType" directive is set to "LoggedInUsers" and no user is logged in or doesn't have the CMS_Access_CMSMAIN permission code
* - "CanEditType" directive is set to "OnlyTheseUsers" and user is not in the given groups
*
* @uses alternateCanEdit()
* @uses canView()
* @uses EditorGroups()
*
* @param Member $member * @param Member $member
* @return boolean True if the current user can edit this page. * @return boolean True if the current user can edit this page.
*/ */
public function canEdit($member = null) { public function canEdit($member = null) {
if(!isset($member)) { if(!isset($member)) $member = Member::currentUser();
$member = Member::currentUser();
}
if($member && $member->isAdmin()) {
return true;
}
// admin override
if($member && $member->isAdmin()) return true;
// decorated access checks
$args = array($member, true); $args = array($member, true);
$this->extend('alternateCanEdit', $args); $this->extend('alternateCanEdit', $args);
if($args[1] == false) return false; if($args[1] == false) return false;
if((Permission::check('CMS_ACCESS_CMSMain') && // if page can't be viewed, don't grant edit permissions
(($this->Editors == 'LoggedInUsers' && $member) || if(!$this->canView()) return false;
($this->Editors == 'OnlyTheseUsers' && $member &&
$member->inGroup($this->EditorsGroup)))) == false)
return false;
return true; // check for empty spec
if(
!$this->CanEditType || $this->CanEditType == 'Anyone'
) return true;
// check for inherit
if(
$this->CanEditType == 'Inherit' && $this->Parent()
) return $this->Parent()->canEdit($member);
// check for any logged-in users
if(
$this->CanEditType == 'LoggedInUsers'
&& Permission::checkMember($member, 'CMS_ACCESS_CMSMain')
) return true;
// check for specific groups
if(
$this->CanEditType == 'OnlyTheseUsers'
&& $member
&& $member->inGroups($this->EditorGroups())
) return true;
return false;
} }
/** /**
* This function should return true if the current user can publish this * This function should return true if the current user can publish this
* page. * page. It can be overloaded to customise the security model for an
*
* It can be overloaded to customise the security model for an
* application. * application.
* *
* Denies permission if any of the following conditions is TRUE:
* - alternateCanPublish() on any decorator returns FALSE
* - canEdit() returns FALSE
*
* @uses alternateCanPublish()
*
* @param Member $member * @param Member $member
* @return boolean True if the current user can publish this page. * @return boolean True if the current user can publish this page.
*/ */
public function canPublish($member = null) { public function canPublish($member = null) {
if(!isset($member)) { if(!isset($member)) $member = Member::currentUser();
$member = Member::currentUser();
} if($member && $member->isAdmin()) return true;
if($member && $member->isAdmin()) {
return true;
}
$args = array($member, true); $args = array($member, true);
$this->extend('alternateCanPublish', $args); $this->extend('alternateCanPublish', $args);
@ -1146,32 +1220,36 @@ class SiteTree extends DataObject {
) )
), ),
$tabAccess = new Tab('Access', $tabAccess = new Tab('Access',
new HeaderField('WhoCanViewHeader',_t('SiteTree.ACCESSHEADER', "Who can view this page on my site?"), 2), new HeaderField('WhoCanViewHeader',_t('SiteTree.ACCESSHEADER', "Who can view this page?"), 2),
new OptionsetField( $viewersOptionsField = new OptionsetField(
"Viewers", "CanViewType",
"", ""
array(
"Anyone" => _t('SiteTree.ACCESSANYONE', "Anyone"),
"LoggedInUsers" => _t('SiteTree.ACCESSLOGGEDIN', "Logged-in users"),
"OnlyTheseUsers" => _t('SiteTree.ACCESSONLYTHESE', "Only these people (choose from list)")
)
), ),
new DropdownField("ViewersGroup", $this->fieldLabel('ViewersGroup'), Group::map()), new TreeMultiselectField("ViewerGroups", $this->fieldLabel('ViewerGroups')),
new HeaderField('WhoCanEditHeader',_t('SiteTree.EDITHEADER', "Who can edit this inside the CMS?"), 2), new HeaderField('WhoCanEditHeader',_t('SiteTree.EDITHEADER', "Who can edit this page?"), 2),
new OptionsetField( $editorsOptionsField = new OptionsetField(
"Editors", "CanEditType",
"", ""
array(
"LoggedInUsers" => _t('SiteTree.EDITANYONE', "Anyone who can log-in to the CMS"),
"OnlyTheseUsers" => _t('SiteTree.EDITONLYTHESE', "Only these people (choose from list)")
)
), ),
new DropdownField("EditorsGroup", $this->fieldLabel('EditorsGroup'), Group::map()) new TreeMultiselectField("EditorGroups", $this->fieldLabel('EditorGroups'))
) )
) )
//new NamedLabelField("Status", $message, "pageStatusMessage", true) //new NamedLabelField("Status", $message, "pageStatusMessage", true)
); );
$viewersOptionsSource = array();
if($this->Parent()->ID) $viewersOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
$viewersOptionsSource["Anyone"] = _t('SiteTree.ACCESSANYONE', "Anyone");
$viewersOptionsSource["LoggedInUsers"] = _t('SiteTree.ACCESSLOGGEDIN', "Logged-in users");
$viewersOptionsSource["OnlyTheseUsers"] = _t('SiteTree.ACCESSONLYTHESE', "Only these people (choose from list)");
$viewersOptionsField->setSource($viewersOptionsSource);
$editorsOptionsSource = array();
if($this->Parent()->ID) $editorsOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
$editorsOptionsSource["LoggedInUsers"] = _t('SiteTree.EDITANYONE', "Anyone who can log-in to the CMS");
$editorsOptionsSource["OnlyTheseUsers"] = _t('SiteTree.EDITONLYTHESE', "Only these people (choose from list)");
$editorsOptionsField->setSource($editorsOptionsSource);
$tabContent->setTitle(_t('SiteTree.TABCONTENT', "Content")); $tabContent->setTitle(_t('SiteTree.TABCONTENT', "Content"));
$tabMain->setTitle(_t('SiteTree.TABMAIN', "Main")); $tabMain->setTitle(_t('SiteTree.TABMAIN', "Main"));
$tabMeta->setTitle(_t('SiteTree.TABMETA', "Meta-data")); $tabMeta->setTitle(_t('SiteTree.TABMETA', "Meta-data"));
@ -1205,8 +1283,8 @@ class SiteTree extends DataObject {
$labels['URLSegment'] = _t('SiteTree.URLSegment', 'URL Segment', PR_MEDIUM, 'URL for this page'); $labels['URLSegment'] = _t('SiteTree.URLSegment', 'URL Segment', PR_MEDIUM, 'URL for this page');
$labels['Content'] = _t('SiteTree.Content', 'Content', PR_MEDIUM, 'Main HTML Content for a page'); $labels['Content'] = _t('SiteTree.Content', 'Content', PR_MEDIUM, 'Main HTML Content for a page');
$labels['HomepageForDomain'] = _t('SiteTree.HomepageForDomain', 'Hompage for this domain'); $labels['HomepageForDomain'] = _t('SiteTree.HomepageForDomain', 'Hompage for this domain');
$labels['Viewers'] = _t('SiteTree.Viewers', 'Viewers Group'); $labels['CanViewType'] = _t('SiteTree.Viewers', 'Viewers Groups');
$labels['Editors'] = _t('SiteTree.Editors', 'Editors Group'); $labels['CanEditType'] = _t('SiteTree.Editors', 'Editors Groups');
$labels['ToDo'] = _t('SiteTree.ToDo', 'Todo Notes'); $labels['ToDo'] = _t('SiteTree.ToDo', 'Todo Notes');
$labels['Parent'] = _t('SiteTree.has_one_Parent', 'Parent Page', PR_MEDIUM, 'The parent page in the site hierarchy'); $labels['Parent'] = _t('SiteTree.has_one_Parent', 'Parent Page', PR_MEDIUM, 'The parent page in the site hierarchy');
$labels['Comments'] = _t('SiteTree.Comments', 'Comments'); $labels['Comments'] = _t('SiteTree.Comments', 'Comments');

View File

@ -0,0 +1,159 @@
<?php
/**
* @package sapphire
* @subpackage tests
*
* @todo Test canAddChildren()
* @todo Test canCreate()
*/
class SiteTreePermissionsTest extends SapphireTest {
static $fixture_file = "sapphire/tests/SiteTreePermissionsTest.yml";
function testRestrictedViewLoggedInUsers() {
$page = $this->objFromFixture('Page', 'restrictedViewLoggedInUsers');
$randomUnauthedMember = new Member();
$randomUnauthedMember->ID = 99;
$this->assertFalse(
$page->canView($randomUnauthedMember),
'Unauthenticated members cant view a page marked as "Viewable for any logged in users"'
);
$websiteuser = $this->objFromFixture('Member', 'websiteuser');
$websiteuser->logIn();
$this->assertTrue(
$page->canView($websiteuser),
'Authenticated members can view a page marked as "Viewable for any logged in users" even if they dont have access to the CMS'
);
$websiteuser->logOut();
}
function testRestrictedViewOnlyTheseUsers() {
$page = $this->objFromFixture('Page', 'restrictedViewOnlyWebsiteUsers');
$randomUnauthedMember = new Member();
$randomUnauthedMember->ID = 99;
$this->assertFalse(
$page->canView($randomUnauthedMember),
'Unauthenticated members cant view a page marked as "Viewable by these groups"'
);
$subadminuser = $this->objFromFixture('Member', 'subadmin');
$this->assertFalse(
$page->canView($subadminuser),
'Authenticated members cant view a page marked as "Viewable by these groups" if theyre not in the listed groups'
);
$websiteuser = $this->objFromFixture('Member', 'websiteuser');
$this->assertTrue(
$page->canView($websiteuser),
'Authenticated members can view a page marked as "Viewable by these groups" if theyre in the listed groups'
);
}
function testRestrictedEditLoggedInUsers() {
$page = $this->objFromFixture('Page', 'restrictedEditLoggedInUsers');
$randomUnauthedMember = new Member();
$randomUnauthedMember->ID = 99;
$this->assertFalse(
$page->canEdit($randomUnauthedMember),
'Unauthenticated members cant edit a page marked as "Editable by logged in users"'
);
$websiteuser = $this->objFromFixture('Member', 'websiteuser');
$websiteuser->logIn();
$this->assertFalse(
$page->canEdit($websiteuser),
'Authenticated members cant edit a page marked as "Editable by logged in users" if they dont have cms permissions'
);
$subadminuser = $this->objFromFixture('Member', 'subadmin');
$this->assertTrue(
$page->canEdit($subadminuser),
'Authenticated members can edit a page marked as "Editable by logged in users" if they have cms permissions and belong to any of these groups'
);
$websiteuser->logOut();
}
function testRestrictedEditOnlySubadminGroup() {
$page = $this->objFromFixture('Page', 'restrictedEditOnlySubadminGroup');
$randomUnauthedMember = new Member();
$randomUnauthedMember->ID = 99;
$this->assertFalse(
$page->canEdit($randomUnauthedMember),
'Unauthenticated members cant edit a page marked as "Editable by these groups"'
);
$subadminuser = $this->objFromFixture('Member', 'subadmin');
$this->assertTrue(
$page->canEdit($subadminuser),
'Authenticated members can view a page marked as "Editable by these groups" if theyre in the listed groups'
);
$websiteuser = $this->objFromFixture('Member', 'websiteuser');
$websiteuser->logIn();
$this->assertFalse(
$page->canEdit($websiteuser),
'Authenticated members cant edit a page marked as "Editable by these groups" if theyre not in the listed groups'
);
$websiteuser->logOut();
}
function testRestrictedViewInheritance() {
$parentPage = $this->objFromFixture('Page', 'parent_restrictedViewOnlySubadminGroup');
$childPage = $this->objFromFixture('Page', 'child_restrictedViewOnlySubadminGroup');
$randomUnauthedMember = new Member();
$randomUnauthedMember->ID = 99;
$this->assertFalse(
$childPage->canView($randomUnauthedMember),
'Unauthenticated members cant view a page marked as "Viewable by these groups" by inherited permission'
);
$subadminuser = $this->objFromFixture('Member', 'subadmin');
$this->assertTrue(
$childPage->canView($subadminuser),
'Authenticated members can view a page marked as "Viewable by these groups" if theyre in the listed groups by inherited permission'
);
}
function testRestrictedEditInheritance() {
$parentPage = $this->objFromFixture('Page', 'parent_restrictedEditOnlySubadminGroup');
$childPage = $this->objFromFixture('Page', 'child_restrictedEditOnlySubadminGroup');
$randomUnauthedMember = new Member();
$randomUnauthedMember->ID = 99;
$this->assertFalse(
$childPage->canEdit($randomUnauthedMember),
'Unauthenticated members cant edit a page marked as "Editable by these groups" by inherited permission'
);
$subadminuser = $this->objFromFixture('Member', 'subadmin');
$this->assertTrue(
$childPage->canEdit($subadminuser),
'Authenticated members can edit a page marked as "Editable by these groups" if theyre in the listed groups by inherited permission'
);
}
function testDeleteRestrictedChild() {
$parentPage = $this->objFromFixture('Page', 'deleteTestParentPage');
$childPage = $this->objFromFixture('Page', 'deleteTestChildPage');
$randomUnauthedMember = new Member();
$randomUnauthedMember->ID = 99;
$this->assertFalse(
$parentPage->canDelete($randomUnauthedMember),
'Unauthenticated members cant delete a page if it doesnt have delete permissions on any of its descendants'
);
$this->assertFalse(
$childPage->canDelete($randomUnauthedMember),
'Unauthenticated members cant delete a child page marked as "Editable by these groups"'
);
}
}
?>

View File

@ -0,0 +1,60 @@
Permission:
cmsmain1:
Code: CMS_ACCESS_CMSMain
cmsmain2:
Code: CMS_ACCESS_CMSMain
Group:
subadmingroup:
Title: Create, edit and delete pages
Code: subadmingroup
Permissions: =>Permission.cmsmain1
editorgroup:
Title: Edit existing pages
Code: editorgroup
Permissions: =>Permission.cmsmain2
websiteusers:
Title: View certain restricted pages
Member:
subadmin:
Email: subadmin@test.com
Password: test
Groups: =>Group.subadmingroup
editor:
Email: editor@test.com
Password: test
Groups: =>Group.editorgroup
websiteuser:
Email: websiteuser@test.com
Password: test
Groups: =>Group.websiteusers
Page:
restrictedViewLoggedInUsers:
CanViewType: LoggedInUsers
restrictedViewOnlyWebsiteUsers:
CanViewType: OnlyTheseUsers
ViewerGroups: =>Group.websiteusers
restrictedViewOnlySubadminGroup:
CanViewType: OnlyTheseUsers
ViewerGroups: =>Group.subadmingroup
restrictedEditLoggedInUsers:
CanEditType: LoggedInUsers
restrictedEditOnlySubadminGroup:
CanEditType: OnlyTheseUsers
EditorGroups: =>Group.subadmingroup
parent_restrictedViewOnlySubadminGroup:
CanViewType: OnlyTheseUsers
ViewerGroups: =>Group.subadmingroup
child_restrictedViewOnlySubadminGroup:
CanViewType: Inherit
Parent: =>Page.parent_restrictedViewOnlySubadminGroup
parent_restrictedEditOnlySubadminGroup:
CanEditType: OnlyTheseUsers
EditorGroups: =>Group.subadmingroup
child_restrictedEditOnlySubadminGroup:
CanEditType: Inherit
Parent: =>Page.parent_restrictedEditOnlySubadminGroup
deleteTestParentPage:
CanEditType: Inherit
deleteTestChildPage:
CanEditType: OnlyTheseUsers
EditorGroups: =>Group.subadmingroup