Skip to content

Commit a4fd673

Browse files
committed
Merge branch 'development' into release
2 parents e794c97 + 813d140 commit a4fd673

File tree

333 files changed

+5121
-2371
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

333 files changed

+5121
-2371
lines changed

.env.example.complete

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ OIDC_DUMP_USER_DETAILS=false
268268
OIDC_USER_TO_GROUPS=false
269269
OIDC_GROUPS_CLAIM=groups
270270
OIDC_REMOVE_FROM_GROUPS=false
271+
OIDC_EXTERNAL_ID_CLAIM=sub
271272

272273
# Disable default third-party services such as Gravatar and Draw.IO
273274
# Service-specific options will override this option

.github/translators.txt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ Alexander Predl (Harveyhase68) :: German
176176
Rem (Rem9000) :: Dutch
177177
Michał Stelmach (stelmach-web) :: Polish
178178
arniom :: French
179-
REMOVED_USER :: ; Dutch; Turkish
179+
REMOVED_USER :: ; French; Dutch; Turkish
180180
林祖年 (contagion) :: Chinese Traditional
181181
Siamak Guodarzi (siamakgoudarzi88) :: Persian
182182
Lis Maestrelo (lismtrl) :: Portuguese, Brazilian
@@ -302,3 +302,9 @@ Angelos Chouvardas (achouvardas) :: Greek
302302
rndrss :: Portuguese, Brazilian
303303
rirac294 :: Russian
304304
David Furman (thefourCraft) :: Hebrew
305+
Pafzedog :: French
306+
Yllelder :: Spanish
307+
Adrian Ocneanu (aocneanu) :: Romanian
308+
Eduardo Castanho (EduardoCastanho) :: Portuguese
309+
VIET NAM VPS (vietnamvps) :: Vietnamese
310+
m4tthi4s :: French

.github/workflows/test-php.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
uses: shivammathur/setup-php@v2
1717
with:
1818
php-version: ${{ matrix.php }}
19-
extensions: gd, mbstring, json, curl, xml, mysql, ldap
19+
extensions: gd, mbstring, json, curl, xml, mysql, ldap, gmp
2020

2121
- name: Get Composer Cache Directory
2222
id: composer-cache

app/Actions/Activity.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
namespace BookStack\Actions;
44

5+
use BookStack\Auth\Permissions\JointPermission;
56
use BookStack\Auth\User;
67
use BookStack\Entities\Models\Entity;
78
use BookStack\Model;
89
use Illuminate\Database\Eloquent\Relations\BelongsTo;
10+
use Illuminate\Database\Eloquent\Relations\HasMany;
911
use Illuminate\Database\Eloquent\Relations\MorphTo;
1012
use Illuminate\Support\Str;
1113

@@ -40,6 +42,12 @@ public function user(): BelongsTo
4042
return $this->belongsTo(User::class);
4143
}
4244

45+
public function jointPermissions(): HasMany
46+
{
47+
return $this->hasMany(JointPermission::class, 'entity_id', 'entity_id')
48+
->whereColumn('activities.entity_type', '=', 'joint_permissions.entity_type');
49+
}
50+
4351
/**
4452
* Returns text from the language files, Looks up by using the activity key.
4553
*/

app/Actions/Favourite.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
namespace BookStack\Actions;
44

5+
use BookStack\Auth\Permissions\JointPermission;
56
use BookStack\Model;
7+
use Illuminate\Database\Eloquent\Relations\HasMany;
68
use Illuminate\Database\Eloquent\Relations\MorphTo;
79

810
class Favourite extends Model
@@ -16,4 +18,10 @@ public function favouritable(): MorphTo
1618
{
1719
return $this->morphTo();
1820
}
21+
22+
public function jointPermissions(): HasMany
23+
{
24+
return $this->hasMany(JointPermission::class, 'entity_id', 'favouritable_id')
25+
->whereColumn('favourites.favouritable_type', '=', 'joint_permissions.entity_type');
26+
}
1927
}

app/Actions/Tag.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
namespace BookStack\Actions;
44

5+
use BookStack\Auth\Permissions\JointPermission;
56
use BookStack\Model;
67
use Illuminate\Database\Eloquent\Factories\HasFactory;
8+
use Illuminate\Database\Eloquent\Relations\HasMany;
79
use Illuminate\Database\Eloquent\Relations\MorphTo;
810

911
/**
@@ -27,6 +29,12 @@ public function entity(): MorphTo
2729
return $this->morphTo('entity');
2830
}
2931

32+
public function jointPermissions(): HasMany
33+
{
34+
return $this->hasMany(JointPermission::class, 'entity_id', 'entity_id')
35+
->whereColumn('tags.entity_type', '=', 'joint_permissions.entity_type');
36+
}
37+
3038
/**
3139
* Get a full URL to start a tag name search for this tag name.
3240
*/

app/Actions/View.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
namespace BookStack\Actions;
44

5+
use BookStack\Auth\Permissions\JointPermission;
56
use BookStack\Interfaces\Viewable;
67
use BookStack\Model;
8+
use Illuminate\Database\Eloquent\Relations\HasMany;
79
use Illuminate\Database\Eloquent\Relations\MorphTo;
810

911
/**
@@ -28,6 +30,12 @@ public function viewable(): MorphTo
2830
return $this->morphTo();
2931
}
3032

33+
public function jointPermissions(): HasMany
34+
{
35+
return $this->hasMany(JointPermission::class, 'entity_id', 'viewable_id')
36+
->whereColumn('views.viewable_type', '=', 'joint_permissions.entity_type');
37+
}
38+
3139
/**
3240
* Increment the current user's view count for the given viewable model.
3341
*/

