Added ability to secure DMS assets by user groups.

This commit is contained in:
Franco Springveldt 2017-05-08 15:30:24 +12:00
parent 38501542ee
commit 8682c7fca9
8 changed files with 344 additions and 61 deletions

View File

@ -15,11 +15,11 @@ if (!file_exists(BASE_PATH . DIRECTORY_SEPARATOR . DMS_DIR)) {
CMSMenu::remove_menu_item('DMSDocumentAddController');
ShortcodeParser::get('default')->register(
'dms_document_link',
'dms_document_link',
array('DMSShortcodeHandler', 'handle')
);
if ($config->get('DMSDocument_versions', 'enable_versions')) {
//using the same db relations for the versioned documents, as for the actual documents
$config->update('DMSDocument_versions', 'db', $config->get('DMSDocument', 'db'));
//using the same db relations for the versioned documents, as for the actual documents
$config->update('DMSDocument_versions', 'db', $config->get('DMSDocument', 'db'));
}

View File

@ -2,6 +2,26 @@
/**
* @package dms
*
* @property Varchar Filename
* @property Varchar Folder
* @property Varchar Title
* @property Text Description
* @property int ViewCount
* @property DateTime LastChanged
* @property Boolean EmbargoedIndefinitely
* @property Boolean EmbargoedUntilPublished
* @property DateTime EmbargoedUntilDate
* @property DateTime ExpireAtDate
* @property Enum DownloadBehavior
* @property Enum CanViewType Enum('Anyone, LoggedInUsers, OnlyTheseUsers', 'Anyone')
* @property Enum CanEditType Enum('LoggedInUsers, OnlyTheseUsers', 'LoggedInUsers')
*
* @method ManyManyList RelatedDocuments
* @method ManyManyList Tags
* @method ManyManyList ViewerGroups
* @method ManyManyList EditorGroups
*
*/
class DMSDocument extends DataObject implements DMSDocumentInterface
{
@ -19,12 +39,16 @@ class DMSDocument extends DataObject implements DMSDocumentInterface
"EmbargoedUntilDate" => 'SS_DateTime',
"ExpireAtDate" => 'SS_DateTime',
"DownloadBehavior" => 'Enum(array("open","download"), "download")',
"CanViewType" => "Enum('Anyone, LoggedInUsers, OnlyTheseUsers', 'Anyone')",
"CanEditType" => "Enum('LoggedInUsers, OnlyTheseUsers', 'LoggedInUsers')",
);
private static $many_many = array(
'Pages' => 'SiteTree',
'RelatedDocuments' => 'DMSDocument',
'Tags' => 'DMSTag'
'Tags' => 'DMSTag',
'ViewerGroups' => 'Group',
'EditorGroups' => 'Group',
);
private static $many_many_extraFields = array(
@ -67,11 +91,6 @@ class DMSDocument extends DataObject implements DMSDocumentInterface
*/
private static $default_download_behaviour = 'download';
/**
* @param Member $member
*
* @return boolean
*/
public function canView($member = null)
{
if (!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
@ -87,18 +106,36 @@ class DMSDocument extends DataObject implements DMSDocumentInterface
}
}
if ($member && $member->ID) {
if (!$this->CanViewType || $this->CanViewType == 'Anyone') {
return true;
}
return false;
if ($member && Permission::checkMember($member,
array(
'ADMIN',
'SITETREE_EDIT_ALL',
'SITETREE_VIEW_ALL',
)
)
) {
return true;
}
if ($this->isHidden()) {
return false;
}
if ($this->CanViewType == 'LoggedInUsers') {
return $member && $member->exists();
}
if ($this->CanViewType == 'OnlyTheseUsers' && $this->ViewerGroups()->count()) {
return ($member && $member->inGroups($this->ViewerGroups()));
}
return $this->canEdit($member);
}
/**
* @param Member $member
*
* @return boolean
*/
public function canEdit($member = null)
{
if (!$member || !(is_a($member, 'Member')) || is_numeric($member)) {
@ -113,7 +150,27 @@ class DMSDocument extends DataObject implements DMSDocumentInterface
}
}
return $this->canView();
// Do early admin check
if ($member && Permission::checkMember($member,
array(
'ADMIN',
'SITETREE_EDIT_ALL',
'SITETREE_VIEW_ALL',
)
)
) {
return true;
}
if ($this->CanEditType === 'LoggedInUsers') {
return $member && $member->exists();
}
if ($this->CanEditType === 'OnlyTheseUsers' && $this->EditorGroups()->count()) {
return $member && $member->inGroups($this->EditorGroups());
}
return ($member && Permission::checkMember($member, array('ADMIN', 'SITETREE_EDIT_ALL')));
}
/**
@ -135,7 +192,7 @@ class DMSDocument extends DataObject implements DMSDocumentInterface
}
}
return $this->canView();
return $this->canEdit($member);
}
/**
@ -294,7 +351,7 @@ class DMSDocument extends DataObject implements DMSDocumentInterface
* @param string $category of a metadata category to add (required)
* @param string $value of a metadata value to add (required)
* @param bool $multiValue Boolean that determines if the category is
* multi-value or single-value (optional)
* multi-value or single-value (optional)
*
* @return DMSDocument
*/
@ -445,12 +502,30 @@ class DMSDocument extends DataObject implements DMSDocumentInterface
/**
* Returns a link to download this document from the DMS store.
* Alternatively a basic javascript alert will be shown should the user not have view permissions. An extension
* point for this was also added.
*
* To extend use the following from within an Extension subclass:
*
* <code>
* public function updateGetLink($result)
* {
* // Do something here
* }
* </code>
*
* @return string
*/
public function getLink()
{
return Controller::join_links(Director::baseURL(), 'dmsdocument/'.$this->ID);
$result = Controller::join_links(Director::baseURL(), 'dmsdocument/' . $this->ID);
if (!$this->canView()) {
$result = sprintf("javascript:alert('%s')", $this->getPermissionDeniedReason());
}
$this->extend('updateGetLink', $result);
return $result;
}
/**
@ -1095,14 +1170,52 @@ class DMSDocument extends DataObject implements DMSDocumentInterface
$actionsPanel->setName("ActionsPanel");
$actionsPanel->addExtraClass("DMSDocumentActionsPanel");
$fields->push($actionsPanel);
$this->addPermissionsFields($fields);
$this->extend('updateCMSFields', $fields);
return $fields;
}
/**
* Adds permissions selection fields to the FieldList.
*
* @param FieldList $fields
*/
public function addPermissionsFields($fields)
{
$showFields = array(
'CanViewType' => '',
'ViewerGroups' => 'hide',
'CanEditType' => '',
'EditorGroups' => 'hide',
);
/** @var SiteTree $siteTree */
$siteTree = singleton('SiteTree');
$settingsFields = $siteTree->getSettingsFields();
foreach ($showFields as $name => $extraCss) {
$compositeName = "Root.Settings.$name";
/** @var FormField $field */
if ($field = $settingsFields->fieldByName($compositeName)) {
$field->addExtraClass($extraCss);
$title = str_replace('page', 'document', $field->Title());
$field->setTitle($title);
// Remove Inherited source option from DropdownField
if ($field instanceof DropdownField) {
$options = $field->getSource();
unset($options['Inherit']);
$field->setSource($options);
}
$fields->push($field);
}
}
$this->extend('updatePermissionsFields', $fields);
}
public function onBeforeWrite()
{
parent::onBeforeWrite();
@ -1329,4 +1442,55 @@ class DMSDocument extends DataObject implements DMSDocumentInterface
return $gridField;
}
/**
* Checks at least one group is selected if CanViewType || CanEditType == 'OnlyTheseUsers'
*
* @return ValidationResult
*/
protected function validate()
{
$valid = parent::validate();
if ($this->CanViewType == 'OnlyTheseUsers' && !$this->ViewerGroups()->count()) {
$valid->error(
_t(
'DMSDocument.VALIDATIONERROR_NOVIEWERSELECTED',
"Selecting 'Only these people' from a viewers list needs at least one group selected."
)
);
}
if ($this->CanEditType == 'OnlyTheseUsers' && !$this->EditorGroups()->count()) {
$valid->error(
_t(
'DMSDocument.VALIDATIONERROR_NOEDITORSELECTED',
"Selecting 'Only these people' from a editors list needs at least one group selected."
)
);
}
return $valid;
}
/**
* Returns a reason as to why this document cannot be viewed.
*
* @return string
*/
public function getPermissionDeniedReason()
{
$result = '';
if ($this->CanViewType == 'LoggedInUsers') {
$result = _t('DMSDocument.PERMISSIONDENIEDREASON_LOGINREQUIRED', 'Please log in to view this document');
}
if ($this->CanViewType == 'OnlyTheseUsers') {
$result = _t('DMSDocument.PERMISSIONDENIEDREASON_NOTAUTHORISED',
'You are not authorised to view this document');
}
return $result;
}
}

