Skip to content
Snippets Groups Projects
Commit cd58a168 authored by Kristiaan Van den Eynde's avatar Kristiaan Van den Eynde Committed by Kristiaan Van den Eynde
Browse files

Issue #3352235 by kristiaanvandeneynde, graber, vensires: An already assigned...

Issue #3352235 by kristiaanvandeneynde, graber, vensires: An already assigned individual role can be made out/insider, leading to a crash in the permission calculation
parent 15728f3b
No related branches found
Tags 8.x-1.0
2 merge requests!193Issue #3304728 by kristiaanvandeneynde: Add member page missing due to...,!111Issue #3397021 by adamfranco: Add entity_mappings for phpstan-drupal
Pipeline #307083 passed
......@@ -65,7 +65,8 @@ use Drupal\user\RoleInterface;
* "group_type"
* },
* constraints = {
* "GroupRoleScope" = {}
* "GroupRoleScope" = {},
* "GroupRoleAssigned" = {}
* }
* )
*/
......
......@@ -6,6 +6,7 @@ use Drupal\Component\Uuid\UuidInterface;
use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\Entity\ConfigEntityStorage;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityMalformedException;
use Drupal\Core\Entity\EntityTypeInterface;
......@@ -32,42 +33,17 @@ class GroupRoleStorage extends ConfigEntityStorage implements GroupRoleStorageIn
*/
protected $userGroupRoleIds = [];
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The group membership loader.
*
* @var \Drupal\group\GroupMembershipLoaderInterface
*/
protected $groupMembershipLoader;
/**
* Constructs a GroupRoleStorage object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\group\GroupMembershipLoaderInterface $group_membership_loader
* The group membership loader.
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\Component\Uuid\UuidInterface $uuid_service
* The UUID service.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface $memory_cache
* The memory cache backend.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, GroupMembershipLoaderInterface $group_membership_loader, EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, UuidInterface $uuid_service, LanguageManagerInterface $language_manager, MemoryCacheInterface $memory_cache) {
public function __construct(
protected EntityTypeManagerInterface $entityTypeManager,
protected GroupMembershipLoaderInterface $groupMembershipLoader,
EntityTypeInterface $entity_type,
ConfigFactoryInterface $config_factory,
UuidInterface $uuid_service,
LanguageManagerInterface $language_manager,
MemoryCacheInterface $memory_cache,
protected Connection $database,
) {
parent::__construct($entity_type, $config_factory, $uuid_service, $language_manager, $memory_cache);
$this->entityTypeManager = $entity_type_manager;
$this->groupMembershipLoader = $group_membership_loader;
}
/**
......@@ -81,7 +57,8 @@ class GroupRoleStorage extends ConfigEntityStorage implements GroupRoleStorageIn
$container->get('config.factory'),
$container->get('uuid'),
$container->get('language_manager'),
$container->get('entity.memory_cache')
$container->get('entity.memory_cache'),
$container->get('database')
);
}
......@@ -142,4 +119,35 @@ class GroupRoleStorage extends ConfigEntityStorage implements GroupRoleStorageIn
}
}
/**
* {@inheritdoc}
*/
public function hasMembershipReferences(array $group_role_ids): bool {
return (bool) $this->database->select('group_content__group_roles', 'gr')
->condition('gr.group_roles_target_id', $group_role_ids, 'IN')
->countQuery()
->execute()
->fetchField();
}
/**
* {@inheritdoc}
*/
public function deleteMembershipReferences(array $group_role_ids): void {
$this->database->delete('group_content__group_roles')
->condition('group_roles_target_id', $group_role_ids, 'IN')
->execute();
$this->userGroupRoleIds = [];
$this->entityTypeManager->getStorage('group_content')->resetCache();
}
/**
* {@inheritdoc}
*/
public function resetCache(?array $ids = NULL) {
parent::resetCache($ids);
$this->userGroupRoleIds = [];
}
}
......@@ -38,4 +38,24 @@ interface GroupRoleStorageInterface extends ConfigEntityStorageInterface {
*/
public function resetUserGroupRoleCache(AccountInterface $account, ?GroupInterface $group = NULL);
/**
* Checks if group roles have membership references.
*
* @param string[] $group_role_ids
* The list of group role IDs being checked.
*
* @return bool
* Whether any of the group roles are being referenced by a membership.
*/
public function hasMembershipReferences(array $group_role_ids): bool;
/**
* Deletes group role membership references.
*
* @param string[] $group_role_ids
* The list of group role IDs being deleted. The storage should
* remove member references to this role.
*/
public function deleteMembershipReferences(array $group_role_ids): void;
}
......@@ -55,7 +55,7 @@ class GroupRelationshipCardinalityValidator extends ConstraintValidator implemen
/**
* {@inheritdoc}
*/
public function validate($group_relationship, Constraint $constraint) {
public function validate($group_relationship, Constraint $constraint): void {
assert($group_relationship instanceof GroupRelationshipInterface);
assert($constraint instanceof GroupRelationshipCardinality);
......
<?php
namespace Drupal\group\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
/**
* Checks if a group role is assigned to a membership even though it can't be.
*
* @Constraint(
* id = "GroupRoleAssigned",
* label = @Translation("Group role assignment check", context = "Validation"),
* type = "entity:group_role"
* )
*/
class GroupRoleAssigned extends Constraint {
/**
* When a group role is already assigned and put in a synchronized scope.
*
* @var string
*/
public $alreadyAssignedMessage = 'Cannot save this group role in the %scope scope as it has already been assigned to individual members.';
}
<?php
namespace Drupal\group\Plugin\Validation\Constraint;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\group\Entity\GroupRoleInterface;
use Drupal\group\Entity\Storage\GroupRoleStorageInterface;
use Drupal\group\PermissionScopeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Checks whether a group role is assigned when it shouldn't be.
*/
class GroupRoleAssignedValidator extends ConstraintValidator implements ContainerInjectionInterface {
public function __construct(protected EntityTypeManagerInterface $entityTypeManager) {}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('entity_type.manager'));
}
/**
* {@inheritdoc}
*/
public function validate(mixed $value, Constraint $constraint): void {
$group_role = $value;
assert($group_role instanceof GroupRoleInterface);
assert($constraint instanceof GroupRoleAssigned);
$scope = $group_role->getScope();
if ($scope !== PermissionScopeInterface::INDIVIDUAL_ID) {
$role_storage = $this->entityTypeManager->getStorage('group_role');
assert($role_storage instanceof GroupRoleStorageInterface);
if ($group_role->id() && $role_storage->hasMembershipReferences([$group_role->id()])) {
$this->context->buildViolation($constraint->alreadyAssignedMessage)
->setParameter('%scope', $scope)
->atPath('scope')
->addViolation();
}
}
}
}
......@@ -16,14 +16,14 @@ use Symfony\Component\Validator\Constraint;
class GroupRoleScope extends Constraint {
/**
* The message to show when an entity has reached the group cardinality.
* When someone attempts to create an anonymous insider group role.
*
* @var string
*/
public $anonymousMemberMessage = 'Anonymous users cannot be members so you may not create an insider role for the %role global role.';
/**
* The message to show when an entity has reached the entity cardinality.
* When a duplicate group role - global role pair is detected.
*
* @var string
*/
......
......@@ -45,7 +45,7 @@ class GroupRoleScopeValidator extends ConstraintValidator implements ContainerIn
/**
* {@inheritdoc}
*/
public function validate($group_role, Constraint $constraint) {
public function validate($group_role, Constraint $constraint): void {
assert($group_role instanceof GroupRoleInterface);
assert($constraint instanceof GroupRoleScope);
......
<?php
namespace Drupal\Tests\group\Kernel;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\group\PermissionScopeInterface;
use Drupal\user\RoleInterface;
/**
* Tests for the GroupRoleAssigned constraint.
*
* @group group
*
* @coversDefaultClass \Drupal\group\Plugin\Validation\Constraint\GroupRoleAssignedValidator
*/
class GroupRoleAssignedTest extends GroupKernelTestBase {
/**
* Tests individual roles.
*
* @covers ::validate
*/
public function testIndividualRole(): void {
$group_type_id = $this->createGroupType()->id();
$individual_role = [
'scope' => PermissionScopeInterface::INDIVIDUAL_ID,
'group_type' => $group_type_id,
];
// Verify the absence of violations in a newly created role.
$group_role = $this->createGroupRole($individual_role);
$violations = $group_role->getTypedData()->validate();
$this->assertCount(0, $violations, 'A freshly created role has no violations.');
// Assign the role to a member of a group.
$this->createGroup(['type' => $group_type_id])->addMember($this->createUser(), ['group_roles' => [$group_role->id()]]);
// Verify that are still no violations on the group role.
$violations = $group_role->getTypedData()->validate();
$this->assertCount(0, $violations, 'Assigning an individual role still leads to no violations.');
// Create another insider role and verify that it has no violations.
$violations = $this->createGroupRole($individual_role)->getTypedData()->validate();
$this->assertCount(0, $violations, 'Another freshly created role has no violations.');
}
/**
* Tests synchronized roles.
*
* @param string $scope
* The synchronized scope to create the group role in.
*
* @covers ::validate
* @dataProvider synchronizedRoleProvider
*/
public function testSynchronizedRole(string $scope): void {
$group_type_id_a = $this->createGroupType()->id();
$group_type_id_b = $this->createGroupType()->id();
$synchronized_role = ['scope' => $scope, 'global_role' => RoleInterface::AUTHENTICATED_ID];
// Verify the absence of violations in a newly created role.
$group_role = $this->createGroupRole($synchronized_role + ['group_type' => $group_type_id_a]);
$violations = $group_role->getTypedData()->validate();
$this->assertCount(0, $violations, 'A freshly created role has no violations.');
// Assign the role to a member of a group.
$this->createGroup(['type' => $group_type_id_a])->addMember($this->createUser(), ['group_roles' => [$group_role->id()]]);
// Verify that there is now a violation on the group role.
$violations = $group_role->getTypedData()->validate();
$this->assertCount(1, $violations, 'Assigning the role to a member triggers a violation.');
$message = new TranslatableMarkup(
'Cannot save this group role in the %scope scope as it has already been assigned to individual members.',
['%scope' => $group_role->getScope()]
);
$this->assertEquals((string) $message, (string) $violations->get(0)->getMessage());
// Create another role and verify that it has no violations.
$violations = $this->createGroupRole($synchronized_role + ['group_type' => $group_type_id_b])->getTypedData()->validate();
$this->assertCount(0, $violations, 'Another freshly created role has no violations.');
}
/**
* Data provider for testSynchronizedRole().
*
* @return array
* A list of testSynchronizedRole method arguments.
*/
public function synchronizedRoleProvider() {
return [
'insider' => ['scope' => PermissionScopeInterface::INSIDER_ID],
'outsider' => ['scope' => PermissionScopeInterface::OUTSIDER_ID],
];
}
}
......@@ -2,6 +2,9 @@
namespace Drupal\Tests\group\Kernel;
use Drupal\group\Entity\GroupMembership;
use Drupal\group\Entity\GroupMembershipInterface;
use Drupal\group\Entity\Storage\GroupRoleStorageInterface;
use Drupal\group\PermissionScopeInterface;
use Drupal\user\RoleInterface;
......@@ -98,6 +101,106 @@ class GroupRoleStorageTest extends GroupKernelTestBase {
$this->compareMemberRoles([$individual_role->id(), $insider_role->id()], TRUE, 'User also has synchronized insider role.');
}
/**
* Tests checking whether a group role is assigned to someone.
*
* @covers ::hasMembershipReferences
*/
public function testHasMembershipReferences() {
$storage = $this->entityTypeManager->getStorage('group_role');
assert($storage instanceof GroupRoleStorageInterface);
$group_role_a = $this->createGroupRole([
'group_type' => $this->group->bundle(),
'scope' => PermissionScopeInterface::INDIVIDUAL_ID,
]);
$group_role_b = $this->createGroupRole([
'group_type' => $this->group->bundle(),
'scope' => PermissionScopeInterface::INDIVIDUAL_ID,
]);
$this->assertFalse($storage->hasMembershipReferences([$group_role_a->id()]), 'Group role A is not referenced right after creation.');
$this->assertFalse($storage->hasMembershipReferences([$group_role_b->id()]), 'Group role B is not referenced right after creation.');
$this->assertFalse($storage->hasMembershipReferences([$group_role_a->id(), $group_role_b->id()]), 'Group roles are not referenced right after creation.');
$this->group->addMember($this->account);
$this->assertFalse($storage->hasMembershipReferences([$group_role_a->id()]), 'Group role A is not referenced after a member is created.');
$this->assertFalse($storage->hasMembershipReferences([$group_role_b->id()]), 'Group role B is not referenced after a member is created.');
$this->assertFalse($storage->hasMembershipReferences([$group_role_a->id(), $group_role_b->id()]), 'Group roles are not referenced after a member is created.');
$membership = GroupMembership::loadSingle($this->group, $this->account);
assert($membership instanceof GroupMembershipInterface);
$membership->addRole($group_role_a->id());
$this->assertTrue($storage->hasMembershipReferences([$group_role_a->id()]), 'Group role A is referenced after a member was assigned group role A.');
$this->assertFalse($storage->hasMembershipReferences([$group_role_b->id()]), 'Group role B is not referenced after a member was assigned group role A.');
$this->assertTrue($storage->hasMembershipReferences([$group_role_a->id(), $group_role_b->id()]), 'Group role A is referenced after a member was assigned group role A.');
$membership->removeRole($group_role_a->id());
$this->assertFalse($storage->hasMembershipReferences([$group_role_a->id()]), 'Group role A is not referenced after a member was revoked group role A.');
$this->assertFalse($storage->hasMembershipReferences([$group_role_b->id()]), 'Group role B is not referenced after a member was revoked group role A.');
$this->assertFalse($storage->hasMembershipReferences([$group_role_a->id(), $group_role_b->id()]), 'Group role A is referenced after a member was revoked group role A.');
}
/**
* Tests the deleting of a group role's assignments.
*
* @covers ::deleteMembershipReferences
* @depends testHasMembershipReferences
*/
public function testDeleteMembershipReferences() {
$storage = $this->entityTypeManager->getStorage('group_role');
assert($storage instanceof GroupRoleStorageInterface);
$group_role_a = $this->createGroupRole([
'group_type' => $this->group->bundle(),
'scope' => PermissionScopeInterface::INDIVIDUAL_ID,
]);
$group_role_b = $this->createGroupRole([
'group_type' => $this->group->bundle(),
'scope' => PermissionScopeInterface::INDIVIDUAL_ID,
]);
$group_role_c = $this->createGroupRole([
'group_type' => $this->group->bundle(),
'scope' => PermissionScopeInterface::INDIVIDUAL_ID,
]);
$group_role_ids = [
$group_role_a->id(),
$group_role_b->id(),
$group_role_c->id(),
];
$this->group->addMember($this->account, ['group_roles' => $group_role_ids]);
$this->assertTrue($storage->hasMembershipReferences([$group_role_a->id()]));
$this->assertTrue($storage->hasMembershipReferences([$group_role_b->id()]));
$this->assertTrue($storage->hasMembershipReferences([$group_role_c->id()]));
$this->assertTrue($storage->hasMembershipReferences([$group_role_a->id(), $group_role_b->id(), $group_role_c->id()]));
// Check whether the membership entity is also correct.
$membership = GroupMembership::loadSingle($this->group, $this->account);
assert($membership instanceof GroupMembershipInterface);
$this->assertSame($group_role_ids, array_keys($membership->getRoles()));
// Delete a single reference.
$storage->deleteMembershipReferences([$group_role_a->id()]);
$this->assertFalse($storage->hasMembershipReferences([$group_role_a->id()]));
$this->assertTrue($storage->hasMembershipReferences([$group_role_b->id()]));
$this->assertTrue($storage->hasMembershipReferences([$group_role_c->id()]));
$this->assertTrue($storage->hasMembershipReferences([$group_role_a->id(), $group_role_b->id(), $group_role_c->id()]));
// Check whether the membership entity is also updated.
$this->assertSame([$group_role_b->id(), $group_role_c->id()], array_keys($membership->getRoles()));
// Delete multiple references.
$storage->deleteMembershipReferences([$group_role_b->id(), $group_role_c->id()]);
$this->assertFalse($storage->hasMembershipReferences([$group_role_a->id()]));
$this->assertFalse($storage->hasMembershipReferences([$group_role_b->id()]));
$this->assertFalse($storage->hasMembershipReferences([$group_role_c->id()]));
$this->assertFalse($storage->hasMembershipReferences([$group_role_a->id(), $group_role_b->id(), $group_role_c->id()]));
// Check whether the membership entity is also updated.
$this->assertSame([], array_keys($membership->getRoles()));
}
/**
* Asserts that the test user's group roles match a provided list of IDs.
*
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment