1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace SilverStripe\Security\Tests; |
4
|
|
|
|
5
|
|
|
use InvalidArgumentException; |
6
|
|
|
use SilverStripe\Control\Controller; |
7
|
|
|
use SilverStripe\Dev\FunctionalTest; |
8
|
|
|
use SilverStripe\ORM\ArrayList; |
9
|
|
|
use SilverStripe\ORM\DataObject; |
10
|
|
|
use SilverStripe\Security\Group; |
11
|
|
|
use SilverStripe\Security\Member; |
12
|
|
|
use SilverStripe\Security\Permission; |
13
|
|
|
use SilverStripe\Security\Tests\GroupTest\TestMember; |
14
|
|
|
|
15
|
|
|
class GroupTest extends FunctionalTest |
16
|
|
|
{ |
17
|
|
|
protected static $fixture_file = 'GroupTest.yml'; |
18
|
|
|
|
19
|
|
|
protected static $extra_dataobjects = [ |
20
|
|
|
TestMember::class |
21
|
|
|
]; |
22
|
|
|
|
23
|
|
|
protected function setUp() |
24
|
|
|
{ |
25
|
|
|
parent::setUp(); |
26
|
|
|
} |
27
|
|
|
|
28
|
|
|
public function testGroupCodeDefaultsToTitle() |
29
|
|
|
{ |
30
|
|
|
$g1 = new Group(); |
31
|
|
|
$g1->Title = "My Title"; |
32
|
|
|
$g1->write(); |
33
|
|
|
$this->assertEquals('my-title', $g1->Code, 'Custom title gets converted to code if none exists already'); |
34
|
|
|
|
35
|
|
|
$g2 = new Group(); |
36
|
|
|
$g2->Title = "My Title"; |
37
|
|
|
$g2->Code = "my-code"; |
38
|
|
|
$g2->write(); |
39
|
|
|
$this->assertEquals('my-code', $g2->Code, 'Custom attributes are not overwritten by Title field'); |
40
|
|
|
|
41
|
|
|
$g3 = new Group(); |
42
|
|
|
$g3->Title = _t('SilverStripe\\Admin\\SecurityAdmin.NEWGROUP', "New Group"); |
43
|
|
|
$g3->write(); |
44
|
|
|
$this->assertNull($g3->Code, 'Default title doesnt trigger attribute setting'); |
45
|
|
|
} |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* @skipUpgrade |
49
|
|
|
*/ |
50
|
|
|
public function testMemberGroupRelationForm() |
51
|
|
|
{ |
52
|
|
|
$this->logInAs($this->idFromFixture(TestMember::class, 'admin')); |
53
|
|
|
|
54
|
|
|
$adminGroup = $this->objFromFixture(Group::class, 'admingroup'); |
55
|
|
|
$parentGroup = $this->objFromFixture(Group::class, 'parentgroup'); |
56
|
|
|
|
57
|
|
|
// Test single group relation through checkboxsetfield |
58
|
|
|
$form = new GroupTest\MemberForm(Controller::curr(), 'Form'); |
59
|
|
|
/** @var Member $member */ |
60
|
|
|
$member = $this->objFromFixture(TestMember::class, 'admin'); |
61
|
|
|
$form->loadDataFrom($member); |
62
|
|
|
$checkboxSetField = $form->Fields()->fieldByName('Groups'); |
63
|
|
|
$checkboxSetField->setValue( |
64
|
|
|
array( |
65
|
|
|
$adminGroup->ID => $adminGroup->ID, // keep existing relation |
66
|
|
|
$parentGroup->ID => $parentGroup->ID, // add new relation |
67
|
|
|
) |
68
|
|
|
); |
69
|
|
|
$form->saveInto($member); |
70
|
|
|
$updatedGroups = $member->Groups(); |
71
|
|
|
|
72
|
|
|
$this->assertEquals( |
73
|
|
|
2, |
74
|
|
|
count($updatedGroups->column()), |
75
|
|
|
"Adding a toplevel group works" |
76
|
|
|
); |
77
|
|
|
$this->assertContains($adminGroup->ID, $updatedGroups->column('ID')); |
78
|
|
|
$this->assertContains($parentGroup->ID, $updatedGroups->column('ID')); |
79
|
|
|
|
80
|
|
|
// Test unsetting relationship |
81
|
|
|
$form->loadDataFrom($member); |
82
|
|
|
$checkboxSetField = $form->Fields()->fieldByName('Groups'); |
83
|
|
|
$checkboxSetField->setValue( |
84
|
|
|
array( |
85
|
|
|
$adminGroup->ID => $adminGroup->ID, // keep existing relation |
86
|
|
|
//$parentGroup->ID => $parentGroup->ID, // remove previously set relation |
|
|
|
|
87
|
|
|
) |
88
|
|
|
); |
89
|
|
|
$form->saveInto($member); |
90
|
|
|
$member->flushCache(); |
91
|
|
|
$updatedGroups = $member->Groups(); |
92
|
|
|
$this->assertEquals( |
93
|
|
|
1, |
94
|
|
|
count($updatedGroups->column()), |
95
|
|
|
"Removing a previously added toplevel group works" |
96
|
|
|
); |
97
|
|
|
$this->assertContains($adminGroup->ID, $updatedGroups->column('ID')); |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
public function testUnsavedGroups() |
101
|
|
|
{ |
102
|
|
|
$member = $this->objFromFixture(TestMember::class, 'admin'); |
103
|
|
|
$group = new Group(); |
104
|
|
|
|
105
|
|
|
// Can save user to unsaved group |
106
|
|
|
$group->Members()->add($member); |
107
|
|
|
$this->assertEquals(array($member->ID), array_values($group->Members()->getIDList())); |
108
|
|
|
|
109
|
|
|
// Persists after writing to DB |
110
|
|
|
$group->write(); |
111
|
|
|
|
112
|
|
|
/** @var Group $group */ |
113
|
|
|
$group = Group::get()->byID($group->ID); |
114
|
|
|
$this->assertEquals(array($member->ID), array_values($group->Members()->getIDList())); |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
public function testCollateAncestorIDs() |
118
|
|
|
{ |
119
|
|
|
/** @var Group $parentGroup */ |
120
|
|
|
$parentGroup = $this->objFromFixture(Group::class, 'parentgroup'); |
121
|
|
|
/** @var Group $childGroup */ |
122
|
|
|
$childGroup = $this->objFromFixture(Group::class, 'childgroup'); |
123
|
|
|
$orphanGroup = new Group(); |
124
|
|
|
$orphanGroup->ParentID = 99999; |
125
|
|
|
$orphanGroup->write(); |
126
|
|
|
|
127
|
|
|
$this->assertEquals( |
128
|
|
|
1, |
129
|
|
|
count($parentGroup->collateAncestorIDs()), |
130
|
|
|
'Root node only contains itself' |
131
|
|
|
); |
132
|
|
|
$this->assertContains($parentGroup->ID, $parentGroup->collateAncestorIDs()); |
133
|
|
|
|
134
|
|
|
$this->assertEquals( |
135
|
|
|
2, |
136
|
|
|
count($childGroup->collateAncestorIDs()), |
137
|
|
|
'Contains parent nodes, with child node first' |
138
|
|
|
); |
139
|
|
|
$this->assertContains($parentGroup->ID, $childGroup->collateAncestorIDs()); |
140
|
|
|
$this->assertContains($childGroup->ID, $childGroup->collateAncestorIDs()); |
141
|
|
|
|
142
|
|
|
$this->assertEquals( |
143
|
|
|
1, |
144
|
|
|
count($orphanGroup->collateAncestorIDs()), |
145
|
|
|
'Orphaned nodes dont contain invalid parent IDs' |
146
|
|
|
); |
147
|
|
|
$this->assertContains($orphanGroup->ID, $orphanGroup->collateAncestorIDs()); |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
/** |
151
|
|
|
* Test that Groups including their children (recursively) are collated and returned |
152
|
|
|
*/ |
153
|
|
|
public function testCollateFamilyIds() |
154
|
|
|
{ |
155
|
|
|
/** @var Group $group */ |
156
|
|
|
$group = $this->objFromFixture(Group::class, 'parentgroup'); |
157
|
|
|
$groupIds = $this->allFixtureIDs(Group::class); |
158
|
|
|
$ids = array_intersect_key($groupIds, array_flip(['parentgroup', 'childgroup', 'grandchildgroup'])); |
159
|
|
|
$this->assertEquals(array_values($ids), $group->collateFamilyIDs()); |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
/** |
163
|
|
|
* Test that an exception is thrown if collateFamilyIDs is called on an unsaved Group |
164
|
|
|
*/ |
165
|
|
|
public function testCannotCollateUnsavedGroupFamilyIds() |
166
|
|
|
{ |
167
|
|
|
$this->expectException(InvalidArgumentException::class); |
168
|
|
|
$this->expectExceptionMessage('Cannot call collateFamilyIDs on unsaved Group.'); |
169
|
|
|
$group = new Group; |
170
|
|
|
$group->collateFamilyIDs(); |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
/** |
174
|
|
|
* Test that a Group's children can be retrieved |
175
|
|
|
*/ |
176
|
|
|
public function testGetAllChildren() |
177
|
|
|
{ |
178
|
|
|
/** @var Group $group */ |
179
|
|
|
$group = $this->objFromFixture(Group::class, 'parentgroup'); |
180
|
|
|
$children = $group->getAllChildren(); |
181
|
|
|
$this->assertInstanceOf(ArrayList::class, $children); |
182
|
|
|
$this->assertSame(['childgroup', 'grandchildgroup'], $children->column('Code')); |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
public function testGroupInGroupMethods() |
186
|
|
|
{ |
187
|
|
|
$parentGroup = $this->objFromFixture(Group::class, 'parentgroup'); |
188
|
|
|
$childGroup = $this->objFromFixture(Group::class, 'childgroup'); |
189
|
|
|
$grandchildGroup = $this->objFromFixture(Group::class, 'grandchildgroup'); |
190
|
|
|
$adminGroup = $this->objFromFixture(Group::class, 'admingroup'); |
191
|
|
|
$group1 = $this->objFromFixture(Group::class, 'group1'); |
192
|
|
|
|
193
|
|
|
$this->assertTrue($grandchildGroup->inGroup($childGroup)); |
|
|
|
|
194
|
|
|
$this->assertTrue($grandchildGroup->inGroup($childGroup->ID)); |
195
|
|
|
$this->assertTrue($grandchildGroup->inGroup($childGroup->Code)); |
|
|
|
|
196
|
|
|
|
197
|
|
|
$this->assertTrue($grandchildGroup->inGroup($parentGroup)); |
198
|
|
|
$this->assertTrue($grandchildGroup->inGroups([$parentGroup, $childGroup])); |
|
|
|
|
199
|
|
|
$this->assertTrue($grandchildGroup->inGroups([$childGroup, $parentGroup])); |
200
|
|
|
$this->assertTrue($grandchildGroup->inGroups([$parentGroup, $childGroup], true)); |
201
|
|
|
|
202
|
|
|
$this->assertFalse($grandchildGroup->inGroup($adminGroup)); |
203
|
|
|
$this->assertFalse($grandchildGroup->inGroups([$adminGroup, $group1])); |
204
|
|
|
$this->assertFalse($grandchildGroup->inGroups([$adminGroup, $childGroup], true)); |
205
|
|
|
|
206
|
|
|
$this->assertFalse($grandchildGroup->inGroup('NotARealGroup')); |
207
|
|
|
$this->assertFalse($grandchildGroup->inGroup(99999999999)); |
208
|
|
|
$this->assertFalse($grandchildGroup->inGroup(new TestMember())); |
209
|
|
|
|
210
|
|
|
// Edgecases |
211
|
|
|
$this->assertTrue($grandchildGroup->inGroup($grandchildGroup)); |
212
|
|
|
$this->assertFalse($grandchildGroup->inGroups([])); |
213
|
|
|
$this->assertFalse($grandchildGroup->inGroups([], true)); |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
public function testDelete() |
217
|
|
|
{ |
218
|
|
|
$group = $this->objFromFixture(Group::class, 'parentgroup'); |
219
|
|
|
$groupID = $group->ID; |
220
|
|
|
$childGroupID = $this->idFromFixture(Group::class, 'childgroup'); |
221
|
|
|
$group->delete(); |
222
|
|
|
|
223
|
|
|
$this->assertEquals( |
224
|
|
|
0, |
225
|
|
|
DataObject::get(Group::class, "\"ID\" = {$groupID}")->count(), |
226
|
|
|
'Group is removed' |
227
|
|
|
); |
228
|
|
|
$this->assertEquals( |
229
|
|
|
0, |
230
|
|
|
DataObject::get(Permission::class, "\"GroupID\" = {$groupID}")->count(), |
231
|
|
|
'Permissions removed along with the group' |
232
|
|
|
); |
233
|
|
|
$this->assertEquals( |
234
|
|
|
0, |
235
|
|
|
DataObject::get(Group::class, "\"ParentID\" = {$groupID}")->count(), |
236
|
|
|
'Child groups are removed' |
237
|
|
|
); |
238
|
|
|
$this->assertEquals( |
239
|
|
|
0, |
240
|
|
|
DataObject::get(Group::class, "\"ParentID\" = {$childGroupID}")->count(), |
241
|
|
|
'Grandchild groups are removed' |
242
|
|
|
); |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
public function testValidatesPrivilegeLevelOfParent() |
246
|
|
|
{ |
247
|
|
|
/** @var Group $nonAdminGroup */ |
248
|
|
|
$nonAdminGroup = $this->objFromFixture(Group::class, 'childgroup'); |
249
|
|
|
/** @var Group $adminGroup */ |
250
|
|
|
$adminGroup = $this->objFromFixture(Group::class, 'admingroup'); |
251
|
|
|
|
252
|
|
|
// Making admin group parent of a non-admin group, effectively expanding is privileges |
253
|
|
|
$nonAdminGroup->ParentID = $adminGroup->ID; |
254
|
|
|
|
255
|
|
|
$this->logInWithPermission('APPLY_ROLES'); |
256
|
|
|
$result = $nonAdminGroup->validate(); |
257
|
|
|
$this->assertFalse( |
258
|
|
|
$result->isValid(), |
259
|
|
|
'Members with only APPLY_ROLES can\'t assign parent groups with direct ADMIN permissions' |
260
|
|
|
); |
261
|
|
|
|
262
|
|
|
$this->logInWithPermission('ADMIN'); |
263
|
|
|
$result = $nonAdminGroup->validate(); |
264
|
|
|
$this->assertTrue( |
265
|
|
|
$result->isValid(), |
266
|
|
|
'Members with ADMIN can assign parent groups with direct ADMIN permissions' |
267
|
|
|
); |
268
|
|
|
$nonAdminGroup->write(); |
269
|
|
|
|
270
|
|
|
$this->logInWithPermission('ADMIN'); |
271
|
|
|
/** @var Group $inheritedAdminGroup */ |
272
|
|
|
$inheritedAdminGroup = $this->objFromFixture(Group::class, 'group1'); |
273
|
|
|
$inheritedAdminGroup->ParentID = $adminGroup->ID; |
274
|
|
|
$inheritedAdminGroup->write(); // only works with ADMIN login |
275
|
|
|
|
276
|
|
|
$this->logInWithPermission('APPLY_ROLES'); |
277
|
|
|
$result = $nonAdminGroup->validate(); |
278
|
|
|
$this->assertFalse( |
279
|
|
|
$result->isValid(), |
280
|
|
|
'Members with only APPLY_ROLES can\'t assign parent groups with inherited ADMIN permission' |
281
|
|
|
); |
282
|
|
|
} |
283
|
|
|
} |
284
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.