From e77c0d4768e995ea26016087e6912ebd5b752875 Mon Sep 17 00:00:00 2001 From: Andrew Paxley Date: Fri, 20 Oct 2023 15:12:51 +1300 Subject: [PATCH] ENH add optional SiteConfig root level permissions --- composer.json | 1 + src/File.php | 7 + src/RootLevelAccessSiteConfigExtension.php | 161 +++++++++++++++++++++ src/SiteConfigFilePermissions.php | 131 +++++++++++++++++ 4 files changed, 300 insertions(+) create mode 100644 src/RootLevelAccessSiteConfigExtension.php create mode 100644 src/SiteConfigFilePermissions.php diff --git a/composer.json b/composer.json index dd3b7a97..2bb78a42 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ "require": { "php": "^8.1", "silverstripe/framework": "^5.1", + "silverstripe/siteconfig": "^5.1", "silverstripe/vendor-plugin": "^2", "symfony/filesystem": "^6.1", "intervention/image": "^2.7.2", diff --git a/src/File.php b/src/File.php index 50e01d98..3f2c6220 100644 --- a/src/File.php +++ b/src/File.php @@ -105,6 +105,7 @@ class File extends DataObject implements AssetContainer, Thumbnail, CMSPreviewab * Permission for edit all files */ const EDIT_ALL = 'FILE_EDIT_ALL'; + const GRANT_ACCESS = 'FILE_GRANT_ACCESS'; private static $default_sort = "\"Name\""; @@ -1427,6 +1428,12 @@ public function providePermissions() 'category' => _t('SilverStripe\\Security\\Permission.CONTENT_CATEGORY', 'Content permissions'), 'sort' => -100, 'help' => _t(__CLASS__.'.EDIT_ALL_HELP', 'Edit any file on the site, even if restricted') + ], + self::GRANT_ACCESS => [ + 'name' => _t(__CLASS__.'.GRANT_ACCESS_DESCRIPTION', 'Manage access rights for files'), + 'category' => _t('SilverStripe\\Security\\Permission.CONTENT_CATEGORY', 'Content permissions'), + 'sort' => -100, + 'help' => _t(__CLASS__.'.GRANT_ACCESS_HELP', 'Allow setting of file-specific access restrictions in the "Files" section') ] ]; } diff --git a/src/RootLevelAccessSiteConfigExtension.php b/src/RootLevelAccessSiteConfigExtension.php new file mode 100644 index 00000000..62f26b68 --- /dev/null +++ b/src/RootLevelAccessSiteConfigExtension.php @@ -0,0 +1,161 @@ + "Enum('Anyone, LoggedInUsers, OnlyTheseUsers, OnlyTheseMembers', 'Anyone')", + "RootAssetsCanEditType" => "Enum('LoggedInUsers, OnlyTheseUsers, OnlyTheseMembers', 'OnlyTheseUsers')", + ]; + + private static $many_many = [ + "RootAssetsViewerGroups" => Group::class, + "RootAssetsEditorGroups" => Group::class, + "RootAssetsViewerMembers" => Member::class, + "RootAssetsEditorMembers" => Member::class, + ]; + + private static $defaults = [ + "RootAssetsCanViewType" => "Anyone", + "RootAssetsCanEditType" => "OnlyTheseUsers", + ]; + + public function requireDefaultRecords() + { + parent::requireDefaultRecords(); + if ($this->getOwner()->RootAssetsEditorGroups()->count() < 1) { + $groups = Permission::get_groups_by_permission(File::EDIT_ALL); + foreach ($groups as $group) { + $this->getOwner()->RootAssetsEditorGroups()->add($group); + } + } + } + + public function updateCMSFields(FieldList $fields) + { + // Lots of logic borrowed from SiteConfig::getCMSFields() + $mapFn = function ($groups = []) { + $map = []; + foreach ($groups as $group) { + // Listboxfield values are escaped, use ASCII char instead of » + $map[$group->ID] = $group->getBreadcrumbs(' > '); + } + asort($map); + return $map; + }; + $groupsMap = $mapFn(Group::get()); + $membersMap = Member::get()->map('ID', 'Name'); + $editAllGroupsMap = $mapFn(Permission::get_groups_by_permission([File::EDIT_ALL, 'ADMIN'])); + + $fields->addFieldsToTab( + 'Root.Access', + [ + $viewersOptionsField = OptionsetField::create( + "RootAssetsCanViewType", + _t(self::class . '.ROOTASSETSVIEWHEADER', "Who can view files on this site?") + ), + $viewerGroupsField = ListboxField::create( + "RootAssetsViewerGroups", + _t('SilverStripe\\CMS\\Model\\SiteTree.ROOTASSETSVIEWERGROUPS', "File Viewer Groups") + ) + ->setSource($groupsMap) + ->setAttribute( + 'data-placeholder', + _t('SilverStripe\\CMS\\Model\\SiteTree.GroupPlaceholder', 'Click to select group') + ), + $viewerMembersField = ListboxField::create( + "RootAssetsViewerMembers", + _t(__CLASS__.'.ROOTASSETSVIEWERMEMBERS', "Viewer Users"), + $membersMap, + ), + $editorsOptionsField = OptionsetField::create( + "RootAssetsCanEditType", + _t(self::class . '.ROOTASSETSEDITHEADER', "Who can edit files on this site?") + ), + $editorGroupsField = ListboxField::create( + "RootAssetsEditorGroups", + _t('SilverStripe\\CMS\\Model\\SiteTree.EDITORGROUPS', "File Editor Groups") + ) + ->setSource($groupsMap) + ->setAttribute( + 'data-placeholder', + _t('SilverStripe\\CMS\\Model\\SiteTree.GroupPlaceholder', 'Click to select group') + ), + $editorMembersField = ListboxField::create( + "RootAssetsEditorMembers", + _t(__CLASS__.'.ROOTASSETSEDITORMEMBERS', "Editor Users"), + $membersMap + ) + ] + ); + + $viewersOptionsSource = []; + $viewersOptionsSource[InheritedPermissions::ANYONE] = _t('SilverStripe\\CMS\\Model\\SiteTree.ACCESSANYONE', "Anyone"); + $viewersOptionsSource[InheritedPermissions::LOGGED_IN_USERS] = _t( + 'SilverStripe\\CMS\\Model\\SiteTree.ACCESSLOGGEDIN', + "Logged-in users" + ); + $viewersOptionsSource[InheritedPermissions::ONLY_THESE_USERS] = _t( + 'SilverStripe\\CMS\\Model\\SiteTree.ACCESSONLYTHESE', + "Only these groups (choose from list)" + ); + $viewersOptionsSource[InheritedPermissions::ONLY_THESE_MEMBERS] = _t( + 'SilverStripe\\CMS\\Model\\SiteTree.ACCESSONLYMEMBERS', + "Only these users (choose from list)" + ); + $viewersOptionsField->setSource($viewersOptionsSource); + $editorsOptionsSource = $viewersOptionsSource; + unset($editorsOptionsSource[InheritedPermissions::ANYONE]); + $editorsOptionsField->setSource($editorsOptionsSource); + + + if ($editAllGroupsMap) { + $viewerGroupsField->setDescription(_t( + 'SilverStripe\\CMS\\Model\\SiteTree.ROOT_ASSETS_VIEWER_GROUPS_FIELD_DESC', + 'Groups with global view permissions: {groupList}', + ['groupList' => implode(', ', array_values($editAllGroupsMap))] + )); + $editorGroupsField->setDescription(_t( + 'SilverStripe\\CMS\\Model\\SiteTree.ROOT_ASSETS_EDITOR_GROUPS_FIELD_DESC', + 'Groups with global edit permissions: {groupList}', + ['groupList' => implode(', ', array_values($editAllGroupsMap))] + )); + } + + if (!Permission::check(File::GRANT_ACCESS)) { + $fields->makeFieldReadonly($viewersOptionsField); + if ($this->getOwner()->RootAssetsCanViewType === InheritedPermissions::ONLY_THESE_USERS) { + $fields->makeFieldReadonly($viewerGroupsField); + $fields->removeByName('RootAssetsViewerMembers'); + } elseif ($this->getOwner()->RootAssetsCanViewType === InheritedPermissions::ONLY_THESE_MEMBERS) { + $fields->makeFieldReadonly($viewerMembersField); + $fields->removeByName('RootAssetsViewerGroups'); + } else { + $fields->removeByName('RootAssetsViewerGroups'); + $fields->removeByName('RootAssetsViewerMembers'); + } + + $fields->makeFieldReadonly($editorsOptionsField); + if ($this->getOwner()->RootAssetsCanEditType === InheritedPermissions::ONLY_THESE_USERS) { + $fields->makeFieldReadonly($editorGroupsField); + $fields->removeByName('RootAssetsEditorMembers'); + } elseif ($this->getOwner()->RootAssetsCanEditType === InheritedPermissions::ONLY_THESE_MEMBERS) { + $fields->makeFieldReadonly($editorMembersField); + $fields->removeByName('RootAssetsEditorGroups'); + } else { + $fields->removeByName('RootAssetsEditorGroups'); + $fields->removeByName('RootAssetsEditorMembers'); + } + } + } +} \ No newline at end of file diff --git a/src/SiteConfigFilePermissions.php b/src/SiteConfigFilePermissions.php new file mode 100644 index 00000000..7a480f04 --- /dev/null +++ b/src/SiteConfigFilePermissions.php @@ -0,0 +1,131 @@ +RootAssetsCanEditType; + + // Any logged in user can edit root files and folders + if ($canEditType === InheritedPermissions::LOGGED_IN_USERS) { + return $member !== null; + } + + // Specific user groups can edit root files and folders + if ($canEditType === InheritedPermissions::ONLY_THESE_USERS) { + if (!$member) { + return false; + } + return $member->inGroups($siteConfig->RootAssetsEditorGroups()); + } + + // Specific users can edit root files and folders + if ($canEditType === InheritedPermissions::ONLY_THESE_MEMBERS) { + if (!$member) { + return false; + } + return $siteConfig->RootAssetsEditorMembers()->filter('ID', $member->ID)->count() > 0; + } + + // Secure by default + return false; + } + + /** + * Can root be viewed? + * + * @param Member $member + * @return bool + */ + public function canView($member = null) + { + if (!$member) { + $member = Security::getCurrentUser(); + } + + if (Permission::checkMember($member, 'ADMIN') || Permission::check($member, File::EDIT_ALL)) { + return true; + } + + $siteConfig = SiteConfig::current_site_config(); + $canViewType = $siteConfig->RootAssetsCanViewType; + + if ($canViewType === InheritedPermissions::ANYONE) { + return true; + } + + // Any logged in user can view root files and folders + if ($canViewType === InheritedPermissions::LOGGED_IN_USERS) { + return $member !== null; + } + + // Specific user groups can view root files and folders + if ($canViewType === InheritedPermissions::ONLY_THESE_USERS) { + if (!$member) { + return false; + } + return $member->inGroups($siteConfig->RootAssetsViewerGroups()); + } + + // Specific users can view root files and folders + if ($canViewType === InheritedPermissions::ONLY_THESE_MEMBERS) { + if (!$member) { + return false; + } + return $siteConfig->RootAssetsViewerMembers()->filter('ID', $member->ID)->count() > 0; + } + + // Secure by default + return false; + } + + /** + * Can root be deleted? + * + * @param Member $member + * @return bool + */ + public function canDelete(Member $member = null) + { + return $this->canEdit($member); + } + + /** + * Can root objects be created? + * + * @param Member $member + * @return bool + */ + public function canCreate(Member $member = null) + { + return $this->canEdit($member); + } +}