Skip to content

Commit

Permalink
API Check model permissions in GridField
Browse files Browse the repository at this point in the history
  • Loading branch information
chillu committed Dec 16, 2012
1 parent 22eeaa4 commit 1848d7e
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 15 deletions.
33 changes: 33 additions & 0 deletions docs/en/changelogs/3.1.0.md
Expand Up @@ -44,6 +44,39 @@ you'll need to adjust your code.
}
}

### GridField and ModelAdmin Permission Checks

`GridFieldDetailForm` now checks for `canEdit()` and `canDelete()` permissions
on your model. `GridFieldAddNewButton` checks `canCreate()`.
The default implementation requires `ADMIN` permissions.
You'll need to loosen those permissions if you want other users with CMS
access to interact with your data.
Since `GridField` is used in `ModelAdmin`, this change will affect both classes.

Example: Require "CMS: Pages section" access

:::php
class MyModel extends DataObject {
public function canView($member = null) {
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
}
public function canEdit($member = null) {
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
}
public function canDelete($member = null) {
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
}
public function canCreate($member = null) {
return Permission::check('CMS_ACCESS_CMSMain', 'any', $member);
}

You can also implement [custom permission codes](/topics/permissions).
For 3.1.0 stable, we aim to further simplify the permission definitions,
in order to reduce the boilerplate code required to get a model editable in the CMS.

Note: GridField is already relying on the permission checks performed
through the CMS controllers, providing a simple level of security.

### Other

* `TableListField`, `ComplexTableField`, `TableField`, `HasOneComplexTableField`, `HasManyComplexTableField` and `ManyManyComplexTableField` have been removed from the core and placed into a module called "legacytablefields" located at https://github.com/silverstripe-labs/legacytablefields
Expand Down
10 changes: 10 additions & 0 deletions docs/en/reference/grid-field.md
Expand Up @@ -310,6 +310,16 @@ transfered between page requests by being inserted as a hidden field in the form

A GridFieldComponent sets and gets data from the GridState.

## Permissions

Since GridField is mostly used in the CMS, the controller managing a GridField instance
will already do some permission checks for you, and can decline display or executing
any logic on your field.

If you need more granular control, e.g. to consistently deny non-admins from deleting
records, use the `DataObject->can...()` methods
(see [DataObject permissions](/reference/dataobject#permissions)).

## Related

* [ModelAdmin: A UI driven by GridField](/reference/modeladmin)
Expand Down
6 changes: 5 additions & 1 deletion forms/gridfield/GridFieldAddNewButton.php
@@ -1,6 +1,7 @@
<?php
/**
* This component provides a button for opening the add new form provided by {@link GridFieldDetailForm}.
* Only returns a button if {@link DataObject->canCreate()} for this record returns true.
*
* @package framework
* @subpackage gridfield
Expand All @@ -21,9 +22,12 @@ public function __construct($targetFragment = 'before') {
}

public function getHTMLFragments($gridField) {
$singleton = singleton($gridField->getModelClass());
if(!$singleton->canCreate()) return array();

if(!$this->buttonName) {
// provide a default button name, can be changed by calling {@link setButtonName()} on this component
$objectName = singleton($gridField->getModelClass())->i18n_singular_name();
$objectName = $singleton->i18n_singular_name();
$this->buttonName = _t('GridField.Add', 'Add {name}', array('name' => $objectName));
}

Expand Down
22 changes: 15 additions & 7 deletions forms/gridfield/GridFieldDeleteAction.php
Expand Up @@ -98,15 +98,16 @@ public function getActions($gridField) {
*/
public function getColumnContent($gridField, $record, $columnName) {
if($this->removeRelation) {
if(!$record->canEdit()) return;

$field = GridField_FormAction::create($gridField, 'UnlinkRelation'.$record->ID, false,
"unlinkrelation", array('RecordID' => $record->ID))
->addExtraClass('gridfield-button-unlink')
->setAttribute('title', _t('GridAction.UnlinkRelation', "Unlink"))
->setAttribute('data-icon', 'chain--minus');
} else {
if(!$record->canDelete()) {
return;
}
if(!$record->canDelete()) return;

$field = GridField_FormAction::create($gridField, 'DeleteRecord'.$record->ID, false, "deleterecord",
array('RecordID' => $record->ID))
->addExtraClass('gridfield-button-delete')
Expand All @@ -132,13 +133,20 @@ public function handleAction(GridField $gridField, $actionName, $arguments, $dat
if(!$item) {
return;
}
if($actionName == 'deleterecord' && !$item->canDelete()) {
throw new ValidationException(
_t('GridFieldAction_Delete.DeletePermissionsFailure',"No delete permissions"),0);
}

if($actionName == 'deleterecord') {
if(!$item->canDelete()) {
throw new ValidationException(
_t('GridFieldAction_Delete.DeletePermissionsFailure',"No delete permissions"),0);
}

$item->delete();
} else {
if(!$item->canEdit()) {
throw new ValidationException(
_t('GridFieldAction_Delete.EditPermissionsFailure',"No permission to unlink record"),0);
}

$gridField->getList()->remove($item);
}
}
Expand Down
41 changes: 34 additions & 7 deletions forms/gridfield/GridFieldDetailForm.php
Expand Up @@ -310,16 +310,31 @@ public function ItemEditForm() {
return $controller->redirect($noActionURL, 302);
}

$canView = $this->record->canView();
$canEdit = $this->record->canEdit();
$canDelete = $this->record->canDelete();
$canCreate = $this->record->canCreate();

if(!$canView) {
$controller = Controller::curr();
// TODO More friendly error
return $controller->httpError(403);
}

$actions = new FieldList();
if($this->record->ID !== 0) {
$actions->push(FormAction::create('doSave', _t('GridFieldDetailForm.Save', 'Save'))
->setUseButtonTag(true)
->addExtraClass('ss-ui-action-constructive')
->setAttribute('data-icon', 'accept'));
if($canEdit) {
$actions->push(FormAction::create('doSave', _t('GridFieldDetailForm.Save', 'Save'))
->setUseButtonTag(true)
->addExtraClass('ss-ui-action-constructive')
->setAttribute('data-icon', 'accept'));
}

$actions->push(FormAction::create('doDelete', _t('GridFieldDetailForm.Delete', 'Delete'))
->setUseButtonTag(true)
->addExtraClass('ss-ui-action-destructive'));
if($canDelete) {
$actions->push(FormAction::create('doDelete', _t('GridFieldDetailForm.Delete', 'Delete'))
->setUseButtonTag(true)
->addExtraClass('ss-ui-action-destructive'));
}

}else{ // adding new record
//Change the Save label to 'Create'
Expand Down Expand Up @@ -353,6 +368,14 @@ public function ItemEditForm() {

$form->loadDataFrom($this->record, $this->record->ID == 0 ? Form::MERGE_IGNORE_FALSEISH : Form::MERGE_DEFAULT);

if($this->record->ID && !$canEdit) {
// Restrict editing of existing records
$form->makeReadonly();
} elseif(!$this->record->ID && !$canCreate) {
// Restrict creation of new records
$form->makeReadonly();
}

// Load many_many extraData for record.
// Fields with the correct 'ManyMany' namespace need to be added manually through getCMSFields().
if($list instanceof ManyManyList) {
Expand Down Expand Up @@ -429,6 +452,10 @@ public function doSave($data, $form) {
$extraData = null;
}

if(!$this->record->canEdit()) {
return $controller->httpError(403);
}

try {
$form->saveInto($this->record);
$this->record->write();
Expand Down
4 changes: 4 additions & 0 deletions tests/forms/gridfield/GridFieldDetailFormTest.php
Expand Up @@ -192,13 +192,17 @@ public function testNestedEditForm() {
}

public function testCustomItemRequestClass() {
$this->logInWithPermission('ADMIN');

$component = new GridFieldDetailForm();
$this->assertEquals('GridFieldDetailForm_ItemRequest', $component->getItemRequestClass());
$component->setItemRequestClass('GridFieldDetailFormTest_ItemRequest');
$this->assertEquals('GridFieldDetailFormTest_ItemRequest', $component->getItemRequestClass());
}

public function testItemEditFormCallback() {
$this->logInWithPermission('ADMIN');

$category = new GridFieldDetailFormTest_Category();
$component = new GridFieldDetailForm();
$component->setItemEditFormCallback(function($form, $component) {
Expand Down

0 comments on commit 1848d7e

Please sign in to comment.