Skip to content

Commit da3642c

Browse files
authored
Merge pull request from GHSA-394j-x37r-2q27
IBX-3821: Added new Role and MemberOf limitations
2 parents 1fcb7c4 + 1c26778 commit da3642c

11 files changed

Lines changed: 1369 additions & 1 deletion

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\Contracts\Core\Repository\Values\User\Limitation;
10+
11+
use Ibexa\Contracts\Core\Repository\Values\User\Limitation;
12+
13+
final class MemberOfLimitation extends Limitation
14+
{
15+
public const IDENTIFIER = 'MemberOf';
16+
17+
public function getIdentifier(): string
18+
{
19+
return self::IDENTIFIER;
20+
}
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\Contracts\Core\Repository\Values\User\Limitation;
10+
11+
use Ibexa\Contracts\Core\Repository\Values\User\Limitation;
12+
13+
final class UserRoleLimitation extends Limitation
14+
{
15+
public const IDENTIFIER = 'Role';
16+
17+
public function getIdentifier(): string
18+
{
19+
return self::IDENTIFIER;
20+
}
21+
}
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\Core\Limitation;
10+
11+
use Ibexa\Contracts\Core\Limitation\Type as SPILimitationTypeInterface;
12+
use Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException;
13+
use Ibexa\Contracts\Core\Repository\Exceptions\NotImplementedException;
14+
use Ibexa\Contracts\Core\Repository\Values\User\Limitation as APILimitationValue;
15+
use Ibexa\Contracts\Core\Repository\Values\User\Limitation\MemberOfLimitation;
16+
use Ibexa\Contracts\Core\Repository\Values\User\User;
17+
use Ibexa\Contracts\Core\Repository\Values\User\UserGroup;
18+
use Ibexa\Contracts\Core\Repository\Values\User\UserGroupRoleAssignment;
19+
use Ibexa\Contracts\Core\Repository\Values\User\UserReference as APIUserReference;
20+
use Ibexa\Contracts\Core\Repository\Values\User\UserRoleAssignment;
21+
use Ibexa\Contracts\Core\Repository\Values\ValueObject;
22+
use Ibexa\Core\Base\Exceptions\InvalidArgumentException;
23+
use Ibexa\Core\Base\Exceptions\InvalidArgumentType;
24+
use Ibexa\Core\FieldType\ValidationError;
25+
26+
final class MemberOfLimitationType extends AbstractPersistenceLimitationType implements SPILimitationTypeInterface
27+
{
28+
public const SELF_USER_GROUP = -1;
29+
30+
/**
31+
* @throws \Ibexa\Core\Base\Exceptions\InvalidArgumentException
32+
*/
33+
public function acceptValue(APILimitationValue $limitationValue): void
34+
{
35+
if (!$limitationValue instanceof MemberOfLimitation) {
36+
throw new InvalidArgumentType(
37+
'$limitationValue',
38+
MemberOfLimitation::class,
39+
$limitationValue
40+
);
41+
}
42+
43+
if (!is_array($limitationValue->limitationValues)) {
44+
throw new InvalidArgumentType(
45+
'$limitationValue->limitationValues',
46+
'array',
47+
$limitationValue->limitationValues
48+
);
49+
}
50+
51+
foreach ($limitationValue->limitationValues as $key => $id) {
52+
if (!is_int($id)) {
53+
throw new InvalidArgumentType("\$limitationValue->limitationValues[{$key}]", 'int|string', $id);
54+
}
55+
}
56+
}
57+
58+
public function validate(APILimitationValue $limitationValue)
59+
{
60+
$validationErrors = [];
61+
62+
foreach ($limitationValue->limitationValues as $key => $id) {
63+
if ($id === self::SELF_USER_GROUP) {
64+
continue;
65+
}
66+
try {
67+
$this->persistence->contentHandler()->loadContentInfo($id);
68+
} catch (NotFoundException $e) {
69+
$validationErrors[] = new ValidationError(
70+
"limitationValues[%key%] => '%value%' does not exist in the backend",
71+
null,
72+
[
73+
'value' => $id,
74+
'key' => $key,
75+
]
76+
);
77+
}
78+
}
79+
80+
return $validationErrors;
81+
}
82+
83+
/**
84+
* @param mixed[] $limitationValues
85+
*/
86+
public function buildValue(array $limitationValues): APILimitationValue
87+
{
88+
return new MemberOfLimitation(['limitationValues' => $limitationValues]);
89+
}
90+
91+
public function evaluate(APILimitationValue $value, APIUserReference $currentUser, ValueObject $object, array $targets = null)
92+
{
93+
if (!$value instanceof MemberOfLimitation) {
94+
throw new InvalidArgumentException(
95+
'$value',
96+
sprintf('Must be of type: %s', MemberOfLimitation::class)
97+
);
98+
}
99+
100+
if (!$object instanceof User
101+
&& !$object instanceof UserGroup
102+
&& !$object instanceof UserRoleAssignment
103+
&& !$object instanceof UserGroupRoleAssignment
104+
) {
105+
return self::ACCESS_ABSTAIN;
106+
}
107+
108+
if ($object instanceof User) {
109+
return $this->evaluateUser($value, $object, $currentUser);
110+
}
111+
112+
if ($object instanceof UserGroup) {
113+
return $this->evaluateUserGroup($value, $object, $currentUser);
114+
}
115+
116+
if ($object instanceof UserRoleAssignment) {
117+
return $this->evaluateUser($value, $object->getUser(), $currentUser);
118+
}
119+
120+
if ($object instanceof UserGroupRoleAssignment) {
121+
return $this->evaluateUserGroup($value, $object->getUserGroup(), $currentUser);
122+
}
123+
124+
return self::ACCESS_DENIED;
125+
}
126+
127+
public function getCriterion(APILimitationValue $value, APIUserReference $currentUser)
128+
{
129+
throw new NotImplementedException('Member of Limitation Criterion');
130+
}
131+
132+
public function valueSchema()
133+
{
134+
throw new NotImplementedException(__METHOD__);
135+
}
136+
137+
private function evaluateUser(MemberOfLimitation $value, User $object, APIUserReference $currentUser): bool
138+
{
139+
if (empty($value->limitationValues)) {
140+
return self::ACCESS_DENIED;
141+
}
142+
143+
$userLocations = $this->persistence->locationHandler()->loadLocationsByContent($object->getUserId());
144+
145+
$userGroups = [];
146+
foreach ($userLocations as $userLocation) {
147+
$userGroups[] = $this->persistence->locationHandler()->load($userLocation->parentId);
148+
}
149+
$userGroupsIdList = array_column($userGroups, 'contentId');
150+
$limitationValuesUserGroupsIdList = $value->limitationValues;
151+
152+
if (in_array(self::SELF_USER_GROUP, $limitationValuesUserGroupsIdList)) {
153+
$currentUserGroupsIdList = $this->getCurrentUserGroupsIdList($currentUser);
154+
155+
// Granted, if current user is in exactly those same groups
156+
if (count(array_intersect($userGroupsIdList, $currentUserGroupsIdList)) === count($userGroupsIdList)) {
157+
return self::ACCESS_GRANTED;
158+
}
159+
160+
// Unset SELF value, for next check
161+
$key = array_search(self::SELF_USER_GROUP, $limitationValuesUserGroupsIdList);
162+
unset($limitationValuesUserGroupsIdList[$key]);
163+
}
164+
165+
// Granted, if limitationValues matched user groups 1:1
166+
if (!empty($limitationValuesUserGroupsIdList)
167+
&& empty(array_diff($userGroupsIdList, $limitationValuesUserGroupsIdList))
168+
) {
169+
return self::ACCESS_GRANTED;
170+
}
171+
172+
return self::ACCESS_DENIED;
173+
}
174+
175+
private function evaluateUserGroup(MemberOfLimitation $value, UserGroup $userGroup, APIUserReference $currentUser): bool
176+
{
177+
$limitationValuesUserGroupsIdList = $value->limitationValues;
178+
if (in_array(self::SELF_USER_GROUP, $limitationValuesUserGroupsIdList)) {
179+
$limitationValuesUserGroupsIdList = $this->getCurrentUserGroupsIdList($currentUser);
180+
}
181+
182+
return in_array($userGroup->id, $limitationValuesUserGroupsIdList);
183+
}
184+
185+
private function getCurrentUserGroupsIdList(APIUserReference $currentUser): array
186+
{
187+
$currentUserLocations = $this->persistence->locationHandler()->loadLocationsByContent($currentUser->getUserId());
188+
$currentUserGroups = [];
189+
foreach ($currentUserLocations as $currentUserLocation) {
190+
$currentUserGroups[] = $this->persistence->locationHandler()->load($currentUserLocation->parentId);
191+
}
192+
193+
return array_column(
194+
$currentUserGroups,
195+
'contentId'
196+
);
197+
}
198+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\Core\Limitation;
10+
11+
use Ibexa\Contracts\Core\Limitation\Type as SPILimitationTypeInterface;
12+
use Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException;
13+
use Ibexa\Contracts\Core\Repository\Exceptions\NotImplementedException;
14+
use Ibexa\Contracts\Core\Repository\Values\User\Limitation as APILimitationValue;
15+
use Ibexa\Contracts\Core\Repository\Values\User\Limitation\UserRoleLimitation;
16+
use Ibexa\Contracts\Core\Repository\Values\User\Role;
17+
use Ibexa\Contracts\Core\Repository\Values\User\User;
18+
use Ibexa\Contracts\Core\Repository\Values\User\UserGroup;
19+
use Ibexa\Contracts\Core\Repository\Values\User\UserGroupRoleAssignment;
20+
use Ibexa\Contracts\Core\Repository\Values\User\UserReference as APIUserReference;
21+
use Ibexa\Contracts\Core\Repository\Values\User\UserRoleAssignment;
22+
use Ibexa\Contracts\Core\Repository\Values\ValueObject;
23+
use Ibexa\Core\Base\Exceptions\InvalidArgumentException;
24+
use Ibexa\Core\Base\Exceptions\InvalidArgumentType;
25+
use Ibexa\Core\FieldType\ValidationError;
26+
27+
final class RoleLimitationType extends AbstractPersistenceLimitationType implements SPILimitationTypeInterface
28+
{
29+
/**
30+
* @throws \Ibexa\Core\Base\Exceptions\InvalidArgumentException
31+
*/
32+
public function acceptValue(APILimitationValue $limitationValue): void
33+
{
34+
if (!$limitationValue instanceof UserRoleLimitation) {
35+
throw new InvalidArgumentType(
36+
'$limitationValue',
37+
UserRoleLimitation::class,
38+
$limitationValue
39+
);
40+
}
41+
42+
if (!is_array($limitationValue->limitationValues)) {
43+
throw new InvalidArgumentType(
44+
'$limitationValue->limitationValues',
45+
'array',
46+
$limitationValue->limitationValues
47+
);
48+
}
49+
50+
foreach ($limitationValue->limitationValues as $key => $id) {
51+
if (!is_int($id)) {
52+
throw new InvalidArgumentType("\$limitationValue->limitationValues[{$key}]", 'int|string', $id);
53+
}
54+
}
55+
}
56+
57+
public function validate(APILimitationValue $limitationValue)
58+
{
59+
$validationErrors = [];
60+
61+
foreach ($limitationValue->limitationValues as $key => $id) {
62+
try {
63+
$this->persistence->userHandler()->loadRole($id);
64+
} catch (NotFoundException $e) {
65+
$validationErrors[] = new ValidationError(
66+
"limitationValues[%key%] => '%value%' does not exist in the backend",
67+
null,
68+
[
69+
'value' => $id,
70+
'key' => $key,
71+
]
72+
);
73+
}
74+
}
75+
76+
return $validationErrors;
77+
}
78+
79+
/**
80+
* @param mixed[] $limitationValues
81+
*/
82+
public function buildValue(array $limitationValues): APILimitationValue
83+
{
84+
return new UserRoleLimitation(['limitationValues' => $limitationValues]);
85+
}
86+
87+
public function evaluate(APILimitationValue $value, APIUserReference $currentUser, ValueObject $object, array $targets = null)
88+
{
89+
if (!$value instanceof UserRoleLimitation) {
90+
throw new InvalidArgumentException(
91+
'$value',
92+
sprintf('Must be of type: %s', UserRoleLimitation::class)
93+
);
94+
}
95+
96+
if (
97+
!$object instanceof Role
98+
&& !$object instanceof UserRoleAssignment
99+
&& !$object instanceof UserGroupRoleAssignment
100+
&& ($targets === null && ($object instanceof User || $object instanceof UserGroup))
101+
) {
102+
return self::ACCESS_ABSTAIN;
103+
}
104+
105+
if ($targets !== null) {
106+
foreach ($targets as $target) {
107+
if ($target instanceof Role && !$this->evaluateRole($value, $target)) {
108+
return self::ACCESS_DENIED;
109+
}
110+
111+
return self::ACCESS_GRANTED;
112+
}
113+
}
114+
115+
if ($object instanceof Role) {
116+
return $this->evaluateRole($value, $object);
117+
}
118+
119+
if ($object instanceof UserRoleAssignment || $object instanceof UserGroupRoleAssignment) {
120+
return $this->evaluateRole($value, $object->getRole());
121+
}
122+
123+
return self::ACCESS_DENIED;
124+
}
125+
126+
public function getCriterion(APILimitationValue $value, APIUserReference $currentUser)
127+
{
128+
throw new NotImplementedException('Role Limitation Criterion');
129+
}
130+
131+
public function valueSchema()
132+
{
133+
throw new NotImplementedException(__METHOD__);
134+
}
135+
136+
private function evaluateRole(UserRoleLimitation $value, Role $role): bool
137+
{
138+
return in_array($role->id, $value->limitationValues);
139+
}
140+
}

src/lib/Resources/settings/policies.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ parameters:
3030
administrate: ~
3131

3232
role:
33-
assign: ~
33+
assign: { MemberOf: true, Role: true }
3434
update: ~
3535
create: ~
3636
delete: ~

0 commit comments

Comments
 (0)