silverstripe-framework/tests/php/Security/GroupTest.php
Guy Sartorelli e2e32317d6
API Move various classes to more appropriate namespaces (#11370)
Also rename ViewableData to ModelData ahead of the template layer
lift-and-shift
2024-09-23 14:31:50 +12:00

377 lines
14 KiB
PHP

<?php
namespace SilverStripe\Security\Tests;
use InvalidArgumentException;
use SilverStripe\Control\Controller;
use SilverStripe\Dev\FunctionalTest;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\Model\List\ArrayList;
use SilverStripe\ORM\DataObject;
use SilverStripe\Security\Group;
use SilverStripe\Security\Member;
use SilverStripe\Security\Permission;
use SilverStripe\Security\Tests\GroupTest\TestMember;
class GroupTest extends FunctionalTest
{
protected static $fixture_file = 'GroupTest.yml';
protected static $extra_dataobjects = [
TestMember::class
];
protected function setUp(): void
{
parent::setUp();
}
public function testGroupCodeDefaultsToTitle()
{
$g1 = new Group();
$g1->Title = "My Title";
$g1->write();
$this->assertEquals('my-title', $g1->Code, 'Custom title gets converted to code if none exists already');
$g2 = new Group();
$g2->Title = "My Title and Code";
$g2->Code = "my-code";
$g2->write();
$this->assertEquals('my-code', $g2->Code, 'Custom attributes are not overwritten by Title field');
$g3 = new Group();
$g3->Title = _t('SilverStripe\\Admin\\SecurityAdmin.NEWGROUP', "New Group");
$g3->write();
$this->assertNull($g3->Code, 'Default title doesnt trigger attribute setting');
}
public function testMemberGroupRelationForm()
{
$this->logInAs($this->idFromFixture(TestMember::class, 'admin'));
$adminGroup = $this->objFromFixture(Group::class, 'admingroup');
$parentGroup = $this->objFromFixture(Group::class, 'parentgroup');
// Test single group relation through checkboxsetfield
$form = new GroupTest\MemberForm(Controller::curr(), 'Form');
/** @var Member $member */
$member = $this->objFromFixture(TestMember::class, 'admin');
$form->loadDataFrom($member);
$checkboxSetField = $form->Fields()->fieldByName('Groups');
$checkboxSetField->setValue(
[
$adminGroup->ID => $adminGroup->ID, // keep existing relation
$parentGroup->ID => $parentGroup->ID, // add new relation
]
);
$form->saveInto($member);
$updatedGroups = $member->Groups();
$this->assertEquals(
2,
count($updatedGroups->column() ?? []),
"Adding a toplevel group works"
);
$this->assertContains($adminGroup->ID, $updatedGroups->column('ID'));
$this->assertContains($parentGroup->ID, $updatedGroups->column('ID'));
// Test unsetting relationship
$form->loadDataFrom($member);
$checkboxSetField = $form->Fields()->fieldByName('Groups');
$checkboxSetField->setValue(
[
$adminGroup->ID => $adminGroup->ID, // keep existing relation
//$parentGroup->ID => $parentGroup->ID, // remove previously set relation
]
);
$form->saveInto($member);
$member->flushCache();
$updatedGroups = $member->Groups();
$this->assertEquals(
1,
count($updatedGroups->column() ?? []),
"Removing a previously added toplevel group works"
);
$this->assertContains($adminGroup->ID, $updatedGroups->column('ID'));
}
public function testUnsavedGroups()
{
$member = $this->objFromFixture(TestMember::class, 'admin');
$group = new Group();
$group->Title = 'Title';
// Can save user to unsaved group
$group->Members()->add($member);
$this->assertEquals([$member->ID], array_values($group->Members()->getIDList() ?? []));
// Persists after writing to DB
$group->write();
/** @var Group $group */
$group = Group::get()->byID($group->ID);
$this->assertEquals([$member->ID], array_values($group->Members()->getIDList() ?? []));
}
public function testCollateAncestorIDs()
{
/** @var Group $parentGroup */
$parentGroup = $this->objFromFixture(Group::class, 'parentgroup');
/** @var Group $childGroup */
$childGroup = $this->objFromFixture(Group::class, 'childgroup');
$orphanGroup = new Group();
$orphanGroup->Title = 'Title';
$orphanGroup->ParentID = 99999;
$orphanGroup->write();
$this->assertEquals(
1,
count($parentGroup->collateAncestorIDs() ?? []),
'Root node only contains itself'
);
$this->assertContains($parentGroup->ID, $parentGroup->collateAncestorIDs());
$this->assertEquals(
2,
count($childGroup->collateAncestorIDs() ?? []),
'Contains parent nodes, with child node first'
);
$this->assertContains($parentGroup->ID, $childGroup->collateAncestorIDs());
$this->assertContains($childGroup->ID, $childGroup->collateAncestorIDs());
$this->assertEquals(
1,
count($orphanGroup->collateAncestorIDs() ?? []),
'Orphaned nodes dont contain invalid parent IDs'
);
$this->assertContains($orphanGroup->ID, $orphanGroup->collateAncestorIDs());
}
/**
* Test that Groups including their children (recursively) are collated and returned
*/
public function testCollateFamilyIds()
{
/** @var Group $group */
$group = $this->objFromFixture(Group::class, 'parentgroup');
$groupIds = $this->allFixtureIDs(Group::class);
$ids = array_intersect_key($groupIds ?? [], array_flip(['parentgroup', 'childgroup', 'grandchildgroup']));
$this->assertEquals(array_values($ids ?? []), $group->collateFamilyIDs());
}
/**
* Test that an exception is thrown if collateFamilyIDs is called on an unsaved Group
*/
public function testCannotCollateUnsavedGroupFamilyIds()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Cannot call collateFamilyIDs on unsaved Group.');
$group = new Group;
$group->collateFamilyIDs();
}
/**
* Test that a Group's children can be retrieved
*/
public function testGetAllChildren()
{
/** @var Group $group */
$group = $this->objFromFixture(Group::class, 'parentgroup');
$children = $group->getAllChildren();
$this->assertInstanceOf(ArrayList::class, $children);
$this->assertSame(['childgroup', 'grandchildgroup'], $children->column('Code'));
}
public function testGroupInGroupMethods()
{
$parentGroup = $this->objFromFixture(Group::class, 'parentgroup');
$childGroup = $this->objFromFixture(Group::class, 'childgroup');
$grandchildGroup = $this->objFromFixture(Group::class, 'grandchildgroup');
$adminGroup = $this->objFromFixture(Group::class, 'admingroup');
$group1 = $this->objFromFixture(Group::class, 'group1');
$this->assertTrue($grandchildGroup->inGroup($childGroup));
$this->assertTrue($grandchildGroup->inGroup($childGroup->ID));
$this->assertTrue($grandchildGroup->inGroup($childGroup->Code));
$this->assertTrue($grandchildGroup->inGroup($parentGroup));
$this->assertTrue($grandchildGroup->inGroups([$parentGroup, $childGroup]));
$this->assertTrue($grandchildGroup->inGroups([$childGroup, $parentGroup]));
$this->assertTrue($grandchildGroup->inGroups([$parentGroup, $childGroup], true));
$this->assertFalse($grandchildGroup->inGroup($adminGroup));
$this->assertFalse($grandchildGroup->inGroups([$adminGroup, $group1]));
$this->assertFalse($grandchildGroup->inGroups([$adminGroup, $childGroup], true));
$this->assertFalse($grandchildGroup->inGroup('NotARealGroup'));
$this->assertFalse($grandchildGroup->inGroup(99999999999));
$this->assertFalse($grandchildGroup->inGroup(new TestMember()));
// Edgecases
$this->assertTrue($grandchildGroup->inGroup($grandchildGroup));
$this->assertFalse($grandchildGroup->inGroups([]));
$this->assertFalse($grandchildGroup->inGroups([], true));
}
public function testDelete()
{
$group = $this->objFromFixture(Group::class, 'parentgroup');
$groupID = $group->ID;
$childGroupID = $this->idFromFixture(Group::class, 'childgroup');
$group->delete();
$this->assertEquals(
0,
DataObject::get(Group::class, "\"ID\" = {$groupID}")->count(),
'Group is removed'
);
$this->assertEquals(
0,
DataObject::get(Permission::class, "\"GroupID\" = {$groupID}")->count(),
'Permissions removed along with the group'
);
$this->assertEquals(
0,
DataObject::get(Group::class, "\"ParentID\" = {$groupID}")->count(),
'Child groups are removed'
);
$this->assertEquals(
0,
DataObject::get(Group::class, "\"ParentID\" = {$childGroupID}")->count(),
'Grandchild groups are removed'
);
}
public function testValidatesPrivilegeLevelOfParent()
{
/** @var Group $nonAdminGroup */
$nonAdminGroup = $this->objFromFixture(Group::class, 'childgroup');
/** @var Group $adminGroup */
$adminGroup = $this->objFromFixture(Group::class, 'admingroup');
// Making admin group parent of a non-admin group, effectively expanding is privileges
$nonAdminGroup->ParentID = $adminGroup->ID;
$this->logInWithPermission('APPLY_ROLES');
$result = $nonAdminGroup->validate();
$this->assertFalse(
$result->isValid(),
'Members with only APPLY_ROLES can\'t assign parent groups with direct ADMIN permissions'
);
$this->logInWithPermission('ADMIN');
$result = $nonAdminGroup->validate();
$this->assertTrue(
$result->isValid(),
'Members with ADMIN can assign parent groups with direct ADMIN permissions'
);
$nonAdminGroup->write();
$this->logInWithPermission('ADMIN');
/** @var Group $inheritedAdminGroup */
$inheritedAdminGroup = $this->objFromFixture(Group::class, 'group1');
$inheritedAdminGroup->ParentID = $adminGroup->ID;
$inheritedAdminGroup->write(); // only works with ADMIN login
$this->logInWithPermission('APPLY_ROLES');
$result = $nonAdminGroup->validate();
$this->assertFalse(
$result->isValid(),
'Members with only APPLY_ROLES can\'t assign parent groups with inherited ADMIN permission'
);
}
public function testGroupTitleValidation()
{
$group1 = $this->objFromFixture(Group::class, 'group1');
$newGroup = new Group();
$validators = $newGroup->getCMSCompositeValidator()->getValidatorsByType(RequiredFields::class);
$this->assertCount(1, $validators);
$validator = array_shift($validators);
$this->assertInstanceOf(RequiredFields::class, $validator);
$this->assertTrue(in_array('Title', $validator->getRequired() ?? []));
$newGroup->Title = $group1->Title;
$result = $newGroup->validate();
$this->assertFalse(
$result->isValid(),
'Group names cannot be duplicated'
);
$newGroup->Title = 'Title';
$result = $newGroup->validate();
$this->assertTrue($result->isValid());
// Title can be empty to avoid breaking backwards-compatibility.
$newGroup->Title = '';
$result = $newGroup->validate();
$this->assertTrue($result->isValid());
}
public function testGroupTitleDuplication()
{
$group = $this->objFromFixture(Group::class, 'group1');
$group->Title = 'Group title modified';
$group->write();
$this->assertEquals('group-1', $group->Code);
$group = new Group();
$group->Title = 'Group 1';
$group->write();
$this->assertEquals('group-1-2', $group->Code);
$group = new Group();
$group->Title = 'Duplicate';
$group->write();
$group->Title = 'Duplicate renamed';
$group->write();
$this->assertEquals('duplicate', $group->Code);
$group = new Group();
$group->Title = 'Duplicate';
$group->write();
$group->Title = 'More renaming';
$group->write();
$this->assertEquals('duplicate-2', $group->Code);
$group = new Group();
$group->Title = 'Any Title';
$group->Code = 'duplicate';
$group->write();
$this->assertEquals('duplicate-3', $group->Code);
$group1 = new Group();
$group1->Title = 'Any Title1';
$group1->Code = 'some-code';
$group2 = new Group();
$group2->Title = 'Any Title2';
$group2->Code = 'some-code';
$group1->write();
$group2->write();
$this->assertEquals('some-code', $group1->Code);
$this->assertEquals('some-code-2', $group2->Code);
}
public function testSettingCodeRepeatedly()
{
// Setting the code to the code it already was doesn't modify it
$group = $this->objFromFixture(Group::class, 'group1');
$previousCode = $group->Code;
$group->Code = $previousCode;
$group->write();
$this->assertEquals($previousCode, $group->Code);
// Setting the code to a new code does modify it
$group->Code = 'new-code';
$group->write();
$this->assertEquals('new-code', $group->Code);
// The old code can be reused
$group->Code = $previousCode;
$group->write();
$this->assertEquals($previousCode, $group->Code);
}
}