Feature disable current user from removing their admin permission

This commit is contained in:
Christopher Joe 2017-10-27 10:49:38 +13:00
parent 921bf7df1e
commit f6b7cf8889
11 changed files with 265 additions and 9 deletions

View File

@ -1,3 +1,10 @@
# Run framework behat tests with this command (installed with silverstripe/installer)
# Note that framework behat tests require CMS module
# ========================================================================= #
# vendor/bin/selenium-server-standalone -Dwebdriver.firefox.bin="/Applications/Firefox31.app/Contents/MacOS/firefox-bin"
# vendor/bin/serve --bootstrap-file vendor/silverstripe/framework/tests/behat/serve-bootstrap.php
# vendor/bin/behat @framework
# ========================================================================= #
default: default:
suites: suites:
framework: framework:

View File

@ -85,6 +85,8 @@ en:
Print: Print Print: Print
RelationSearch: 'Relation search' RelationSearch: 'Relation search'
ResetFilter: Reset ResetFilter: Reset
SilverStripe\Forms\GridField\GridFieldGroupDeleteAction:
UnlinkSelfFailure: 'Cannot remove yourself from this group, you will lose admin rights'
SilverStripe\Forms\GridField\GridFieldDeleteAction: SilverStripe\Forms\GridField\GridFieldDeleteAction:
DELETE_DESCRIPTION: Delete DELETE_DESCRIPTION: Delete
Delete: Delete Delete: Delete
@ -247,6 +249,7 @@ en:
other: '{count} Members' other: '{count} Members'
REMEMBERME: 'Remember me next time? (for {count} days on this device)' REMEMBERME: 'Remember me next time? (for {count} days on this device)'
SINGULARNAME: Member SINGULARNAME: Member
VALIDATIONADMINLOSTACCESS: 'Cannot remove all admin groups from your profile'
SUBJECTPASSWORDCHANGED: 'Your password has been changed' SUBJECTPASSWORDCHANGED: 'Your password has been changed'
SUBJECTPASSWORDRESET: 'Your password reset link' SUBJECTPASSWORDRESET: 'Your password reset link'
SURNAME: Surname SURNAME: Surname

View File

@ -3,6 +3,7 @@
namespace SilverStripe\Dev; namespace SilverStripe\Dev;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\Core\Environment;
use SilverStripe\Core\Manifest\ClassLoader; use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\Core\Manifest\Module; use SilverStripe\Core\Manifest\Module;
use SilverStripe\Core\Manifest\ModuleLoader; use SilverStripe\Core\Manifest\ModuleLoader;
@ -148,7 +149,7 @@ class Deprecation
if (isset(self::$enabled)) { if (isset(self::$enabled)) {
return self::$enabled; return self::$enabled;
} }
return getenv('SS_DEPRECATION_ENABLED') ?: true; return Environment::getEnv('SS_DEPRECATION_ENABLED') ?: true;
} }
/** /**

View File

@ -119,6 +119,7 @@ class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_Actio
if (!$record->canEdit()) { if (!$record->canEdit()) {
return null; return null;
} }
$title = _t('SilverStripe\\Forms\\GridField\\GridFieldDeleteAction.UnlinkRelation', "Unlink");
$field = GridField_FormAction::create( $field = GridField_FormAction::create(
$gridField, $gridField,
@ -128,7 +129,8 @@ class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_Actio
array('RecordID' => $record->ID) array('RecordID' => $record->ID)
) )
->addExtraClass('btn btn--no-text btn--icon-md font-icon-link-broken grid-field__icon-action gridfield-button-unlink') ->addExtraClass('btn btn--no-text btn--icon-md font-icon-link-broken grid-field__icon-action gridfield-button-unlink')
->setAttribute('title', _t('SilverStripe\\Forms\\GridField\\GridFieldDeleteAction.UnlinkRelation', "Unlink")); ->setAttribute('title', $title)
->setAttribute('aria-label', $title);
} else { } else {
if (!$record->canDelete()) { if (!$record->canDelete()) {
return null; return null;

View File

@ -0,0 +1,91 @@
<?php
namespace SilverStripe\Forms\GridField;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ValidationException;
use SilverStripe\Security\Member;
use SilverStripe\Security\Permission;
use SilverStripe\Security\Security;
/**
* Adds a delete action for the gridfield to remove a relationship from group.
* This is a special case where it captures whether the current user is the record being removed and
* prevents removal from happening.
*
* Class GroupGridFieldDeleteAction
* @package SilverStripe\Security
*/
class GridFieldGroupDeleteAction extends GridFieldDeleteAction
{
/**
* @var integer
*/
protected $groupID;
public function __construct($groupID)
{
$this->groupID = $groupID;
parent::__construct(true);
}
/**
*
* @param GridField $gridField
* @param DataObject $record
* @param string $columnName
* @return string the HTML for the column
*/
public function getColumnContent($gridField, $record, $columnName)
{
if ($this->canUnlink($record)) {
return parent::getColumnContent($gridField, $record, $columnName);
}
return null;
}
/**
* Handle the actions and apply any changes to the GridField
*
* @param GridField $gridField
* @param string $actionName
* @param mixed $arguments
* @param array $data - form data
* @throws ValidationException
*/
public function handleAction(GridField $gridField, $actionName, $arguments, $data)
{
$record = $gridField->getList()->find('ID', $arguments['RecordID']);
if (!$record || !$actionName == 'unlinkrelation' || $this->canUnlink($record)) {
parent::handleAction($gridField, $actionName, $arguments, $data);
return;
}
throw new ValidationException(
_t(__CLASS__ . '.UnlinkSelfFailure', 'Cannot remove yourself from this group, you will lose admin rights')
);
}
/**
* @param $record - the record of the User to unlink with
* @return bool
*/
protected function canUnlink($record)
{
$currentUser = Security::getCurrentUser();
if (($record instanceof Member && $record->ID === $currentUser->ID)
&& Permission::checkMember($record, 'ADMIN')
) {
$adminGroups = array_intersect(
$record->Groups()->column(),
Permission::get_groups_by_permission('ADMIN')->column()
);
if (count($adminGroups) === 1 && array_search($this->groupID, $adminGroups) !== false) {
return false;
}
}
return true;
}
}

View File

@ -11,8 +11,11 @@ use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridFieldAddExistingAutocompleter; use SilverStripe\Forms\GridField\GridFieldAddExistingAutocompleter;
use SilverStripe\Forms\GridField\GridFieldButtonRow; use SilverStripe\Forms\GridField\GridFieldButtonRow;
use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor; use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor;
use SilverStripe\Forms\GridField\GridFieldDeleteAction;
use SilverStripe\Forms\GridField\GridFieldDetailForm; use SilverStripe\Forms\GridField\GridFieldDetailForm;
use SilverStripe\Forms\GridField\GridFieldExportButton; use SilverStripe\Forms\GridField\GridFieldExportButton;
use SilverStripe\Forms\GridField\GridFieldGroupDeleteAction;
use SilverStripe\Forms\GridField\GridFieldPageCount;
use SilverStripe\Forms\GridField\GridFieldPrintButton; use SilverStripe\Forms\GridField\GridFieldPrintButton;
use SilverStripe\Forms\HiddenField; use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig; use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig;
@ -150,6 +153,9 @@ class Group extends DataObject
$config->addComponent(new GridFieldButtonRow('after')); $config->addComponent(new GridFieldButtonRow('after'));
$config->addComponents(new GridFieldExportButton('buttons-after-left')); $config->addComponents(new GridFieldExportButton('buttons-after-left'));
$config->addComponents(new GridFieldPrintButton('buttons-after-left')); $config->addComponents(new GridFieldPrintButton('buttons-after-left'));
$config->removeComponentsByType(GridFieldDeleteAction::class);
$config->addComponent(new GridFieldGroupDeleteAction($this->ID), GridFieldPageCount::class);
/** @var GridFieldAddExistingAutocompleter $autocompleter */ /** @var GridFieldAddExistingAutocompleter $autocompleter */
$autocompleter = $config->getComponentByType(GridFieldAddExistingAutocompleter::class); $autocompleter = $config->getComponentByType(GridFieldAddExistingAutocompleter::class);
/** @skipUpgrade */ /** @skipUpgrade */

View File

@ -137,6 +137,37 @@ class Member_Validator extends RequiredFields
} }
} }
$currentUser = Security::getCurrentUser();
$id = $data['ID'];
if ($id === $currentUser->ID && Permission::checkMember($currentUser, 'ADMIN')) {
$stillAdmin = true;
if (!isset($data['DirectGroups'])) {
$stillAdmin = false;
} else {
$adminGroups = array_intersect(
$data['DirectGroups'],
Permission::get_groups_by_permission('ADMIN')->column()
);
if (count($adminGroups) === 0) {
$stillAdmin = false;
}
}
if (!$stillAdmin) {
$this->validationError(
'DirectGroups',
_t(
'SilverStripe\\Security\\Member.VALIDATIONADMINLOSTACCESS',
'Cannot remove all admin groups from your profile'
),
'required'
);
}
}
// Execute the validators on the extensions // Execute the validators on the extensions
$results = $this->extend('updatePHP', $data, $this->form); $results = $this->extend('updatePHP', $data, $this->form);

