Skip to content

Commit

Permalink
More intellegent access control (#470)
Browse files Browse the repository at this point in the history
* Start on #464: enabling and disabling modules and access to their content types.

Requires localgovdrupal/localgov_geo#118

Notes:

 - Should switch to use the user and group roles we define in the
   submodules.
 - Why does Log Out not work? It does outside of tests.

 - Adding tests for global (non-group path) creation of content when
   it's included in the access per domain.

* Add logging out to trait.

Sometimes the repeated / from domain and path matter. Sometimes they
don't. Here they do.

* Switch to use group_sites access providers to supply permissions.

In addition to disabling modules per domain. Remove permissions to
create (group) content on the control domain.

* Test broke in 10.3 because of new logout route option for CSRF.

New route option for redirecting when access is denied to a CSRF protected route
https://www.drupal.org/node/3152693

Not sure what it was doing in 10.2 now to work. But this shouldn't break
that as it checks for the form first.

* Remove unnecessary (wrong type) translation.

* Who knows maybe this works with 10.2 otherwise it's 10.3 only.

* Move shared relationship into optional.

Already exists in optional in localgov_microsites_group_term_ui and
localgov_microsites_blogs. Depending on install order one of these can
be already existing in configuration when the news config is installed.

* Fuller check of permissions paths.

* Add "all" (not quite see comment) admin paths checks.

* See if we can test on github against 10.3

* Test now covers all access by content type options.

* Upgrade configuration for existing sites.

* Add "all" (not quite see comment) admin paths checks.

* See if we can test on github against 10.3

* Test now covers all access by content type options.

* Add blogs tests (and remove permissions not available from module).

* Add blogs tests (and remove permissions not available from module).

* Switch branch used for installing microsites to one that installs 10.3

* It's a composer require string?

* Oops, pay attention to the branch names.

* Script is adding -dev, but it didn't seem to work before. Maybe just wrong string?

* Fixing Github Actions

* Coding standards fixes

* Fix some coding standards.

* Rework attempt for rendering and listing of themes to remove deprecated methods.

* Move to using constructor property promotion, advised by ru and ekes.

* Refactor to use dependency injection.

* Fix naming of variables / properties.

* Fix missing argument for localgov_microsites_group.microsite_content_types_access_policy service.

* Add missing extension.list.theme to create method.

* Fix typos in call to moduleHandler.

---------

Co-authored-by: Stephen Cox <stephen@agile.coop>
Co-authored-by: Finn <finn@finnlewis.com>
Co-authored-by: Finn Lewis <finn@opencode.uk>
  • Loading branch information
4 people committed Jul 23, 2024
1 parent 096dfd0 commit a0c800b
Show file tree
Hide file tree
Showing 15 changed files with 966 additions and 156 deletions.
12 changes: 12 additions & 0 deletions localgov_microsites_group.install
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ function localgov_microsites_group_install($is_syncing) {
// can't, but must update its default config.
$group_sites = \Drupal::configFactory()->getEditable('group_sites.settings');
$group_sites->set('context_provider', '@group_context_domain.group_from_domain_context:group');
$group_sites->set('no_site_access_policy', 'localgov_microsites_group.control_site_access_policy');
$group_sites->set('site_access_policy', 'localgov_microsites_group.microsite_content_types_access_policy');
$group_sites->save();

// Add domain access to exclude other sites results.
Expand Down Expand Up @@ -96,3 +98,13 @@ function localgov_microsites_group_update_9008() {
$group_sites->set('context_provider', '@group_context_domain.group_from_domain_context:group');
$group_sites->save();
}

/**
* Switch to new custom group sites access handlers.
*/
function localgov_microsites_group_update_9009() {
$group_sites = \Drupal::configFactory()->getEditable('group_sites.settings');
$group_sites->set('no_site_access_policy', 'localgov_microsites_group.control_site_access_policy');
$group_sites->set('site_access_policy', 'localgov_microsites_group.microsite_content_types_access_policy');
$group_sites->save();
}
10 changes: 10 additions & 0 deletions localgov_microsites_group.module
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,16 @@ function localgov_microsites_group_toolbar() {
];
}

/**
* Implements hook_toolbar_alter().
*/
function localgov_microsites_group_toolbar_alter(&$items) {
// The control site access provider gives admin mode there.
// Decide to also hide from microsites, so the control site is the admin site
// with our custom permissions.
unset($items['group_sites']);
}

/**
* Implements hook_localgov_microsites_roles_default().
*/
Expand Down
23 changes: 9 additions & 14 deletions localgov_microsites_group.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,16 @@ services:
tags:
- { name: event_subscriber }

localgov_microsites_group.site_admin_mode:
class: Drupal\localgov_microsites_group\EventSubscriber\SiteAdminMode
arguments: ['@domain.negotiator', '@group_sites.admin_mode']
localgov_microsites_group.microsite_content_types_access_policy:
class: 'Drupal\localgov_microsites_group\Access\MicrositeContentTypesAccessPolicy'
arguments: ['@group_sites.admin_mode', '@flexible_permissions.chain_calculator', '@module_handler']
tags:
- { name: event_subscriber }
- { name: group_sites_site_access_policy, priority: 10 }

localgov_microsites_group.control_site_access_policy:
class: 'Drupal\localgov_microsites_group\Access\ControlSiteAccessPolicy'
tags:
- { name: group_sites_no_site_access_policy, priority: 10 }

domain_group_resolver:
class: Drupal\localgov_microsites_group\DomainGroupResolver
Expand All @@ -68,13 +73,3 @@ services:
tags:
- { name: 'context_provider' }
deprecated: 'The "%service_id%" service is deprecated. You should use the group sites context directly instead.'

group.relation_handler.access_control.group_node:
class: 'Drupal\localgov_microsites_group\Plugin\Group\RelationHandler\ContentTypeAccessControl'
arguments: ['@group.relation_handler.access_control', '@localgov_microsites_group.content_type_helper']
shared: false

group.relation_handler.access_control.group_term:
class: 'Drupal\localgov_microsites_group\Plugin\Group\RelationHandler\ContentTypeAccessControl'
arguments: ['@group.relation_handler.access_control', '@localgov_microsites_group.content_type_helper']
shared: false
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,6 @@ function localgov_microsites_blogs_modules_installed($modules) {
*/
function localgov_microsites_blogs_localgov_microsites_roles_default() {
return [
'global' => [
RolesHelper::MICROSITES_CONTROLLER_ROLE => [
'view directory facets',
],
RolesHelper::MICROSITES_EDITOR_ROLE => [
'view directory facets',
],
],
'group' => [
RolesHelper::GROUP_ADMIN_ROLE => [
'access group_term overview',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<?php

namespace Drupal\Tests\localgov_microsites_blogs\Functional;

use Drupal\group\Entity\GroupInterface;
use Drupal\localgov_microsites_group\DomainFromGroupTrait;
use Drupal\node\NodeInterface;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\localgov_microsites_group\Traits\GroupCreationTrait;
use Drupal\Tests\localgov_microsites_group\Traits\InitializeGroupsTrait;
use Drupal\Tests\node\Traits\NodeCreationTrait;

/**
* Tests channel content in a group.
*
* @group localgov_microsites_group
*/
class MicrositeBlogsContentTest extends BrowserTestBase {

use InitializeGroupsTrait;
use NodeCreationTrait;
use GroupCreationTrait, DomainFromGroupTrait {
GroupCreationTrait::getEntityTypeManager insteadof DomainFromGroupTrait;
}

/**
* Will be removed when issue #3204455 on Domain Site Settings gets merged.
*
* See https://www.drupal.org/project/domain_site_settings/issues/3204455.
*
* @var bool
*
* @see \Drupal\Core\Config\Development\ConfigSchemaChecker
* phpcs:disable DrupalPractice.Objects.StrictSchemaDisabled.StrictConfigSchema
*/
protected $strictConfigSchema = FALSE;

/**
* {@inheritdoc}
*/
protected $profile = 'testing';

/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';

/**
* {@inheritdoc}
*/
protected static $modules = [
'localgov_microsites_blogs',
];

/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();

$this->createMicrositeGroups([], 2);
$this->createMicrositeGroupsDomains($this->groups);
$this->domain1 = $this->getDomainFromGroup($this->groups[1]);
$this->domain2 = $this->getDomainFromGroup($this->groups[2]);

// Create some channel content.
$this->blog_channel1 = $this->createBlogChannel($this->groups[1]);
$this->post1 = $this->createBlogPosts($this->blog_channel1, $this->groups[1], 2);
$this->blog_channel2 = $this->createBlogChannel($this->groups[2]);
$this->post2 = $this->createBlogPosts($this->blog_channel2, $this->groups[2], 2);
}

/**
* Test content appears on the correct site.
*/
public function testMicrositeblogContent() {

// Check content appears on the correct sites.
$this->drupalGet($this->domain1->getUrl() . $this->blog_channel1->toUrl()->toString());
$this->assertSession()->pageTextContains($this->post1[0]->label());
$this->assertSession()->pageTextContains($this->post1[1]->label());
$this->assertSession()->pageTextNotContains($this->post2[0]->label());
$this->assertSession()->pageTextNotContains($this->post2[1]->label());

$this->drupalGet($this->domain2->getUrl() . $this->blog_channel2->toUrl()->toString());
$this->assertSession()->pageTextContains($this->post2[0]->label());
$this->assertSession()->pageTextContains($this->post2[1]->label());
$this->assertSession()->pageTextNotContains($this->post1[0]->label());
$this->assertSession()->pageTextNotContains($this->post1[1]->label());
}

/**
* Create blog channel in group.
*
* @param \Drupal\group\Entity\GroupInterface $group
* Group to create blog_channel in.
*
* @return \Drupal\node\NodeInterface
* The blog_channel.
*/
protected function createBlogChannel(GroupInterface $group) {

$channel = $this->createNode([
'type' => 'localgov_blog_channel',
'title' => $this->randomMachineName(12),
'status' => NodeInterface::PUBLISHED,
]);
$channel->save();
$group->addRelationship($channel, 'group_node:localgov_blog_channel');

return $channel;
}

/**
* Create count blog posts in blog channel and group.
*
* @param \Drupal\node\NodeInterface $channel
* Blog channel to create posts in.
* @param \Drupal\group\Entity\GroupInterface $group
* Group to create post in.
* @param int $count
* Number of blog post to create.
*
* @return \Drupal\node\NodeInterface[]
* Array of blog posts.
*/
protected function createBlogPosts(NodeInterface $channel, GroupInterface $group, int $count) {
$posts = [];

for ($i = 0; $i < $count; $i++) {
$post = $this->createNode([
'type' => 'localgov_blog_post',
'title' => $this->randomMachineName(12),
'localgov_blog_channel' => [
'target_id' => $channel->id(),
],
'status' => NodeInterface::PUBLISHED,
]);
$post->save();
$group->addRelationship($post, 'group_node:localgov_blog_post');
$posts[] = $post;
}

return $posts;
}

}
77 changes: 77 additions & 0 deletions src/Access/ControlSiteAccessPolicy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

namespace Drupal\localgov_microsites_group\Access;

use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\flexible_permissions\CalculatedPermissionsItem;
use Drupal\flexible_permissions\RefinableCalculatedPermissionsInterface;
use Drupal\group\PermissionScopeInterface;
use Drupal\group_sites\Access\GroupSitesNoSiteAccessPolicyInterface;

/**
* Access policy for control site.
*/
class ControlSiteAccessPolicy implements GroupSitesNoSiteAccessPolicyInterface {

use StringTranslationTrait;

/**
* {@inheritdoc}
*/
public function getLabel(): string {
return $this->t('Localgov Microsites Control Site');
}

/**
* {@inheritdoc}
*/
public function getDescription(): string {
return $this->t('Prevent most content: nodes, media, etc being created on the control site.');
}

/**
* {@inheritdoc}
*/
public function alterPermissions(AccountInterface $account, string $scope, RefinableCalculatedPermissionsInterface $calculated_permissions) {
// User will probably have permissions for groups.
// Eg. as Outsider with Controller role.
// We might even want to switch off admin and replace with specific
// permissions to prevent doing group content on control.
if ($scope === PermissionScopeInterface::INDIVIDUAL_ID) {
$items = $calculated_permissions->getItemsByScope($scope);
foreach ($items as $item) {
$permissions = $item->getPermissions();
// Permissions to maintain on the control site.
// @todo add control site specific permissions.
$keep = [
'administer group domain site settings',
'administer members',
'edit group',
'invite users to group',
'manage microsite enabled module permissions',
'set localgov microsite theme override',
'view any unpublished group',
'view group',
'view group invitations',
'view latest group version',
'view own unpublished group',
];
$permissions = array_intersect($permissions, $keep);

$control_site_item = new CalculatedPermissionsItem(
$scope,
$item->getIdentifier(),
$permissions,
$item->isAdmin()
);
$calculated_permissions->addItem($control_site_item, TRUE);
}
}
else {
// Neither standard insider nor outside permissions should be required.
$calculated_permissions->removeItemsByScope($scope);
}
}

}
Loading

0 comments on commit a0c800b

Please sign in to comment.