mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
API Create default security permission model for versioned data objects
This commit is contained in:
parent
1bc7297c8f
commit
6089a7c5bd
@ -137,8 +137,76 @@ Example: Get the first 10 live records, filtered by creation date:
|
||||
|
||||
### Permissions
|
||||
|
||||
The `Versioned` extension doesn't provide any permissions on its own, but you can have a look at the `SiteTree` class
|
||||
for implementation samples, specifically `canPublish()` and `canDeleteFromStage()`.
|
||||
By default, `Versioned` will come out of the box with security extensions which restrict
|
||||
the visibility of objects in Draft (stage) or Archive viewing mode.
|
||||
|
||||
<div class="alert" markdown="1">
|
||||
As is standard practice, user code should always invoke `canView()` on any object before
|
||||
rendering it. DataLists do not filter on `canView()` automatically, so this must be
|
||||
done via user code. This be be achieved either by wrapping `<% if $canView %>` in
|
||||
your template, or by implementing your visibility check in PHP.
|
||||
</div>
|
||||
|
||||
Versioned object visibility can be customised in one of the following ways by editing your user code:
|
||||
|
||||
* Override the `canViewVersioned` method in your code. Make sure that this returns true or
|
||||
false if the user is not allowed to view this object in the current viewing mode.
|
||||
* Override the `canView` method to override the method visibility completely.
|
||||
|
||||
E.g.
|
||||
|
||||
:::php
|
||||
class MyObject extends DataObject {
|
||||
private static $extensions = array(
|
||||
'Versioned'
|
||||
);
|
||||
|
||||
public function canViewVersioned($member = null) {
|
||||
// Check if site is live
|
||||
$mode = $this->getSourceQueryParam("Versioned.mode");
|
||||
$stage = $this->getSourceQueryParam("Versioned.stage");
|
||||
if ($mode === 'Stage' && $stage === 'Live') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Only admins can view non-live objects
|
||||
return Permission::checkMember($member, 'ADMIN');
|
||||
}
|
||||
}
|
||||
|
||||
If you want to control permissions of an object in an extension, you can also use
|
||||
one of the below extension points in your `DataExtension` subclass:
|
||||
|
||||
* `canView` to update the visibility of the object's `canView`
|
||||
* `canViewNonLive` to update the visibility of this object only in non-live mode.
|
||||
|
||||
Note that unlike canViewVersioned, the canViewNonLive method will
|
||||
only be invoked if the object is in a non-published state.
|
||||
|
||||
E.g.
|
||||
|
||||
:::php
|
||||
class MyObjectExtension extends DataExtension {
|
||||
public function canViewNonLive($member = null) {
|
||||
return Permission::check($member, 'DRAFT_STATUS');
|
||||
}
|
||||
}
|
||||
|
||||
If none of the above checks are overridden, visibility will be determined by the
|
||||
permissions in the `TargetObject.non_live_permissions` config.
|
||||
|
||||
E.g.
|
||||
|
||||
:::php
|
||||
class MyObject extends DataObject {
|
||||
private static $extensions = array(
|
||||
'Versioned'
|
||||
);
|
||||
private static $non_live_permissions = array('ADMIN');
|
||||
}
|
||||
|
||||
Versioned applies no additional permissions to `canEdit` or `canCreate`, and such
|
||||
these permissions should be implemented as per standard unversioned DataObjects.
|
||||
|
||||
### Page Specific Operations
|
||||
|
||||
|
@ -130,6 +130,14 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
*/
|
||||
protected static $versionableExtensions = array('Translatable' => 'lang');
|
||||
|
||||
/**
|
||||
* Permissions necessary to view records outside of the live stage (e.g. archive / draft stage).
|
||||
*
|
||||
* @config
|
||||
* @var array
|
||||
*/
|
||||
private static $non_live_permissions = array('CMS_ACCESS_LeftAndMain', 'CMS_ACCESS_CMSMain', 'VIEW_DRAFT_CONTENT');
|
||||
|
||||
/**
|
||||
* Reset static configuration variables to their default values.
|
||||
*/
|
||||
@ -728,6 +736,91 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
$this->migrateVersion(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend permissions to include additional security for objects that are not published to live.
|
||||
*
|
||||
* @param Member $member
|
||||
* @return bool|null
|
||||
*/
|
||||
public function canView($member = null) {
|
||||
// Invoke default version-gnostic canView
|
||||
if ($this->owner->canViewVersioned($member) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if there are any additional restrictions on this object for the given reading version.
|
||||
*
|
||||
* Override this in a subclass to customise any additional effect that Versioned applies to canView.
|
||||
*
|
||||
* This is expected to be called by canView, and thus is only responsible for denying access if
|
||||
* the default canView would otherwise ALLOW access. Thus it should not be called in isolation
|
||||
* as an authoritative permission check.
|
||||
*
|
||||
* This has the following extension points:
|
||||
* - canViewDraft is invoked if Mode = stage and Stage = stage
|
||||
* - canViewArchived is invoked if Mode = archive
|
||||
*
|
||||
* @param Member $member
|
||||
* @return bool False is returned if the current viewing mode denies visibility
|
||||
*/
|
||||
public function canViewVersioned($member = null) {
|
||||
// Bypass when live stage
|
||||
$mode = $this->owner->getSourceQueryParam("Versioned.mode");
|
||||
$stage = $this->owner->getSourceQueryParam("Versioned.stage");
|
||||
if ($mode === 'stage' && $stage === static::get_live_stage()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bypass if site is unsecured
|
||||
if (Session::get('unsecuredDraftSite')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we weren't definitely loaded from live, and we can't view non-live content, we need to
|
||||
// check to make sure this version is the live version and so can be viewed
|
||||
$latestVersion = Versioned::get_versionnumber_by_stage($this->owner->class, 'Live', $this->owner->ID);
|
||||
if ($latestVersion == $this->owner->Version) {
|
||||
// Even if this is loaded from a non-live stage, this is the live version
|
||||
return true;
|
||||
}
|
||||
|
||||
// Extend versioned behaviour
|
||||
$extended = $this->owner->extendedCan('canViewNonLive', $member);
|
||||
if($extended !== null) {
|
||||
return (bool)$extended;
|
||||
}
|
||||
|
||||
// Fall back to default permission check
|
||||
$permissions = Config::inst()->get($this->owner->class, 'non_live_permissions', Config::FIRST_SET);
|
||||
$check = Permission::checkMember($member, $permissions);
|
||||
return (bool)$check;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines canView permissions for the latest version of this object on a specific stage.
|
||||
* Usually the stage is read from {@link Versioned::current_stage()}.
|
||||
*
|
||||
* This method should be invoked by user code to check if a record is visible in the given stage.
|
||||
*
|
||||
* This method should not be called via ->extend('canViewStage'), but rather should be
|
||||
* overridden in the extended class.
|
||||
*
|
||||
* @param string $stage
|
||||
* @param Member $member
|
||||
* @return bool
|
||||
*/
|
||||
public function canViewStage($stage = 'Live', $member = null) {
|
||||
$oldMode = Versioned::get_reading_mode();
|
||||
Versioned::reading_stage($stage);
|
||||
|
||||
$versionFromStage = DataObject::get($this->class)->byID($this->ID);
|
||||
|
||||
Versioned::set_reading_mode($oldMode);
|
||||
return $versionFromStage ? $versionFromStage->canView($member) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a table is supporting the Versioned extensions (e.g.
|
||||
* $table_versions does exists).
|
||||
|
@ -24,10 +24,23 @@ class DataObjectLazyLoadingTest extends SapphireTest {
|
||||
'DataObjectTest_TeamComment',
|
||||
'DataObjectTest_EquipmentCompany',
|
||||
'DataObjectTest_SubEquipmentCompany',
|
||||
'VersionedTest_DataObject',
|
||||
'VersionedTest_Subclass',
|
||||
'DataObjectTest\NamespacedClass',
|
||||
'DataObjectTest\RelationClass',
|
||||
'DataObjectTest_ExtendedTeamComment',
|
||||
'DataObjectTest_Company',
|
||||
'DataObjectTest_Staff',
|
||||
'DataObjectTest_CEO',
|
||||
'DataObjectTest_Fan',
|
||||
'VersionedLazy_DataObject',
|
||||
'VersionedLazySub_DataObject',
|
||||
'VersionedTest_DataObject',
|
||||
'VersionedTest_Subclass',
|
||||
'VersionedTest_AnotherSubclass',
|
||||
'VersionedTest_RelatedWithoutVersion',
|
||||
'VersionedTest_SingleStage',
|
||||
'VersionedTest_WithIndexes',
|
||||
'VersionedTest_PublicStage',
|
||||
'VersionedTest_PublicViaExtension',
|
||||
);
|
||||
|
||||
public function testQueriedColumnsID() {
|
||||
|
@ -15,6 +15,8 @@ class VersionedTest extends SapphireTest {
|
||||
'VersionedTest_RelatedWithoutVersion',
|
||||
'VersionedTest_SingleStage',
|
||||
'VersionedTest_WithIndexes',
|
||||
'VersionedTest_PublicStage',
|
||||
'VersionedTest_PublicViaExtension',
|
||||
);
|
||||
|
||||
protected $requiredExtensions = array(
|
||||
@ -750,6 +752,54 @@ class VersionedTest extends SapphireTest {
|
||||
$testData->NewField = 'Test';
|
||||
$testData->write();
|
||||
}
|
||||
|
||||
public function testCanView() {
|
||||
$public1ID = $this->idFromFixture('VersionedTest_PublicStage', 'public1');
|
||||
$public2ID = $this->idFromFixture('VersionedTest_PublicViaExtension', 'public2');
|
||||
$privateID = $this->idFromFixture('VersionedTest_DataObject', 'page1');
|
||||
|
||||
// Test that all (and only) public pages are viewable in stage mode
|
||||
Session::clear("loggedInAs");
|
||||
Versioned::reading_stage('Stage');
|
||||
$public1 = Versioned::get_one_by_stage('VersionedTest_PublicStage', 'Stage', array('"ID"' => $public1ID));
|
||||
$public2 = Versioned::get_one_by_stage('VersionedTest_PublicViaExtension', 'Stage', array('"ID"' => $public2ID));
|
||||
$private = Versioned::get_one_by_stage('VersionedTest_DataObject', 'Stage', array('"ID"' => $privateID));
|
||||
|
||||
$this->assertTrue($public1->canView());
|
||||
$this->assertTrue($public2->canView());
|
||||
$this->assertFalse($private->canView());
|
||||
|
||||
// Adjusting the current stage should not allow objects loaded in stage to be viewable
|
||||
Versioned::reading_stage('Live');
|
||||
$this->assertTrue($public1->canView());
|
||||
$this->assertTrue($public2->canView());
|
||||
$this->assertFalse($private->canView());
|
||||
|
||||
// Writing the private page to live should be fine though
|
||||
$private->publish("Stage", "Live");
|
||||
$privateLive = Versioned::get_one_by_stage('VersionedTest_DataObject', 'Live', array('"ID"' => $privateID));
|
||||
$this->assertTrue($private->canView());
|
||||
$this->assertTrue($privateLive->canView());
|
||||
|
||||
// But if the private version becomes different to the live version, it's once again disallowed
|
||||
Versioned::reading_stage('Stage');
|
||||
$private->Title = 'Secret Title';
|
||||
$private->write();
|
||||
$this->assertFalse($private->canView());
|
||||
$this->assertTrue($privateLive->canView());
|
||||
|
||||
// And likewise, viewing a live page (when mode is draft) should be ok
|
||||
Versioned::reading_stage('Stage');
|
||||
$this->assertFalse($private->canView());
|
||||
$this->assertTrue($privateLive->canView());
|
||||
|
||||
// Logging in as admin should allow all permissions
|
||||
$this->logInWithPermission('ADMIN');
|
||||
Versioned::reading_stage('Stage');
|
||||
$this->assertTrue($public1->canView());
|
||||
$this->assertTrue($public2->canView());
|
||||
$this->assertTrue($private->canView());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -776,6 +826,14 @@ class VersionedTest_DataObject extends DataObject implements TestOnly {
|
||||
'Related' => 'VersionedTest_RelatedWithoutVersion'
|
||||
);
|
||||
|
||||
|
||||
public function canView($member = null) {
|
||||
$extended = $this->extendedCan(__FUNCTION__, $member);
|
||||
if($extended !== null) {
|
||||
return $extended;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class VersionedTest_WithIndexes extends DataObject implements TestOnly {
|
||||
@ -851,3 +909,61 @@ class VersionedTest_SingleStage extends DataObject implements TestOnly {
|
||||
'Versioned("Stage")'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Versioned dataobject with public stage mode
|
||||
*/
|
||||
class VersionedTest_PublicStage extends DataObject implements TestOnly {
|
||||
private static $db = array(
|
||||
'Title' => 'Varchar'
|
||||
);
|
||||
|
||||
private static $extensions = array(
|
||||
"Versioned('Stage', 'Live')"
|
||||
);
|
||||
|
||||
public function canView($member = null) {
|
||||
$extended = $this->extendedCan(__FUNCTION__, $member);
|
||||
if($extended !== null) {
|
||||
return $extended;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function canViewVersioned($member = null) {
|
||||
// All non-live modes are public
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Public access is provided via extension rather than overriding canViewVersioned
|
||||
*/
|
||||
class VersionedTest_PublicViaExtension extends DataObject implements TestOnly {
|
||||
|
||||
public function canView($member = null) {
|
||||
$extended = $this->extendedCan(__FUNCTION__, $member);
|
||||
if($extended !== null) {
|
||||
return $extended;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static $db = array(
|
||||
'Title' => 'Varchar'
|
||||
);
|
||||
|
||||
private static $extensions = array(
|
||||
"Versioned('Stage', 'Live')",
|
||||
"VersionedTest_PublicExtension"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alters stage mode of extended object to be public
|
||||
*/
|
||||
class VersionedTest_PublicExtension extends DataExtension implements TestOnly {
|
||||
public function canViewNonLive($member = null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -17,3 +17,9 @@ VersionedTest_DataObject:
|
||||
page3b:
|
||||
Parent: =>VersionedTest_DataObject.page3
|
||||
Title: Page 3b
|
||||
VersionedTest_PublicStage:
|
||||
public1:
|
||||
Title: 'Some page'
|
||||
VersionedTest_PublicViaExtension:
|
||||
public2:
|
||||
Title: 'Another page'
|
Loading…
Reference in New Issue
Block a user