API Check model permissions in GridField

This commit is contained in:
Ingo Schommer 2012-12-17 00:46:51 +01:00
parent 22eeaa4ac1
commit 1848d7e90a
6 changed files with 101 additions and 15 deletions

View File

@ -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 ### 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 * `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

View File

@ -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. 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 ## Related
* [ModelAdmin: A UI driven by GridField](/reference/modeladmin) * [ModelAdmin: A UI driven by GridField](/reference/modeladmin)

View File

@ -1,6 +1,7 @@
<?php <?php
/** /**
* This component provides a button for opening the add new form provided by {@link GridFieldDetailForm}. * 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 * @package framework
* @subpackage gridfield * @subpackage gridfield
@ -21,9 +22,12 @@ class GridFieldAddNewButton implements GridField_HTMLProvider {
} }
public function getHTMLFragments($gridField) { public function getHTMLFragments($gridField) {
$singleton = singleton($gridField->getModelClass());
if(!$singleton->canCreate()) return array();
if(!$this->buttonName) { if(!$this->buttonName) {
// provide a default button name, can be changed by calling {@link setButtonName()} on this component // 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)); $this->buttonName = _t('GridField.Add', 'Add {name}', array('name' => $objectName));
} }

View File

@ -98,15 +98,16 @@ class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_Actio
*/ */
public function getColumnContent($gridField, $record, $columnName) { public function getColumnContent($gridField, $record, $columnName) {
if($this->removeRelation) { if($this->removeRelation) {
if(!$record->canEdit()) return;
$field = GridField_FormAction::create($gridField, 'UnlinkRelation'.$record->ID, false, $field = GridField_FormAction::create($gridField, 'UnlinkRelation'.$record->ID, false,
"unlinkrelation", array('RecordID' => $record->ID)) "unlinkrelation", array('RecordID' => $record->ID))
->addExtraClass('gridfield-button-unlink') ->addExtraClass('gridfield-button-unlink')
->setAttribute('title', _t('GridAction.UnlinkRelation', "Unlink")) ->setAttribute('title', _t('GridAction.UnlinkRelation', "Unlink"))
->setAttribute('data-icon', 'chain--minus'); ->setAttribute('data-icon', 'chain--minus');
} else { } else {
if(!$record->canDelete()) { if(!$record->canDelete()) return;
return;
}
$field = GridField_FormAction::create($gridField, 'DeleteRecord'.$record->ID, false, "deleterecord", $field = GridField_FormAction::create($gridField, 'DeleteRecord'.$record->ID, false, "deleterecord",
array('RecordID' => $record->ID)) array('RecordID' => $record->ID))
->addExtraClass('gridfield-button-delete') ->addExtraClass('gridfield-button-delete')
@ -132,13 +133,20 @@ class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_Actio
if(!$item) { if(!$item) {
return; return;
} }
if($actionName == 'deleterecord' && !$item->canDelete()) {
throw new ValidationException(
_t('GridFieldAction_Delete.DeletePermissionsFailure',"No delete permissions"),0);
}
if($actionName == 'deleterecord') { if($actionName == 'deleterecord') {
if(!$item->canDelete()) {
throw new ValidationException(
_t('GridFieldAction_Delete.DeletePermissionsFailure',"No delete permissions"),0);
}
$item->delete(); $item->delete();
} else { } else {
if(!$item->canEdit()) {
throw new ValidationException(
_t('GridFieldAction_Delete.EditPermissionsFailure',"No permission to unlink record"),0);
}
$gridField->getList()->remove($item); $gridField->getList()->remove($item);
} }
} }

View File

@ -310,16 +310,31 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
return $controller->redirect($noActionURL, 302); 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(); $actions = new FieldList();
if($this->record->ID !== 0) { if($this->record->ID !== 0) {
$actions->push(FormAction::create('doSave', _t('GridFieldDetailForm.Save', 'Save')) if($canEdit) {
->setUseButtonTag(true) $actions->push(FormAction::create('doSave', _t('GridFieldDetailForm.Save', 'Save'))
->addExtraClass('ss-ui-action-constructive') ->setUseButtonTag(true)
->setAttribute('data-icon', 'accept')); ->addExtraClass('ss-ui-action-constructive')
->setAttribute('data-icon', 'accept'));
}
$actions->push(FormAction::create('doDelete', _t('GridFieldDetailForm.Delete', 'Delete')) if($canDelete) {
->setUseButtonTag(true) $actions->push(FormAction::create('doDelete', _t('GridFieldDetailForm.Delete', 'Delete'))
->addExtraClass('ss-ui-action-destructive')); ->setUseButtonTag(true)
->addExtraClass('ss-ui-action-destructive'));
}
}else{ // adding new record }else{ // adding new record
//Change the Save label to 'Create' //Change the Save label to 'Create'
@ -353,6 +368,14 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
$form->loadDataFrom($this->record, $this->record->ID == 0 ? Form::MERGE_IGNORE_FALSEISH : Form::MERGE_DEFAULT); $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. // Load many_many extraData for record.
// Fields with the correct 'ManyMany' namespace need to be added manually through getCMSFields(). // Fields with the correct 'ManyMany' namespace need to be added manually through getCMSFields().
if($list instanceof ManyManyList) { if($list instanceof ManyManyList) {
@ -429,6 +452,10 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
$extraData = null; $extraData = null;
} }
if(!$this->record->canEdit()) {
return $controller->httpError(403);
}
try { try {
$form->saveInto($this->record); $form->saveInto($this->record);
$this->record->write(); $this->record->write();

View File

@ -192,6 +192,8 @@ class GridFieldDetailFormTest extends FunctionalTest {
} }
public function testCustomItemRequestClass() { public function testCustomItemRequestClass() {
$this->logInWithPermission('ADMIN');
$component = new GridFieldDetailForm(); $component = new GridFieldDetailForm();
$this->assertEquals('GridFieldDetailForm_ItemRequest', $component->getItemRequestClass()); $this->assertEquals('GridFieldDetailForm_ItemRequest', $component->getItemRequestClass());
$component->setItemRequestClass('GridFieldDetailFormTest_ItemRequest'); $component->setItemRequestClass('GridFieldDetailFormTest_ItemRequest');
@ -199,6 +201,8 @@ class GridFieldDetailFormTest extends FunctionalTest {
} }
public function testItemEditFormCallback() { public function testItemEditFormCallback() {
$this->logInWithPermission('ADMIN');
$category = new GridFieldDetailFormTest_Category(); $category = new GridFieldDetailFormTest_Category();
$component = new GridFieldDetailForm(); $component = new GridFieldDetailForm();
$component->setItemEditFormCallback(function($form, $component) { $component->setItemEditFormCallback(function($form, $component) {