From: Alexander Ebert Date: Thu, 23 Jun 2016 13:30:47 +0000 (+0200) Subject: Added a simplified ACL system X-Git-Tag: 3.0.0_Beta_1~1377 X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=2441de4782919d15a6908fb34ce317a095a8c46c;p=GitHub%2FWoltLab%2FWCF.git Added a simplified ACL system The already existing ACL implementation relies on various options to provide access controls to objects. This is great, but is a bit too much for objects that just want a simple yes/no for access based on groups and users. --- diff --git a/com.woltlab.wcf/objectType.xml b/com.woltlab.wcf/objectType.xml index 645096dc6a..27a33f2368 100644 --- a/com.woltlab.wcf/objectType.xml +++ b/com.woltlab.wcf/objectType.xml @@ -1026,6 +1026,13 @@ + + + com.woltlab.wcf.page + com.woltlab.wcf.acl.simple + + + com.woltlab.wcf.page.controller diff --git a/com.woltlab.wcf/objectTypeDefinition.xml b/com.woltlab.wcf/objectTypeDefinition.xml index 26c32bbeef..792d01008c 100644 --- a/com.woltlab.wcf/objectTypeDefinition.xml +++ b/com.woltlab.wcf/objectTypeDefinition.xml @@ -5,6 +5,10 @@ com.woltlab.wcf.acl + + com.woltlab.wcf.acl.simple + + com.woltlab.wcf.collapsibleContent diff --git a/wcfsetup/install/files/acp/templates/aclSimple.tpl b/wcfsetup/install/files/acp/templates/aclSimple.tpl new file mode 100644 index 0000000000..35a2d2b897 --- /dev/null +++ b/wcfsetup/install/files/acp/templates/aclSimple.tpl @@ -0,0 +1,57 @@ +
+
+
+
+
    +
  1. + + +
  2. +
  3. + + +
  4. +
+
+
+
+ + + + diff --git a/wcfsetup/install/files/acp/templates/pageAdd.tpl b/wcfsetup/install/files/acp/templates/pageAdd.tpl index ab2bec8c3a..5a132e68df 100644 --- a/wcfsetup/install/files/acp/templates/pageAdd.tpl +++ b/wcfsetup/install/files/acp/templates/pageAdd.tpl @@ -363,6 +363,7 @@
+ {include file='aclSimple'}
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Acl/Simple.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Acl/Simple.js new file mode 100644 index 0000000000..6dd00ada85 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Acl/Simple.js @@ -0,0 +1,72 @@ +define(['Language', 'Dom/ChangeListener', 'WoltLab/WCF/Ui/User/Search/Input'], function(Language, DomChangeListener, UiUserSearchInput) { + "use strict"; + + function UiAclSimple() { this.init(); } + UiAclSimple.prototype = { + init: function() { + this._build(); + }, + + _build: function () { + var container = elById('aclInputContainer'); + + elById('aclAllowAll').addEventListener('change', (function() { + elHide(container); + })); + elById('aclAllowAll_no').addEventListener('change', (function() { + elShow(container); + })); + + new UiUserSearchInput(elById('aclSearchInput'), { + callbackSelect: this._select.bind(this), + includeUserGroups: true, + preventSubmit: true + }); + + this._aclListContainer = elById('aclListContainer'); + + this._list = elById('aclAccessList'); + this._list.addEventListener(WCF_CLICK_EVENT, this._removeItem.bind(this)); + + DomChangeListener.trigger(); + }, + + _select: function(listItem) { + var type = elData(listItem, 'type'); + + var html = ''; + html += '' + elData(listItem, 'label') + ''; + html += ''; + html += ''; + + var item = elCreate('li'); + item.innerHTML = html; + + var firstUser = elBySel('.fa-user', this._list); + if (firstUser === null) { + this._list.appendChild(item); + } + else { + this._list.insertBefore(item, firstUser.parentNode); + } + + elShow(this._aclListContainer); + + DomChangeListener.trigger(); + + return false; + }, + + _removeItem: function (event) { + if (event.target.classList.contains('fa-times')) { + elRemove(event.target.parentNode); + + if (this._list.childElementCount === 0) { + elHide(this._aclListContainer); + } + } + } + }; + + return UiAclSimple; +}); diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/User/Search/Input.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/User/Search/Input.js index 867651a63e..bf3550baba 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/Ui/User/Search/Input.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/Ui/User/Search/Input.js @@ -36,6 +36,7 @@ define(['Core', 'WoltLab/WCF/Ui/Search/Input'], function(Core, UiSearchInput) { _createListItem: function(item) { var listItem = UiUserSearchInput._super.prototype._createListItem.call(this, item); + elData(listItem, 'type', item.type); var box = elCreate('div'); box.className = 'box16'; diff --git a/wcfsetup/install/files/lib/acp/form/PageAddForm.class.php b/wcfsetup/install/files/lib/acp/form/PageAddForm.class.php index ca27cde06e..9764109c55 100644 --- a/wcfsetup/install/files/lib/acp/form/PageAddForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/PageAddForm.class.php @@ -10,6 +10,7 @@ use wcf\data\page\PageAction; use wcf\data\page\PageEditor; use wcf\data\page\PageNodeTree; use wcf\form\AbstractForm; +use wcf\system\acl\simple\SimpleAclHandler; use wcf\system\exception\IllegalLinkException; use wcf\system\exception\UserInputException; use wcf\system\html\input\HtmlInputProcessor; @@ -137,6 +138,12 @@ class PageAddForm extends AbstractForm { */ public $boxIDs = []; + /** + * acl values + * @var array + */ + public $aclValues = []; + /** * @inheritDoc */ @@ -201,6 +208,8 @@ class PageAddForm extends AbstractForm { if (isset($_POST['metaDescription']) && is_array($_POST['metaDescription'])) $this->metaDescription = ArrayUtil::trim($_POST['metaDescription']); if (isset($_POST['metaKeywords']) && is_array($_POST['metaKeywords'])) $this->metaKeywords = ArrayUtil::trim($_POST['metaKeywords']); if (isset($_POST['boxIDs']) && is_array($_POST['boxIDs'])) $this->boxIDs = ArrayUtil::toIntegerArray($_POST['boxIDs']); + + if (isset($_POST['aclValues']) && is_array($_POST['aclValues'])) $this->aclValues = $_POST['aclValues']; } /** @@ -377,7 +386,8 @@ class PageAddForm extends AbstractForm { $parseHTML = function($content) { if ($this->pageType == 'text') { $htmlInputProcessor = new HtmlInputProcessor(); - $content = $htmlInputProcessor->process($content); + $htmlInputProcessor->process($content); + $content = $htmlInputProcessor->getHtml(); } return $content; @@ -425,13 +435,16 @@ class PageAddForm extends AbstractForm { // set generic page identifier $pageEditor = new PageEditor($page); $pageEditor->update([ - 'identifier' => 'com.woltlab.wcf.generic'.$pageEditor->pageID + 'identifier' => 'com.woltlab.wcf.generic'.$page->pageID ]); if ($this->isLandingPage) { $page->setAsLandingPage(); } + // save acl + SimpleAclHandler::getInstance()->setValues('com.woltlab.wcf.page', $page->pageID, $_POST); + // call saved event $this->saved(); @@ -478,7 +491,8 @@ class PageAddForm extends AbstractForm { 'availableApplications' => $this->availableApplications, 'availableLanguages' => $this->availableLanguages, 'availableBoxes' => $this->availableBoxes, - 'pageNodeList' => (new PageNodeTree())->getNodeList() + 'pageNodeList' => (new PageNodeTree())->getNodeList(), + 'aclValues' => (empty($_POST) ? $this->aclValues : SimpleAclHandler::getInstance()->getOutputValues($this->aclValues)) ]); } } diff --git a/wcfsetup/install/files/lib/acp/form/PageEditForm.class.php b/wcfsetup/install/files/lib/acp/form/PageEditForm.class.php index f6f5497a63..94511417af 100644 --- a/wcfsetup/install/files/lib/acp/form/PageEditForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/PageEditForm.class.php @@ -3,6 +3,7 @@ namespace wcf\acp\form; use wcf\data\page\Page; use wcf\data\page\PageAction; use wcf\form\AbstractForm; +use wcf\system\acl\simple\SimpleAclHandler; use wcf\system\exception\IllegalLinkException; use wcf\system\language\LanguageFactory; use wcf\system\WCF; @@ -176,6 +177,9 @@ class PageEditForm extends PageAddForm { $this->page->setAsLandingPage(); } + // save acl + SimpleAclHandler::getInstance()->setValues('com.woltlab.wcf.page', $this->page->pageID, $this->aclValues); + // call saved event $this->saved(); @@ -219,6 +223,8 @@ class PageEditForm extends PageAddForm { } } } + + $this->aclValues = SimpleAclHandler::getInstance()->getValues('com.woltlab.wcf.page', $this->page->pageID); } } diff --git a/wcfsetup/install/files/lib/data/user/group/UserGroup.class.php b/wcfsetup/install/files/lib/data/user/group/UserGroup.class.php index 200585f324..5dd0d5dfba 100644 --- a/wcfsetup/install/files/lib/data/user/group/UserGroup.class.php +++ b/wcfsetup/install/files/lib/data/user/group/UserGroup.class.php @@ -138,7 +138,7 @@ class UserGroup extends DatabaseObject implements ITitledObject { * exists. * * @param integer $groupID - * @return \wcf\data\user\group\UserGroup + * @return UserGroup|null */ public static function getGroupByID($groupID) { self::getCache(); @@ -150,11 +150,27 @@ class UserGroup extends DatabaseObject implements ITitledObject { return null; } + /** + * Returns a list of groups by group id. + * + * @param integer[] $groupIDs list of group ids + * @return UserGroup[] + */ + public static function getGroupsByIDs(array $groupIDs) { + $groups = []; + foreach ($groupIDs as $groupID) { + $group = self::getGroupByID($groupID); + if ($group !== null) $groups[$groupID] = $group; + } + + return $groups; + } + /** * Returns true if the given user is member of the group. If no user is * given, the active user is used. * - * @param \wcf\data\user\User $user + * @param User $user user object or current user if null * @return boolean */ public function isMember(User $user = null) { diff --git a/wcfsetup/install/files/lib/system/acl/simple/SimpleAclHandler.class.php b/wcfsetup/install/files/lib/system/acl/simple/SimpleAclHandler.class.php new file mode 100644 index 0000000000..16ef7285e2 --- /dev/null +++ b/wcfsetup/install/files/lib/system/acl/simple/SimpleAclHandler.class.php @@ -0,0 +1,218 @@ + + * @package WoltLabSuite\Core\System\Acl\Simple + */ +class SimpleAclHandler extends SingletonFactory { + /** + * list of registered object types + * @var ObjectType[] + */ + protected $objectTypes = []; + + /** + * @inheritDoc + */ + protected function init() { + $this->objectTypes = ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.acl.simple'); + } + + /** + * Returns the object type id by object type. + * + * @param string $objectType object type name + * @return integer object type id + * @throws SystemException + */ + public function getObjectTypeID($objectType) { + if (!isset($this->objectTypes[$objectType])) { + throw new SystemException("Unknown object type '" . $objectType . "'"); + } + + return $this->objectTypes[$objectType]->objectTypeID; + } + + /** + * Returns the user and group values for provided object type and object id. + * + * @param string $objectType object type name + * @param integer $objectID object id + * @return array array containing the keys `allowAll`, `user` and `group` + */ + public function getValues($objectType, $objectID) { + $objectTypeID = $this->getObjectTypeID($objectType); + + $data = [ + 'allowAll' => true, + 'user' => [], + 'group' => [] + ]; + + $sql = "SELECT userID + FROM wcf".WCF_N."_acl_simple_to_user + WHERE objectTypeID = ? + AND objectID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ + $objectTypeID, + $objectID + ]); + $userIDs = []; + while ($row = $statement->fetchArray()) { + $userIDs[] = $row['userID']; + } + + $sql = "SELECT groupID + FROM wcf".WCF_N."_acl_simple_to_group + WHERE objectTypeID = ? + AND objectID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ + $objectTypeID, + $objectID + ]); + $groupIDs = []; + while ($row = $statement->fetchArray()) { + $groupIDs[] = $row['groupID']; + } + + if (!empty($userIDs) || !empty($groupIDs)) { + $data['allowAll'] = false; + + if (!empty($userIDs)) { + $data['user'] = UserRuntimeCache::getInstance()->getObjects($userIDs); + } + + if (!empty($groupIDs)) { + $data['group'] = UserGroup::getGroupsByIDs($groupIDs); + } + } + + return $data; + } + + /** + * Sets the user and group values for provided object type and object id. + * + * @param string $objectType object type name + * @param integer $objectID object id + * @param array $values list of user and group ids + */ + public function setValues($objectType, $objectID, array $values) { + $objectTypeID = $this->getObjectTypeID($objectType); + + // validate data of `$values` + if (empty($values['user']) && empty($values['group']) && !isset($values['allowAll'])) { + throw new SystemException("Missing ACL configuration values."); + } + + // users + $sql = "DELETE FROM wcf".WCF_N."_acl_simple_to_user + WHERE objectTypeID = ? + AND objectID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ + $objectTypeID, + $objectID + ]); + + if ($values['allowAll'] == 0 && !empty($values['user'])) { + $values['user'] = ArrayUtil::toIntegerArray($values['user']); + if (!empty($values['user'])) { + $sql = "INSERT INTO wcf" . WCF_N . "_acl_simple_to_user + (objectTypeID, objectID, userID) + VALUES (?, ?, ?)"; + $statement = WCF::getDB()->prepareStatement($sql); + + WCF::getDB()->beginTransaction(); + foreach ($values['user'] as $userID) { + $statement->execute([ + $objectTypeID, + $objectID, + $userID + ]); + } + WCF::getDB()->commitTransaction(); + } + } + + // groups + $sql = "DELETE FROM wcf".WCF_N."_acl_simple_to_group + WHERE objectTypeID = ? + AND objectID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ + $objectTypeID, + $objectID + ]); + + if ($values['allowAll'] == 0 && !empty($values['group'])) { + $values['group'] = ArrayUtil::toIntegerArray($values['group']); + if (!empty($values['group'])) { + $sql = "INSERT INTO wcf" . WCF_N . "_acl_simple_to_group + (objectTypeID, objectID, groupID) + VALUES (?, ?, ?)"; + $statement = WCF::getDB()->prepareStatement($sql); + + WCF::getDB()->beginTransaction(); + foreach ($values['group'] as $groupID) { + $statement->execute([ + $objectTypeID, + $objectID, + $groupID + ]); + } + WCF::getDB()->commitTransaction(); + } + } + + // reset cache for object type + SimpleAclResolver::getInstance()->resetCache($objectType); + } + + /** + * Processes the provided values and returns the final + * values for template assignment. + * + * @param array $rawValues acl values as provided (by the user input) + * @return array final values for template assignment + */ + public function getOutputValues(array $rawValues) { + $aclValues = [ + 'allowAll' => true, + 'user' => [], + 'group' => [] + ]; + + if ($rawValues['allowAll'] == 0) { + if (!empty($rawValues['user'])) { + $aclValues['user'] = UserRuntimeCache::getInstance()->getObjects($rawValues['user']); + } + + if (!empty($rawValues['group'])) { + $aclValues['group'] = UserGroup::getGroupsByIDs($rawValues['group']); + } + + if (!empty($aclValues['user']) || !empty($aclValues['group'])) { + $aclValues['allowAll'] = false; + } + } + + return $aclValues; + } +} diff --git a/wcfsetup/install/files/lib/system/acl/simple/SimpleAclResolver.class.php b/wcfsetup/install/files/lib/system/acl/simple/SimpleAclResolver.class.php new file mode 100644 index 0000000000..5e763393cb --- /dev/null +++ b/wcfsetup/install/files/lib/system/acl/simple/SimpleAclResolver.class.php @@ -0,0 +1,81 @@ + + * @package WoltLabSuite\Core\System\Acl\Simple + */ +class SimpleAclResolver extends SingletonFactory { + /** + * cached permissions per object type + * @var array + */ + protected $cache = []; + + /** + * Returns true if there are no ACL settings, the user is allowed or + * one of its group is allowed. + * + * @param string $objectType object type name + * @param integer $objectID object id + * @param User|null $user user object, if `null` uses current user + * @return boolean false if user is not allowed + */ + public function canAccess($objectType, $objectID, User $user = null) { + if ($user === null) $user = WCF::getUser(); + + $this->loadCache($objectType); + + // allow all + if (!isset($this->cache[$objectType][$objectID])) { + return true; + } + + if ($user->userID) { + // user is explicitly allowed + if (in_array($user->userID, $this->cache[$objectType][$objectID]['user'])) { + return true; + } + } + + // check for user groups + $groupIDs = $user->getGroupIDs(); + foreach ($groupIDs as $groupID) { + // group is explicitly allowed + if (in_array($groupID, $this->cache[$objectType][$objectID]['group'])) { + return true; + } + } + + return false; + } + + /** + * Resets the cache for provided object type. + * + * @param string $objectType object type name + */ + public function resetCache($objectType) { + SimpleAclCacheBuilder::getInstance()->reset(['objectType' => $objectType]); + } + + /** + * Attempts to load the cache for provided object type. + * + * @param string $objectType object type name + */ + protected function loadCache($objectType) { + if (!isset($this->cache[$objectType])) { + $this->cache[$objectType] = SimpleAclCacheBuilder::getInstance()->getData(['objectType' => $objectType]); + } + } +} diff --git a/wcfsetup/install/files/lib/system/cache/builder/SimpleAclCacheBuilder.class.php b/wcfsetup/install/files/lib/system/cache/builder/SimpleAclCacheBuilder.class.php new file mode 100644 index 0000000000..bfc501d9d1 --- /dev/null +++ b/wcfsetup/install/files/lib/system/cache/builder/SimpleAclCacheBuilder.class.php @@ -0,0 +1,61 @@ + + * @package WoltLabSuite\Core\System\Cache\Builder + */ +class SimpleAclCacheBuilder extends AbstractCacheBuilder { + /** + * @inheritDoc + */ + public function rebuild(array $parameters) { + $data = []; + + $objectTypeID = SimpleAclHandler::getInstance()->getObjectTypeID($parameters['objectType']); + + $sql = "SELECT objectID, userID + FROM wcf".WCF_N."_acl_simple_to_user + WHERE objectTypeID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([$objectTypeID]); + while ($row = $statement->fetchArray()) { + $objectID = $row['objectID']; + + if (!isset($data[$objectID])) { + $data[$objectID] = [ + 'group' => [], + 'user' => [] + ]; + } + + $data[$objectID]['user'][] = $row['userID']; + } + + $sql = "SELECT objectID, groupID + FROM wcf".WCF_N."_acl_simple_to_group + WHERE objectTypeID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([$objectTypeID]); + while ($row = $statement->fetchArray()) { + $objectID = $row['objectID']; + + if (!isset($data[$objectID])) { + $data[$objectID] = [ + 'group' => [], + 'user' => [] + ]; + } + + $data[$objectID]['group'][] = $row['groupID']; + } + + return $data; + } +} diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index 1c7767b88e..935e91d701 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -1,6 +1,11 @@ + + + + + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index 8f6d0e6649..6413cc1d89 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -1,6 +1,11 @@ + + + + + diff --git a/wcfsetup/setup/db/install.sql b/wcfsetup/setup/db/install.sql index 7927e2168e..4feb29cbc9 100644 --- a/wcfsetup/setup/db/install.sql +++ b/wcfsetup/setup/db/install.sql @@ -36,6 +36,22 @@ CREATE TABLE wcf1_acl_option_to_group ( UNIQUE KEY groupID (groupID, objectID, optionID) ); +DROP TABLE IF EXISTS wcf1_acl_simple_to_user; +CREATE TABLE wcf1_acl_simple_to_user ( + objectTypeID INT(10) NOT NULL, + objectID INT(10) NOT NULL, + userID INT(10) NOT NULL, + UNIQUE KEY userKey (objectTypeID, objectID, userID) +); + +DROP TABLE IF EXISTS wcf1_acl_simple_to_group; +CREATE TABLE wcf1_acl_simple_to_group ( + objectTypeID INT(10) NOT NULL, + objectID INT(10) NOT NULL, + groupID INT(10) NOT NULL, + UNIQUE KEY groupKey (objectTypeID, objectID, groupID) +); + DROP TABLE IF EXISTS wcf1_acp_menu_item; CREATE TABLE wcf1_acp_menu_item ( menuItemID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY, @@ -1632,6 +1648,12 @@ ALTER TABLE wcf1_acl_option_to_user ADD FOREIGN KEY (userID) REFERENCES wcf1_use ALTER TABLE wcf1_acl_option_to_group ADD FOREIGN KEY (optionID) REFERENCES wcf1_acl_option (optionID) ON DELETE CASCADE; ALTER TABLE wcf1_acl_option_to_group ADD FOREIGN KEY (groupID) REFERENCES wcf1_user_group (groupID) ON DELETE CASCADE; +ALTER TABLE wcf1_acl_simple_to_user ADD FOREIGN KEY (objectTypeID) REFERENCES wcf1_object_type (objectTypeID) ON DELETE CASCADE; +ALTER TABLE wcf1_acl_simple_to_user ADD FOREIGN KEY (userID) REFERENCES wcf1_user (userID) ON DELETE CASCADE; + +ALTER TABLE wcf1_acl_simple_to_group ADD FOREIGN KEY (objectTypeID) REFERENCES wcf1_object_type (objectTypeID) ON DELETE CASCADE; +ALTER TABLE wcf1_acl_simple_to_group ADD FOREIGN KEY (groupID) REFERENCES wcf1_user_group (groupID) ON DELETE CASCADE; + ALTER TABLE wcf1_acp_menu_item ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE; ALTER TABLE wcf1_acp_search_provider ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE;