ENHANCEMENT Using LeftAndMain->canView() in LeftAndMain->init() - if current admin interface can't be viewed, iterate over remaining interfaces until we find a valid one. This only includes admin interfaces with a valid controller, so it should fix the obnoxious redirect to userhelp.silverstripe.com when a website-user tries to access the CMS.

ENHANCEMENT Added LeftAndMain->canView() to check for logged-in member and CMS_ACCESS_* permissions in a testable way
ENHANCEMENT Don't show "Reports" admin section if no subclasses of SSReport are found (or none of the existing subclasses returns a valid canView())
ENHANCEMENT Added CMSMenu::get_viewable_menu_items() and using it in LeftAndMain->MainMenu()

git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/cms/branches/2.3@68460 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Ingo Schommer 2008-12-11 22:25:42 +00:00 committed by Sam Minnee
parent e817a013d8
commit d9e9e5f348
6 changed files with 195 additions and 48 deletions

View File

@ -1,6 +1,7 @@
<?php <?php
/** /**
* The object manages the main CMS menu * The object manages the main CMS menu.
* See {@link LeftAndMain::init()} for example usage.
* *
* @package cms * @package cms
* @subpackage content * @subpackage content
@ -123,6 +124,31 @@ class CMSMenu extends Object implements Iterator, i18nEntityProvider
return self::$menu_items; return self::$menu_items;
} }
/**
* Get all menu items that the passed member can view.
* Defaults to {@link Member::currentUser()}.
*
* @param Member $member
* @return array
*/
public static function get_viewable_menu_items($member = null) {
if(!$member && $member !== FALSE) {
$member = Member::currentUser();
}
$viewableMenuItems = array();
$allMenuItems = self::get_menu_items();
if($allMenuItems) foreach($allMenuItems as $code => $menuItem) {
// exclude all items which have a controller to perform permission
// checks on
if($menuItem->controller && !singleton($menuItem->controller)->canView($member)) continue;
$viewableMenuItems[$code] = $menuItem;
}
return $viewableMenuItems;
}
/** /**
* Removes an existing item from the menu. * Removes an existing item from the menu.
* *

View File

@ -64,11 +64,41 @@ class LeftAndMain extends Controller {
'themedcss' => array(), 'themedcss' => array(),
); );
/**
* @param Member $member
* @return boolean
*/
function canView($member = null) {
if(!$member && $member !== FALSE) {
$member = Member::currentUser();
}
// cms menus only for logged-in members
if(!$member) return false;
// alternative decorated checks
if($this->hasMethod('alternateAccessCheck')) {
$alternateAllowed = $this->alternateAccessCheck();
if($alternateAllowed === FALSE) return false;
}
// Default security check for LeftAndMain sub-class permissions
if(!Permission::checkMember($member, "CMS_ACCESS_$this->class")) {
return false;
}
return true;
}
/** /**
* @uses LeftAndMainDecorator->init() * @uses LeftAndMainDecorator->init()
* @uses LeftAndMainDecorator->accessedCMS() * @uses LeftAndMainDecorator->accessedCMS()
* @uses CMSMenu
* @uses Director::set_site_mode()
*/ */
function init() { function init() {
parent::init();
Director::set_site_mode('cms'); Director::set_site_mode('cms');
// set language // set language
@ -89,39 +119,38 @@ class LeftAndMain extends Controller {
Translatable::choose_site_lang(array_keys(i18n::get_existing_content_languages('SiteTree'))); Translatable::choose_site_lang(array_keys(i18n::get_existing_content_languages('SiteTree')));
} }
parent::init();
// Allow customisation of the access check by a decorator // Allow customisation of the access check by a decorator
if($this->hasMethod('alternateAccessCheck')) { if(!$this->canView()) {
$isAllowed = $this->alternateAccessCheck(); // When access /admin/, we should try a redirect to another part of the admin rather than be locked out
$menu = $this->MainMenu();
// Default security check for LeftAndMain sub-class permissions foreach($menu as $candidate) {
} else { if(
$isAllowed = Permission::check("CMS_ACCESS_$this->class"); $candidate->Link &&
if(!$isAllowed && $this->class == 'CMSMain') { $candidate->Link != $this->Link()
// When access /admin/, we should try a redirect to another part of the admin rather than be locked out && $candidate->MenuItem->controller
$menu = $this->MainMenu(); && singleton($candidate->MenuItem->controller)->canView()
if(($first = $menu->First()) && $first->Link) { ) {
Director::redirect($first->Link); return Director::redirect($candidate->Link);
} }
} }
}
// Don't continue if there's already been a redirection request. if(Member::currentUser()) {
if(Director::redirected_to()) return; Session::set("BackURL", null);
}
// Access failure! // if no alternate menu items have matched, return a permission error
if(!$isAllowed) {
$messageSet = array( $messageSet = array(
'default' => _t('LeftAndMain.PERMDEFAULT',"Please choose an authentication method and enter your credentials to access the CMS."), 'default' => _t('LeftAndMain.PERMDEFAULT',"Please choose an authentication method and enter your credentials to access the CMS."),
'alreadyLoggedIn' => _t('LeftAndMain.PERMALREADY',"I'm sorry, but you can't access that part of the CMS. If you want to log in as someone else, do so below"), 'alreadyLoggedIn' => _t('LeftAndMain.PERMALREADY',"I'm sorry, but you can't access that part of the CMS. If you want to log in as someone else, do so below"),
'logInAgain' => _t('LeftAndMain.PERMAGAIN',"You have been logged out of the CMS. If you would like to log in again, enter a username and password below."), 'logInAgain' => _t('LeftAndMain.PERMAGAIN',"You have been logged out of the CMS. If you would like to log in again, enter a username and password below."),
); );
Security::permissionFailure($this, $messageSet); return Security::permissionFailure($this, $messageSet);
return;
} }
// Don't continue if there's already been a redirection request.
if(Director::redirected_to()) return;
// Audit logging hook // Audit logging hook
if(empty($_REQUEST['executeForm']) && !Director::is_ajax()) $this->extend('accessedCMS'); if(empty($_REQUEST['executeForm']) && !Director::is_ajax()) $this->extend('accessedCMS');
@ -344,17 +373,17 @@ class LeftAndMain extends Controller {
// Encode into DO set // Encode into DO set
$menu = new DataObjectSet(); $menu = new DataObjectSet();
foreach(singleton('CMSMenu') as $code => $menuItem) { $menuItems = CMSMenu::get_viewable_menu_items();
if(isset($menuItem->controller) && $this->hasMethod('alternateMenuDisplayCheck')) { if($menuItems) foreach($menuItems as $code => $menuItem) {
$isAllowed = $this->alternateMenuDisplayCheck($menuItem->controller); // alternate permission checks (in addition to LeftAndMain->canView())
} elseif(isset($menuItem->controller)) { if(
$isAllowed = Permission::check("CMS_ACCESS_" . $menuItem->controller); isset($menuItem->controller)
} else { && $this->hasMethod('alternateMenuDisplayCheck')
$isAllowed = true; && !$this->alternateMenuDisplayCheck($menuItem->controller)
) {
continue;
} }
if(!$isAllowed) continue;
$linkingmode = ""; $linkingmode = "";
if(strpos($this->Link(), $menuItem->url) !== false) { if(strpos($this->Link(), $menuItem->url) !== false) {
@ -381,6 +410,7 @@ class LeftAndMain extends Controller {
} }
$menu->push(new ArrayData(array( $menu->push(new ArrayData(array(
"MenuItem" => $menuItem,
"Title" => Convert::raw2xml($title), "Title" => Convert::raw2xml($title),
"Code" => $code, "Code" => $code,
"Link" => $menuItem->url, "Link" => $menuItem->url,

View File

@ -41,6 +41,35 @@ class ReportAdmin extends LeftAndMain {
} }
} }
/**
* Does the parent permission checks, but also
* makes sure that instantiatable subclasses of
* {@link Report} exist. By default, the CMS doesn't
* include any Reports, so there's no point in showing
*
* @param Member $member
* @return boolean
*/
function canView($member = null) {
if(!$member && $member !== FALSE) {
$member = Member::currentUser();
}
if(!parent::canView($member)) return false;
$hasViewableSubclasses = false;
$subClasses = array_values(ClassInfo::subclassesFor('SSReport'));
foreach($subClasses as $subclass) {
// Remove abstract classes and LeftAndMain
$classReflection = new ReflectionClass($subclass);
if($classReflection->isInstantiable() && $subclass != 'SSReport') {
if(singleton($subclass)->canView()) $hasViewableSubclasses = true;
}
}
return $hasViewableSubclasses;
}
/** /**
* Return a DataObjectSet of SSReport subclasses * Return a DataObjectSet of SSReport subclasses
* that are available for use. * that are available for use.

View File

@ -69,6 +69,18 @@ class SSReport extends ViewableData {
return $fields; return $fields;
} }
/**
* @param Member $member
* @return boolean
*/
function canView($member = null) {
if(!$member && $member !== FALSE) {
$member = Member::currentUser();
}
return true;
}
/** /**
* Return a field, such as a {@link ComplexTableField} that is * Return a field, such as a {@link ComplexTableField} that is
* used to show and manipulate data relating to this report. * used to show and manipulate data relating to this report.

View File

@ -16,14 +16,20 @@ Group:
Title: Administrators Title: Administrators
empty: empty:
Title: Empty Group Title: Empty Group
assetsonly:
Title: assetsonly
Member: Member:
admin: admin:
Email: admin@example.com Email: admin@example.com
Password: ZXXlkwecxz2390232233 Password: ZXXlkwecxz2390232233
Groups: =>Group.admin Groups: =>Group.admin
assetsonlyuser:
Email: assetsonlyuser@test.com
Groups: =>Group.assetsonly
Permission: Permission:
admin: admin:
Code: ADMIN Code: ADMIN
GroupID: =>Group.admin GroupID: =>Group.admin
assetsonly:
Code: CMS_ACCESS_AssetAdmin
GroupID: =>Group.assetsonly

View File

@ -3,30 +3,32 @@
* @package cms * @package cms
* @subpackage tests * @subpackage tests
*/ */
class LeftAndMainTest extends SapphireTest { class LeftAndMainTest extends FunctionalTest {
static $fixture_file = 'cms/tests/CMSMainTest.yml'; static $fixture_file = 'cms/tests/CMSMainTest.yml';
function setUp() {
parent::setUp();
// @todo fix controller stack problems and re-activate
//$this->autoFollowRedirection = false;
}
/** /**
* Check that all subclasses of leftandmain can be accessed * Check that all subclasses of leftandmain can be accessed
*/ */
public function testLeftAndMainSubclasses() { public function testLeftAndMainSubclasses() {
$session = new Session(array( $adminuser = $this->objFromFixture('Member','admin');
'loggedInAs' => $this->idFromFixture('Member','admin') $this->session()->inst_set('loggedInAs', $adminuser->ID);
));
// This controller stuff is needed because LeftAndMain::MainMenu() inspects the current user's permissions
$controller = new Controller();
$controller->setSession($session);
$controller->pushCurrent();
$menuItems = singleton('CMSMain')->MainMenu(); $menuItems = singleton('CMSMain')->MainMenu();
$controller->popCurrent();
$classes = ClassInfo::subclassesFor("LeftAndMain");
foreach($menuItems as $menuItem) { foreach($menuItems as $menuItem) {
$link = $menuItem->Link; $link = $menuItem->Link;
// don't test external links
if(preg_match('/^https?:\/\//',$link)) continue; if(preg_match('/^https?:\/\//',$link)) continue;
$response = Director::test($link, null, $session); $response = $this->get($link);
$this->assertType('HTTPResponse', $response, "$link should return a response object"); $this->assertType('HTTPResponse', $response, "$link should return a response object");
$this->assertEquals(200, $response->getStatusCode(), "$link should return 200 status code"); $this->assertEquals(200, $response->getStatusCode(), "$link should return 200 status code");
// Check that a HTML page has been returned // Check that a HTML page has been returned
@ -34,6 +36,48 @@ class LeftAndMainTest extends SapphireTest {
$this->assertRegExp('/<head[^>]*>/i', $response->getBody(), "$link should contain <head> tag"); $this->assertRegExp('/<head[^>]*>/i', $response->getBody(), "$link should contain <head> tag");
$this->assertRegExp('/<body[^>]*>/i', $response->getBody(), "$link should contain <body> tag"); $this->assertRegExp('/<body[^>]*>/i', $response->getBody(), "$link should contain <body> tag");
} }
$this->session()->inst_set('loggedInAs', null);
}
function testCanView() {
$adminuser = $this->objFromFixture('Member', 'admin');
$assetsonlyuser = $this->objFromFixture('Member', 'assetsonlyuser');
// anonymous user
$this->session()->inst_set('loggedInAs', null);
$menuItems = singleton('LeftAndMain')->MainMenu();
$this->assertEquals(
$menuItems->column('Code'),
array(),
'Without valid login, members cant access any menu entries'
);
// restricted cms user
$this->session()->inst_set('loggedInAs', $assetsonlyuser->ID);
$menuItems = singleton('LeftAndMain')->MainMenu();
$this->assertEquals(
$menuItems->column('Code'),
array('AssetAdmin','Help'),
'Groups with limited access can only access the interfaces they have permissions for'
);
// admin
$this->session()->inst_set('loggedInAs', $adminuser->ID);
$menuItems = singleton('LeftAndMain')->MainMenu();
$this->assertContains(
'CMSMain',
$menuItems->column('Code'),
'Administrators can access CMS'
);
$this->assertContains(
'AssetAdmin',
$menuItems->column('Code'),
'Administrators can access Assets'
);
$this->session()->inst_set('loggedInAs', null);
} }
} }