View File

@ -5,12 +5,30 @@ Feature: Manage users
So that I can control access to the CMS So that I can control access to the CMS
Background: Background:
Given a "member" "ADMIN" belonging to "ADMIN Group" with "Email"="admin@test.com" Given a "member" "ADMIN" belonging to "ADMIN group" with "Email"="ADMIN@example.org"
And a "member" "Staff" belonging to "Staff Group" with "Email"="staffmember@test.com" And the "member" "ADMIN" belonging to "ADMIN group2"
And a "member" "Staff" belonging to "Staff group" with "Email"="staffmember@test.com"
And the "group" "ADMIN group" has permissions "Full administrative rights" And the "group" "ADMIN group" has permissions "Full administrative rights"
And the "group" "ADMIN group2" has permissions "Full administrative rights"
And I am logged in with "ADMIN" permissions And I am logged in with "ADMIN" permissions
And I go to "/admin/security" And I go to "/admin/security"
Scenario: I cannot remove my admin access, but can remove myself from an admin group
When I click the "Groups" CMS tab
And I click "ADMIN group" in the "#Root_Groups" element
And I should see the "Unlink" button in the "Members" gridfield for the "ADMIN" row
Then I click "Groups" in the ".breadcrumbs-wrapper" element
And I click the "Groups" CMS tab
And I click "ADMIN group2" in the "#Root_Groups" element
And I should see the "Unlink" button in the "Members" gridfield for the "ADMIN" row
Then I click the "Unlink" button in the "Members" gridfield for the "ADMIN" row
And I should not see the "Unlink" button in the "Members" gridfield for the "ADMIN" row
Then I click "Groups" in the ".breadcrumbs-wrapper" element
And I click the "Groups" CMS tab
And I click "ADMIN group" in the "#Root_Groups" element
And I should not see the "Unlink" button in the "Members" gridfield for the "ADMIN" row
Scenario: I can list all users regardless of group Scenario: I can list all users regardless of group
When I click the "Users" CMS tab When I click the "Users" CMS tab
Then I should see "admin@test.com" in the "#Root_Users" element Then I should see "admin@test.com" in the "#Root_Users" element

View File

@ -5,11 +5,25 @@ Feature: Manage my own settings
In order to streamline my CMS experience In order to streamline my CMS experience
Background: Background:
Given a "member" "Joe" belonging to "Admin Group" with "Email"="joe@test.com" and "Password"="secret" Given a "member" "Joe" belonging to "Admin group" with "Email"="joe@test.com" and "Password"="secret"
And the "group" "Admin Group" has permissions "Full administrative rights" And the "group" "Admin group" has permissions "Full administrative rights"
And the "member" "Joe" belonging to "Admin group2"
And the "group" "Admin group2" has permissions "Full administrative rights"
And I log in with "joe@test.com" and "secret" And I log in with "joe@test.com" and "secret"
And I go to "admin/myprofile" And I go to "admin/myprofile"
Scenario: I cannot remove all my admin groups
When I click the "Admin group" option in the "DirectGroups" listbox
And I click the "Admin group2" option in the "DirectGroups" listbox
And I press the "Save" button
Then I should see "Cannot remove all admin groups from your profile" in the "#Form_EditForm" element
Scenario: I can remove one of my admin groups
When I click the "Admin group" option in the "DirectGroups" listbox
And I press the "Save" button
Then I should see a "Saved" notice
And I should not see "Cannot remove all admin groups from your profile" in the "#Form_EditForm" element
Scenario: I can edit my personal details Scenario: I can edit my personal details
Given I fill in "First Name" with "Jack" Given I fill in "First Name" with "Jack"
And I fill in "Surname" with "Johnson" And I fill in "Surname" with "Johnson"

View File

@ -6,7 +6,7 @@ Feature: Manage Security Permissions for Groups
Background: Background:
Given a "group" "test group" Given a "group" "test group"
And a "member" "ADMIN" belonging to "ADMIN Group" with "Email"="admin@test.com" And a "member" "ADMIN" belonging to "ADMIN group" with "Email"="admin@test.com"
And the "group" "ADMIN group" has permissions "Full administrative rights" And the "group" "ADMIN group" has permissions "Full administrative rights"
And I am logged in with "ADMIN" permissions And I am logged in with "ADMIN" permissions
And I go to "/admin/security" And I go to "/admin/security"

View File

@ -95,7 +95,7 @@ class CmsFormsContext implements Context
{ {
$element = $this->getHtmlField($locator); $element = $this->getHtmlField($locator);
$actual = $element->getValue(); $actual = $element->getValue();
$regex = '/'.preg_quote($html, '/').'/ui'; $regex = '/' . preg_quote($html, '/') . '/ui';
$failed = false; $failed = false;
if (trim($negative)) { if (trim($negative)) {
@ -230,7 +230,7 @@ JS;
*/ */
public function iClickOnTheHtmlFieldButton($button) public function iClickOnTheHtmlFieldButton($button)
{ {
$xpath = "//*[@aria-label='".$button."']"; $xpath = "//*[@aria-label='" . $button . "']";
$session = $this->getSession(); $session = $this->getSession();
$element = $session->getPage()->find('xpath', $xpath); $element = $session->getPage()->find('xpath', $xpath);
if (null === $element) { if (null === $element) {
@ -352,4 +352,87 @@ JS;
// Destroy cookie to detach session // Destroy cookie to detach session
$this->getMainContext()->getSession()->setCookie('PHPSESSID', null); $this->getMainContext()->getSession()->setCookie('PHPSESSID', null);
} }
/**
* @When /^I should see the "([^"]*)" button in the "([^"]*)" gridfield for the "([^"]*)" row$/
* @param string $buttonLabel
* @param string $gridFieldName
* @param string $rowName
*/
public function assertIShouldSeeTheGridFieldButtonForRow($buttonLabel, $gridFieldName, $rowName)
{
$button = $this->getGridFieldButton($gridFieldName, $rowName, $buttonLabel);
assertNotNull($button, sprintf('Button "%s" not found', $buttonLabel));
}
/**
* @When /^I should not see the "([^"]*)" button in the "([^"]*)" gridfield for the "([^"]*)" row$/
* @param string $buttonLabel
* @param string $gridFieldName
* @param string $rowName
*/
public function assertIShouldNotSeeTheGridFieldButtonForRow($buttonLabel, $gridFieldName, $rowName)
{
$button = $this->getGridFieldButton($gridFieldName, $rowName, $buttonLabel);
assertNull($button, sprintf('Button "%s" not found', $buttonLabel));
}
/**
* @When /^I click the "([^"]*)" button in the "([^"]*)" gridfield for the "([^"]*)" row$/
* @param string $buttonLabel
* @param string $gridFieldName
* @param string $rowName
*/
public function stepIClickTheGridFieldButtonForRow($buttonLabel, $gridFieldName, $rowName)
{
$button = $this->getGridFieldButton($gridFieldName, $rowName, $buttonLabel);
assertNotNull($button, sprintf('Button "%s" not found', $buttonLabel));
$button->click();
}
/**
* Finds a button in the gridfield row
*
* @param $gridFieldName
* @param $rowName
* @param $buttonLabel
* @return $button
*/
protected function getGridFieldButton($gridFieldName, $rowName, $buttonLabel)
{
$page = $this->getSession()->getPage();
$gridField = $page->find('xpath', sprintf('//*[@data-name="%s"]', $gridFieldName));
assertNotNull($gridField, sprintf('Gridfield "%s" not found', $gridFieldName));
$name = $gridField->find('xpath', sprintf('//*[count(*)=0 and contains(.,"%s")]', $rowName));
if (!$name) {
return null;
}
$button = $name->getParent()->find('xpath', sprintf('//*[@aria-label="%s"]', $buttonLabel));
return $button;
}
/**
* @When /^I click the "([^"]*)" option in the "([^"]*)" listbox$/
* @param $optionLabel
* @param $fieldName
*/
public function stepIClickTheListBoxOption($optionLabel, $fieldName)
{
$page = $this->getSession()->getPage();
$listBox = $page->find('xpath', sprintf('//*[@name="%s[]"]', $fieldName));
assertNotNull($listBox, sprintf('The listbox %s is not found', $fieldName));
$option = $listBox->getParent()
->find('css', '.chosen-choices')
->find('xpath', sprintf('//*[count(*)=0 and contains(.,"%s")]', $optionLabel));
assertNotNull($option, sprintf('Option %s is not found', $optionLabel));
$button = $option->getParent()->find('css', 'a');
$button->click();
}
} }