app/Auth/Access/Oidc/OidcService.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,8 @@ protected function getUserGroups(OidcIdToken $token): array
198198
*/
199199
protected function getUserDetails(OidcIdToken $token): array
200200
{
201-
$id = $token->getClaim('sub');
201+
$idClaim = $this->config()['external_id_claim'];
202+
$id = $token->getClaim($idClaim);
202203

203204
return [
204205
'external_id' => $id,
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
<?php
2+
3+
namespace BookStack\Auth\Permissions;
4+
5+
use BookStack\Auth\Role;
6+
use BookStack\Entities\Models\Entity;
7+
use Illuminate\Database\Eloquent\Builder;
8+
9+
class EntityPermissionEvaluator
10+
{
11+
protected string $action;
12+
13+
public function __construct(string $action)
14+
{
15+
$this->action = $action;
16+
}
17+
18+
public function evaluateEntityForUser(Entity $entity, array $userRoleIds): ?bool
19+
{
20+
if ($this->isUserSystemAdmin($userRoleIds)) {
21+
return true;
22+
}
23+
24+
$typeIdChain = $this->gatherEntityChainTypeIds(SimpleEntityData::fromEntity($entity));
25+
$relevantPermissions = $this->getPermissionsMapByTypeId($typeIdChain, [...$userRoleIds, 0]);
26+
$permitsByType = $this->collapseAndCategorisePermissions($typeIdChain, $relevantPermissions);
27+
28+
$status = $this->evaluatePermitsByType($permitsByType);
29+
30+
return is_null($status) ? null : $status === PermissionStatus::IMPLICIT_ALLOW || $status === PermissionStatus::EXPLICIT_ALLOW;
31+
}
32+
33+
/**
34+
* @param array<string, array<string, int>> $permitsByType
35+
*/
36+
protected function evaluatePermitsByType(array $permitsByType): ?int
37+
{
38+
// Return grant or reject from role-level if exists
39+
if (count($permitsByType['role']) > 0) {
40+
return max($permitsByType['role']) ? PermissionStatus::EXPLICIT_ALLOW : PermissionStatus::EXPLICIT_DENY;
41+
}
42+
43+
// Return fallback permission if exists
44+
if (count($permitsByType['fallback']) > 0) {
45+
return $permitsByType['fallback'][0] ? PermissionStatus::IMPLICIT_ALLOW : PermissionStatus::IMPLICIT_DENY;
46+
}
47+
48+
return null;
49+
}
50+
51+
/**
52+
* @param string[] $typeIdChain
53+
* @param array<string, EntityPermission[]> $permissionMapByTypeId
54+
* @return array<string, array<string, int>>
55+
*/
56+
protected function collapseAndCategorisePermissions(array $typeIdChain, array $permissionMapByTypeId): array
57+
{
58+
$permitsByType = ['fallback' => [], 'role' => []];
59+
60+
foreach ($typeIdChain as $typeId) {
61+
$permissions = $permissionMapByTypeId[$typeId] ?? [];
62+
foreach ($permissions as $permission) {
63+
$roleId = $permission->role_id;
64+
$type = $roleId === 0 ? 'fallback' : 'role';
65+
if (!isset($permitsByType[$type][$roleId])) {
66+
$permitsByType[$type][$roleId] = $permission->{$this->action};
67+
}
68+
}
69+
70+
if (isset($permitsByType['fallback'][0])) {
71+
break;
72+
}
73+
}
74+
75+
return $permitsByType;
76+
}
77+
78+
/**
79+
* @param string[] $typeIdChain
80+
* @return array<string, EntityPermission[]>
81+
*/
82+
protected function getPermissionsMapByTypeId(array $typeIdChain, array $filterRoleIds): array
83+
{
84+
$query = EntityPermission::query()->where(function (Builder $query) use ($typeIdChain) {
85+
foreach ($typeIdChain as $typeId) {
86+
$query->orWhere(function (Builder $query) use ($typeId) {
87+
[$type, $id] = explode(':', $typeId);
88+
$query->where('entity_type', '=', $type)
89+
->where('entity_id', '=', $id);
90+
});
91+
}
92+
});
93+
94+
if (!empty($filterRoleIds)) {
95+
$query->where(function (Builder $query) use ($filterRoleIds) {
96+
$query->whereIn('role_id', [...$filterRoleIds, 0]);
97+
});
98+
}
99+
100+
$relevantPermissions = $query->get(['entity_id', 'entity_type', 'role_id', $this->action])->all();
101+
102+
$map = [];
103+
foreach ($relevantPermissions as $permission) {
104+
$key = $permission->entity_type . ':' . $permission->entity_id;
105+
if (!isset($map[$key])) {
106+
$map[$key] = [];
107+
}
108+
109+
$map[$key][] = $permission;
110+
}
111+
112+
return $map;
113+
}
114+
115+
/**
116+
* @return string[]
117+
*/
118+
protected function gatherEntityChainTypeIds(SimpleEntityData $entity): array
119+
{
120+
// The array order here is very important due to the fact we walk up the chain
121+
// elsewhere in the class. Earlier items in the chain have higher priority.
122+
123+
$chain = [$entity->type . ':' . $entity->id];
124+
125+
if ($entity->type === 'page' && $entity->chapter_id) {
126+
$chain[] = 'chapter:' . $entity->chapter_id;
127+
}
128+
129+
if ($entity->type === 'page' || $entity->type === 'chapter') {
130+
$chain[] = 'book:' . $entity->book_id;
131+
}
132+
133+
return $chain;
134+
}
135+
136+
protected function isUserSystemAdmin($userRoleIds): bool
137+
{
138+
$adminRoleId = Role::getSystemRole('admin')->id;
139+
return in_array($adminRoleId, $userRoleIds);
140+
}
141+
}

0 commit comments

Comments
 (0)