Commit de5bb72e authored by kroky6's avatar kroky6

[MOD] extend core tiki permission system to allow trackeritems inherit object...

[MOD] extend core tiki permission system to allow trackeritems inherit object permissions from parent trackers; side effect: permission levels are now composed allowing for greater flexibility, i.e. object permissions are complemented with category permissions and then with global permissions
parent 3730be92
......@@ -2228,6 +2228,7 @@ lib/core/Perms/ResolverFactory/GlobalFactory.php -text
lib/core/Perms/ResolverFactory/ObjectFactory.php -text
lib/core/Perms/ResolverFactory/StaticFactory.php -text
lib/core/Perms/ResolverFactory/TestFactory.php -text
lib/core/Perms/ResolverFactory/TrackerParentFactory.php -text
lib/core/Perms/ResolverFactory/index.php -text
lib/core/Perms/index.php -text
lib/core/Report/Builder.js -text
......@@ -4289,6 +4290,7 @@ lib/test/core/Perms/ResolverFactory/CategoryFactoryTest.php -text
lib/test/core/Perms/ResolverFactory/GlobalFactoryTest.php -text
lib/test/core/Perms/ResolverFactory/ObjectFactoryTest.php -text
lib/test/core/Perms/ResolverFactory/TestFactoryTest.php -text
lib/test/core/Perms/ResolverFactory/TrackerParentFactoryTest.php -text
lib/test/core/Perms/ResolverFactory/index.php -text
lib/test/core/Perms/index.php -text
lib/test/core/Recommendation/BatchTest.php -text
......
......@@ -15,7 +15,7 @@ $smarty = TikiLib::lib('smarty');
global $prefs;
$catobjperms = Perms::getCombined(array( 'type' => $cat_type, 'object' => $cat_objid ));
$catobjperms = Perms::get(array( 'type' => $cat_type, 'object' => $cat_objid ));
if ($prefs['feature_categories'] == 'y' && $catobjperms->modify_object_categories ) {
$categlib = TikiLib::lib('categ');
......
......@@ -17,7 +17,7 @@ $smarty = TikiLib::lib('smarty');
global $prefs;
$catobjperms = Perms::getCombined(array( 'type' => $cat_type, 'object' => $cat_objid ));
$catobjperms = Perms::get(array( 'type' => $cat_type, 'object' => $cat_objid ));
$smarty->assign('mandatory_category', '-1');
if ($prefs['feature_categories'] == 'y' && isset($cat_type) && isset($cat_objid)) {
......
......@@ -1663,7 +1663,7 @@ class CategLib extends ObjectLib
// Change an object's categories
// $objId: A unique identifier of an object of the given type, for example "Foo" for Wiki page Foo.
function update_object_categories($categories, $objId, $objType, $desc=NULL, $name=NULL, $href=NULL, $managedCategories = null, $override_perms = false, $parent = null)
function update_object_categories($categories, $objId, $objType, $desc=NULL, $name=NULL, $href=NULL, $managedCategories = null, $override_perms = false)
{
global $prefs, $user;
$userlib = TikiLib::lib('user');
......@@ -1675,7 +1675,7 @@ class CategLib extends ObjectLib
}
}
$manip = new Category_Manipulator($objType, $objId, $parent);
$manip = new Category_Manipulator($objType, $objId);
if ($override_perms) {
$manip->overrideChecks();
}
......
......@@ -9,7 +9,6 @@ class Category_Manipulator
{
private $objectType;
private $objectId;
private $parent;
private $current = array();
private $managed = array();
......@@ -23,11 +22,10 @@ class Category_Manipulator
private $overrides = array();
private $overrideAll = false;
function __construct($objectType, $objectId, $parent = null)
function __construct($objectType, $objectId)
{
$this->objectType = $objectType;
$this->objectId = $objectId;
$this->parent = $parent;
}
function addRequiredSet(array $categories, $default, $filter=null, $type=null)
......@@ -95,11 +93,7 @@ class Category_Manipulator
{
$objectperms = Perms::get(array('type' => $this->objectType, 'object' => $this->objectId));
$canModifyObject = $objectperms->modify_object_categories;
if( !$canModifyObject && $this->parent ) {
$objectperms = Perms::get(array('type' => $this->parent['objectType'], 'object' => $this->parent['objectId']));
$canModifyObject = $objectperms->modify_object_categories;
}
$out = array();
foreach ($categories as $categ) {
$perms = Perms::get(array('type' => 'category', 'object' => $categ));
......
......@@ -23,12 +23,10 @@
*
* Global permissions may be obtained using Perms::get() without a context.
*
* Please note that the Perms will not be correct for checking of access for
* objects that depend on their parent, for example, even if a trackeritem has
* no object or category perms on itself, the tracker's perms should be considered
* in the checking. However, the Perms object with 'type' => 'trackeritem' will
* only get the perms of the object/it's categories itself and not take into
* account the parent tracker. To do so, use the new Perms::getCombined instead.
* Please note that the Perms will now be correct for checking trackeritem
* context and permissions assigned to parent tracker. If no trackeritem
* specific permissions are set on the object or category level, system will
* check parent tracker permissions before continuing to the global level.
*
* The facade also provides a convenient way to filter lists based on
* permissions. Using the method will also used the underlying::bulk()
......@@ -136,34 +134,6 @@ class Perms
}
}
public static function getCombined( $context = array() ) {
if (! is_array($context)) {
$args = func_get_args();
$context = array(
'type' => $args[0],
'object' => $args[1],
);
}
if ($context['type'] == 'trackeritem') {
$perms = Perms::get('trackeritem', $context['object']);
$resolver = $perms->getResolver();
if (method_exists($resolver, 'from') && $resolver->from() != '') {
// Item permissions are valid if they are assigned directly to the object or category, otherwise
// tracker permissions are better than global ones.
return Perms::get($context);
} else {
$context['type'] = 'tracker';
$context['object'] = TikiLib::lib('trk')->get_tracker_for_item($context['object']);
return Perms::get($context);
}
}
return Perms::get($context);
}
public function getAccessor(array $context = array())
{
$accessor = new Perms_Accessor;
......@@ -358,25 +328,26 @@ class Perms
private function getResolver(array $context)
{
$toSet = array();
$resolver = null;
$finalResolver = false;
foreach ($this->factories as $factory) {
$hash = $factory->getHash($context);
if (isset($this->hashes[$hash])) {
if( isset($this->hashes[$hash]) ) {
$resolver = $this->hashes[$hash];
break;
} else {
$toSet[] = $hash;
$resolver = $toSet[$hash] = $factory->getResolver($context);
}
if ($resolver = $factory->getResolver($context)) {
break;
if( !$resolver ) {
continue;
}
}
if (! $resolver) {
$resolver = false;
if( !$finalResolver ) {
$finalResolver = $resolver;
} else {
$finalResolver->extend($resolver);
}
}
// Limit the amount of hashes preserved to reduce memory consumption
......@@ -384,11 +355,11 @@ class Perms
$this->hashes = array();
}
foreach ($toSet as $hash) {
foreach ($toSet as $hash => $resolver) {
$this->hashes[$hash] = $resolver;
}
return $resolver;
return $finalResolver;
}
private function loadBulk($baseContext, $bulkKey, $data)
......
......@@ -83,6 +83,7 @@ class Perms_Builder
{
$factories = array(
new Perms_ResolverFactory_ObjectFactory,
new Perms_ResolverFactory_TrackerParentFactory
);
if ($this->categories) {
......
......@@ -21,6 +21,18 @@ interface Perms_Resolver
*/
function check( $permission, array $groups );
/*
* Extend resolver permissions with another resolver
* @param Perms_Resolver $resolver - the other resolver
*/
function extend( Perms_Resolver $resolver );
/*
* Retrieve the current permissions of this resolver
* @return mixed - permission list or value depending on resolver type
*/
function getPermissions();
/*
* Get name of the object type the permissons to check belong to : i.e 'object', 'category'
......
......@@ -24,6 +24,14 @@ class Perms_Resolver_Default implements Perms_Resolver
return $this->value;
}
function extend( Perms_Resolver $resolver ) {
$this->value = (bool)$resolver->getPermissions();
}
function getPermissions() {
return $this->value;
}
function from()
{
return 'system';
......
......@@ -29,6 +29,31 @@ class Perms_Resolver_Static implements Perms_Resolver
$this->from = $from;
}
/*
* Extend $known permissions with those from another resolver effectively merging
* the two arrays.
* @param Perms_Resolver $resolver - the other resolver (usually higher level)
*/
function extend( Perms_Resolver $resolver ) {
$known = $resolver->getPermissions();
if( !is_array($known) ) {
return;
}
foreach( $known as $group => $perms ) {
if( !isset($this->known[$group]) ) {
$this->known[$group] = array();
}
$this->known[$group] = array_merge($this->known[$group], $perms);
}
}
/* Retrieve known permissions. Primarily used to extend other resolvers.
* @return array - the known permissions for this resolver
*/
function getPermissions() {
return $this->known;
}
/*
* Check if a specific permission like 'add_object' exist in any of the groups
......
<?php
// (c) Copyright 2002-2016 by authors of the Tiki Wiki CMS Groupware Project
//
// All Rights Reserved. See copyright.txt for details and a complete list of authors.
// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
// $Id: ObjectFactory.php 57971 2016-03-17 20:09:05Z jonnybradley $
/**
* Obtains the parent tracker object permissions for each object of type trackeritem.
* Bulk loading provides loading for multiple objects in a single query.
*/
class Perms_ResolverFactory_TrackerParentFactory implements Perms_ResolverFactory
{
private $known = array();
function getHash( array $context )
{
if ( isset( $context['type'], $context['object'] ) && $context['type'] === 'trackeritem' ) {
return 'object:' . $context['type'] . ':' . $this->cleanObject($context['object']);
} else {
return '';
}
}
function bulk( array $baseContext, $bulkKey, array $values )
{
if ( $bulkKey != 'object' || ! isset($baseContext['type']) || $baseContext['type'] !== 'trackeritem' ) {
return $values;
}
// Limit the amount of hashes preserved to reduce memory consumption
if (count($this->known) > 128) {
$this->known = array();
}
if ( count($values) == 0 ) {
return array();
}
foreach( $values as $v ) {
$hash = $this->getHash(array_merge($baseContext, array( 'object' => $v )));
if ( ! isset($this->known[$hash]) ) {
$this->known[$hash] = array();
}
}
$db = TikiDb::get();
$bindvars = array();
$result = $db->fetchAll(
"SELECT tti.`itemId`, op.`groupName`, op.`permName` FROM `tiki_tracker_items` tti, `users_objectpermissions` op
WHERE op.`objectType` = 'tracker' AND op.`objectId` = md5(concat('tracker', LOWER(tti.`trackerId`))) AND " .
$db->in('tti.itemId', $values, $bindvars),
$bindvars
);
$found = array();
foreach ( $result as $row ) {
$itemId = $row['itemId'];
$group = $row['groupName'];
$perm = $this->sanitize($row['permName']);
$hash = $this->getHash(array_merge($baseContext, array( 'object' => $itemId )));
$found[] = $itemId;
if ( ! isset($this->known[$hash][$group] )) {
$this->known[$hash][$group] = array();
}
$this->known[$hash][$group][] = $perm;
}
return array_values(array_diff($values, $found));
}
function getResolver( array $context )
{
if ( ! isset($context['type'], $context['object'] )) {
return null;
}
$hash = $this->getHash($context);
$this->bulk($context, 'object', array( $context['object'] ));
$perms = $this->known[$hash];
if ( count($perms) == 0 ) {
return null;
} else {
return new Perms_Resolver_Static($perms, 'object');
}
}
private function sanitize( $name )
{
if ( strpos($name, 'tiki_p_') === 0 ) {
return substr($name, strlen('tiki_p_'));
} else {
return $name;
}
}
private function cleanObject($name)
{
return TikiLib::strtolower(trim($name));
}
}
......@@ -87,7 +87,7 @@ class Services_Category_Controller
$type = $input->type->text();
$object = $input->object->text();
$perms = Perms::getCombined($type, $object);
$perms = Perms::get($type, $object);
if (! $perms->modify_object_categories) {
throw new Services_Exception_Denied('Not allowed to modify categories');
}
......@@ -173,7 +173,7 @@ class Services_Category_Controller
if (count($object) == 2) {
list($type, $id) = $object;
$objectPerms = Perms::getCombined($type, $id);
$objectPerms = Perms::get($type, $id);
if ($objectPerms->modify_object_categories) {
$out[] = array('type' => $type, 'id' => $id);
......
......@@ -33,7 +33,7 @@ class Services_Exception_Denied extends Services_Exception
public static function checkObject($perm, $type, $object)
{
$perms = Perms::getCombined($type, $object);
$perms = Perms::get($type, $object);
if (! $perms->$perm) {
throw new self(tr('Permission denied'));
}
......
......@@ -195,7 +195,7 @@ class Services_Language_TranslationController
private function canAttach($type, $object)
{
global $prefs, $user;
$perms = Perms::getCombined($type, $object);
$perms = Perms::get($type, $object);
if ($type == 'wiki page' && $perms->edit) {
return true;
......@@ -221,7 +221,7 @@ class Services_Language_TranslationController
private function canDetach($type, $object)
{
$perms = Perms::getCombined($type, $object);
$perms = Perms::get($type, $object);
return $perms->detach_translation;
}
......
......@@ -77,7 +77,7 @@ function smarty_function_permission_link( $params, $smarty )
$link = 'tiki-objectpermissions.php';
}
$perms = Perms::getCombined($type, $id);
$perms = Perms::get($type, $id);
$source = $perms->getResolver()->from();
return $smarty->fetch('permission_link.tpl', [
......
......@@ -126,7 +126,7 @@ class Perms_BaseTest extends TikiTestCase
$mock->expects($this->once())
->method('getResolver')
->will($this->returnValue(null));
->will($this->returnValue(false));
$perms = new Perms;
$perms->setResolverFactories(array($mock,));
......@@ -136,6 +136,34 @@ class Perms_BaseTest extends TikiTestCase
Perms::get();
}
function testResolverExtension() {
$resolverMock = $this->createMock('Perms_Resolver_Static');
$resolverMock->expects($this->once())
->method('extend')
->with($this->equalTo($resolverMock));
$objectFactory = $this->createMock('Perms_ResolverFactory');
$objectFactory->expects($this->once())
->method('getHash')
->will($this->returnValue('123'));
$objectFactory->expects($this->once())
->method('getResolver')
->will($this->returnValue($resolverMock));
$globalFactory = $this->createMock('Perms_ResolverFactory');
$globalFactory->expects($this->once())
->method('getHash')
->will($this->returnValue('222'));
$globalFactory->expects($this->once())
->method('getResolver')
->will($this->returnValue($resolverMock));
$perms = new Perms;
$perms->setResolverFactories(array($objectFactory, $globalFactory));
Perms::set($perms);
Perms::get();
}
function testBulkLoading()
{
$mockObject = $this->createMock('Perms_ResolverFactory');
......
......@@ -99,6 +99,7 @@ class Perms_BuilderTest extends PHPUnit_Framework_TestCase
array_filter(
array(
new Perms_ResolverFactory_ObjectFactory,
new Perms_ResolverFactory_TrackerParentFactory,
$categories ? new Perms_ResolverFactory_CategoryFactory : null,
new Perms_ResolverFactory_GlobalFactory,
)
......
......@@ -42,5 +42,18 @@ class Perms_Resolver_StaticTest extends TikiTestCase
$this->assertTrue($static->check('edit', array('Anonymous', 'Registered')));
$this->assertEquals(array('Anonymous', 'Registered'), $static->applicableGroups());
}
function testExtension() {
$objectStatic = new Perms_Resolver_Static(array(
'Anonymous' => array('view'),
'Registered' => array('view', 'edit'),
));
$globalStatic = new Perms_Resolver_Static(array(
'Anonymous' => array('view', 'create')
));
$objectStatic->extend($globalStatic);
$this->assertTrue($objectStatic->check('view', array('Anonymous')));
$this->assertTrue($objectStatic->check('create', array('Anonymous')));
}
}
<?php
// (c) Copyright 2002-2016 by authors of the Tiki Wiki CMS Groupware Project
//
// All Rights Reserved. See copyright.txt for details and a complete list of authors.
// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
// $Id: ObjectFactoryTest.php 57963 2016-03-17 20:03:23Z jonnybradley $
/**
* @group unit
*
*/
class Perms_ResolverFactory_TrackerParentFactoryTest extends PHPUnit_Framework_TestCase
{
private $tableData = array();
private $itemIds = array();
function setUp()
{
$db = TikiDb::get();
$result = $db->query('SELECT groupName, permName, objectType, objectId FROM users_objectpermissions');
while ($row = $result->fetchRow()) {
$this->tableData[] = $row;
}
$db->query('DELETE FROM users_objectpermissions');
$db->query('INSERT INTO tiki_tracker_items (trackerId) values (12)');
$this->itemIds[] = $db->lastInsertId();
$db->query('INSERT INTO tiki_tracker_items (trackerId) values (12)');
$this->itemIds[] = $db->lastInsertId();
}
function tearDown()
{
$db = TikiDb::get();
$db->query('DELETE FROM users_objectpermissions');
foreach ($this->tableData as $row) {
$db->query('INSERT INTO users_objectpermissions (groupName, permName, objectType, objectId) VALUES(?,?,?,?)', array_values($row));
}
$db->query('DELETE FROM tiki_tracker_items WHERE itemId in (?, ?)', $this->itemIds);
$this->itemIds = array();
}
function testHash()
{
$factory = new Perms_ResolverFactory_TrackerParentFactory;
$this->assertEquals('object:trackeritem:123', $factory->getHash(array('type' => 'trackeritem', 'object' => '123')));
}
function testHashMissingType()
{
$factory = new Perms_ResolverFactory_TrackerParentFactory;
$this->assertEquals('', $factory->getHash(array('object' => '123')));
}
function testHashWrongType()
{
$factory = new Perms_ResolverFactory_TrackerParentFactory;
$this->assertEquals('', $factory->getHash(array('type' => 'wiki page', 'object' => '123')));
}
function testHashMissingObject()
{
$factory = new Perms_ResolverFactory_TrackerParentFactory;
$this->assertEquals('', $factory->getHash(array('type' => 'trackeritem')));
}
function testObtainPermissions()
{
$data = array(
array('Anonymous', 'tiki_p_view', 'tracker', md5('tracker12')),
array('Anonymous', 'tiki_p_modify_tracker_items', 'tracker', md5('tracker12')),
array('Admins', 'tiki_p_admin', 'tracker', md5('tracker12')),
);
$db = TikiDb::get();
foreach ($data as $row) {
$db->query('INSERT INTO users_objectpermissions (groupName, permName, objectType, objectId) VALUES(?,?,?,?)', array_values($row));
}
$factory = new Perms_ResolverFactory_TrackerParentFactory;
$expect = new Perms_Resolver_Static(
array(
'Admins' => array('admin'),
'Anonymous' => array('modify_tracker_items', 'view'),
),
'object'
);
$this->assertEquals($expect, $factory->getResolver(array('type' => 'trackeritem', 'object' => $this->itemIds[0])));
}
function testObtainPermissionsWhenNoneSpecific()
{
$data = array(
array('Anonymous', 'tiki_p_view', 'tracker', md5('tracker12')),
array('Anonymous', 'tiki_p_modify_tracker_items', 'tracker', md5('tracker12')),
array('Admins', 'tiki_p_admin', 'tracker', md5('tracker12')),
);
$db = TikiDb::get();
foreach ($data as $row) {
$db->query('INSERT INTO users_objectpermissions (groupName, permName, objectType, objectId) VALUES(?,?,?,?)', array_values($row));
}
$factory = new Perms_ResolverFactory_TrackerParentFactory;
$this->assertNull($factory->getResolver(array('type' => 'trackeritem', 'object' => $this->itemIds[1]+1)));
}
function testObtainResolverIncompleteContext()
{
$factory = new Perms_ResolverFactory_TrackerParentFactory;
$this->assertNull($factory->getResolver(array('type' => 'trackeritem')));
$this->assertNull($factory->getResolver(array('object' => $this->itemIds[0])));
}
function testBulkLoading()
{
$data = array(
array('Anonymous', 'tiki_p_view', 'tracker', md5('tracker12')),
array('Anonymous', 'tiki_p_modify_tracker_items', 'tracker', md5('tracker12')),
array('Admins', 'tiki_p_admin', 'tracker', md5('tracker12')),
);
$db = TikiDb::get();
foreach ($data as $row) {
$db->query('INSERT INTO users_objectpermissions (groupName, permName, objectType, objectId) VALUES(?,?,?,?)', array_values($row));
}
$factory = new Perms_ResolverFactory_TrackerParentFactory;
$out = $factory->bulk(array('type' => 'trackeritem'), 'object', array($this->itemIds[0], $this->itemIds[1], $this->itemIds[1]+1));
$this->assertEquals(array($this->itemIds[1]+1), $out);
}
function testBulkLoadingWithoutObject()
{
$factory = new Perms_ResolverFactory_TrackerParentFactory;
$out = $factory->bulk(array('type' => 'trackeritem'), 'objectId', $this->itemIds);
$this->assertEquals($this->itemIds, $out);
}
function testBulkLoadingWithoutType()
{
$factory = new Perms_ResolverFactory_TrackerParentFactory;
$out = $factory->bulk(array(), 'object', $this->itemIds);
$this->assertEquals($this->itemIds, $out);
}
function testBulkLoadingWithWrongType()
{
$factory = new Perms_ResolverFactory_TrackerParentFactory;
$out = $factory->bulk(array('type' => 'wiki page'), 'object', $this->itemIds);
$this->assertEquals($this->itemIds, $out);
}
}
......@@ -164,7 +164,7 @@ class TikiAccessLib extends TikiLib
foreach ($permissions as $permission) {
if (false !== $objectType) {
$applicable = Perms::getCombined($objectType, $objectId);
$applicable = Perms::get($objectType, $objectId);
} else {
$applicable = Perms::get();
}
......@@ -205,7 +205,7 @@ class TikiAccessLib extends TikiLib
foreach ($permissions as $permission) {
if (false !== $objectType) {
$applicable = Perms::getCombined($objectType, $objectId);
$applicable = Perms::get($objectType, $objectId);
} else {
$applicable = Perms::get();
}
......
......@@ -3320,7 +3320,7 @@ class TikiLib extends TikiDb_Bridge
global $user;
if ($type && $object) {
$context = array( 'type' => $type, 'object' => $object );
$accessor = Perms::getCombined($context);
$accessor = Perms::get($context);
} else {
$accessor = Perms::get();
}
......
......@@ -3423,7 +3423,7 @@ class TrackerLib extends TikiLib