View File

@ -54,39 +54,9 @@ class DMSDocument_Controller extends Controller
{
$doc = $this->getDocumentFromID($request);
if (!empty($doc)) {
$canView = false;
// Runs through all pages that this page links to and sets canView
// to true if the user can view ONE of these pages
if (method_exists($doc, 'Pages')) {
$pages = $doc->Pages();
if ($pages->Count() > 0) {
foreach ($pages as $page) {
if ($page->CanView()) {
// just one canView is enough to know that we can
// view the file
$canView = true;
break;
}
}
} else {
// if the document isn't on any page, then allow viewing of
// the document (because there is no canView() to consult)
$canView = true;
}
}
// check for embargo or expiry
if ($doc->isHidden()) {
$canView = false;
}
//admins can always download any document, even if otherwise hidden
$member = Member::currentUser();
if ($member && Permission::checkMember($member, 'ADMIN')) {
$canView = true;
}
$canView = $doc->canView();
if ($canView) {
$path = $doc->getFullPath();
@ -129,8 +99,7 @@ class DMSDocument_Controller extends Controller
//its ViewCount should be increased by 1 just before the browser sending the file to front.
$doc->trackView();
$this->sendFile($path, $mime, $doc->getFilenameWithoutID(), $disposition);
return;
return $this->sendFile($path, $mime, $doc->getFilenameWithoutID(), $disposition);
}
}
}

View File

@ -1,7 +1,7 @@
(function ($) {
(function($) {
"use strict";
$.entwine('ss', function ($) {
$.entwine('ss', function($) {
$('#DocumentTypeID ul li').entwine({
onadd: function () {
@ -11,7 +11,7 @@
this.addClass('selected');
}
},
onclick: function (e) {
onclick: function(e) {
$('#DocumentTypeID').find('li.selected').removeClass('selected');
this.find('input').prop("checked", true);
this.addClass('selected');
@ -34,7 +34,22 @@
}
}
});*/
$('#CanViewType input, #CanEditType input').entwine({
onchange: function () {
var dropDown = $(this).closest('.field').next();
if ($(this).val() === 'OnlyTheseUsers') {
dropDown.removeClass('hide');
} else {
dropDown.addClass('hide');
}
},
onadd: function () {
if ($(this).is(':checked') && $(this).val() === 'OnlyTheseUsers') {
var dropDown = $(this).closest('.field').next();
dropDown.removeClass('hide');
}
}
});
$('#Actions ul li').entwine({
onclick: function (e) {
@ -56,7 +71,6 @@
$('#Form_ItemEditForm_Embargo input, #Form_EditForm_Embargo input').entwine({
onchange: function () {
console.log('called');
//selected the date options
if (this.attr('value') === 'Date') {
$('.embargoDatetime').children().show();

View File

@ -1,4 +1,4 @@
<% if $isHidden != true %>
<% if not $isHidden %>
<div class="document $Extension">
<% if $Title %>
<h4><a href="$Link" title="<%t DMSDocument.DOWNLOAD "Download {title}" title=$Title %>">$Title</a></h4>

View File

@ -237,4 +237,103 @@ class DMSDocumentTest extends SapphireTest
'Related documents GridField does not have an "add new" button'
);
}
/*
* Tests whether the permissions fields are added
*/
public function testAddPermissionsFields()
{
$document = $this->objFromFixture('DMSDocument', 'd1');
$fields = $document->getCMSFields();
$this->assertNotNull($fields->fieldByName('CanViewType'));
$this->assertNotNull($fields->fieldByName('ViewerGroups'));
}
/**
* Test view permissions
*/
public function testCanView()
{
/** @var DMSDocument $document */
$document = $this->objFromFixture('DMSDocument', 'doc-logged-in-users');
// Make sure user is logged out
if ($member = Member::currentUser()) {
$member->logOut();
}
// Logged out user test
$this->assertFalse($document->canView());
// Logged in user test
$adminID = $this->logInWithPermission();
$admin = Member::get()->byID($adminID);
$this->assertTrue($document->canView($admin));
/** @var Member $member */
$admin->logout();
// Check anyone
$document = $this->objFromFixture('DMSDocument', 'doc-anyone');
$this->assertTrue($document->canView());
// Check OnlyTheseUsers
$document = $this->objFromFixture('DMSDocument', 'doc-only-these-users');
$reportAdminID = $this->logInWithPermission('cable-guy');
/** @var Member $reportAdmin */
$reportAdmin = Member::get()->byID($reportAdminID);
$this->assertFalse($document->canView($reportAdmin));
// Add reportAdmin to group
$reportAdmin->addToGroupByCode('content-author');
$this->assertTrue($document->canView($reportAdmin));
$reportAdmin->logout();
}
/**
* Tests edit permissions
*/
public function testCanEdit()
{
// Make sure user is logged out
if ($member = Member::currentUser()) {
$member->logOut();
}
/** @var DMSDocument $document1 */
$document1 = $this->objFromFixture('DMSDocument', 'doc-logged-in-users');
// Logged out user test
$this->assertFalse($document1->canEdit());
//Logged in user test
$contentAuthor = $this->objFromFixture('Member', 'editor');
$this->assertTrue($document1->canEdit($contentAuthor));
// Check OnlyTheseUsers
/** @var DMSDocument $document2 */
$document2 = $this->objFromFixture('DMSDocument', 'doc-only-these-users');
/** @var Member $cableGuy */
$cableGuy = $this->objFromFixture('Member', 'non-editor');
$this->assertFalse($document2->canEdit($cableGuy));
$cableGuy->addToGroupByCode('content-author');
$this->assertTrue($document2->canEdit($cableGuy));
}
/**
* Test permission denied reasons for documents
*/
public function testGetPermissionDeniedReason()
{
/** @var DMSDocument $document1 */
$doc1 = $this->objFromFixture('DMSDocument', 'doc-logged-in-users');
$this->assertContains('Please log in to view this document', $doc1->getPermissionDeniedReason());
/** @var DMSDocument $doc2 */
$doc2 = $this->objFromFixture('DMSDocument', 'doc-only-these-users');
$this->assertContains('You are not authorised to view this document', $doc2->getPermissionDeniedReason());
/** @var DMSDocument $doc3 */
$doc3 = $this->objFromFixture('DMSDocument', 'doc-anyone');
$this->assertEquals('', $doc3->getPermissionDeniedReason());
}
}

View File

@ -35,6 +35,7 @@ class DMSEmbargoTest extends SapphireTest
$doc = new DMSDocument();
$doc->Filename = "DMS-test-lorum-file.pdf";
$doc->Folder = "tests";
$doc->CanViewType = 'LoggedInUsers';
$docID = $doc->write();
//fake a request for a document

View File

@ -36,6 +36,13 @@ DMSTag:
t6:
Category: tag6
Value: tag6value
Group:
content-author:
Code: content-author
Title: "Content Author"
cable-guy:
Code: cable-guy
Title: "Cable Guy"
DMSDocument:
d1:
Filename: test-file-file-doesnt-exist-1
@ -51,3 +58,32 @@ DMSDocument:
Filename: file-with-relations
Folder: 5
RelatedDocuments: =>DMSDocument.d1, =>DMSDocument.d2
doc-logged-in-users:
FileName: doc-logged-in-users
CanViewType: LoggedInUsers
CanEditType: LoggedInUsers
Folder: 5
Tags: =>DMSTag.t1, =>DMSTag.t2, =>DMSTag.t3, =>DMSTag.t4
Pages: =>SiteTree.s1, =>SiteTree.s2, =>SiteTree.s3, =>SiteTree.s4, =>SiteTree.s5, =>SiteTree.s6
doc-anyone:
FileName: doc-anyone
CanViewType: Anyone
Folder: 5
Tags: =>DMSTag.t1, =>DMSTag.t2, =>DMSTag.t3, =>DMSTag.t4
Pages: =>SiteTree.s1, =>SiteTree.s2, =>SiteTree.s3, =>SiteTree.s4, =>SiteTree.s5, =>SiteTree.s6
doc-only-these-users:
FileName: doc-only-these-users
CanViewType: OnlyTheseUsers
CanEditType: OnlyTheseUsers
Folder: 5
Tags: =>DMSTag.t1, =>DMSTag.t2, =>DMSTag.t3, =>DMSTag.t4
Pages: =>SiteTree.s1, =>SiteTree.s2, =>SiteTree.s3, =>SiteTree.s4, =>SiteTree.s5, =>SiteTree.s6
ViewerGroups: =>Group.content-author
EditorGroups: =>Group.content-author
Member:
editor:
Name: editor
Groups: =>Group.content-author
non-editor:
Name: cable-guy
Groups: =>Group.cable-guy