Skip to content

Commit 3756aa0

Browse files
committed
Add user-group permissions to query
1 parent 75749e6 commit 3756aa0

11 files changed

Lines changed: 152 additions & 24 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/**
4+
* SPDX-License-Identifier: MIT
5+
* Copyright (c) 2017-2018 Tobias Reich
6+
* Copyright (c) 2018-2025 LycheeOrg.
7+
*/
8+
9+
namespace App\Constants;
10+
11+
class UsersUserGroupsConstants
12+
{
13+
// computed table name
14+
public const USERS_USER_GROUPS = 'users_user_groups';
15+
16+
// Id names
17+
public const USER_ID = 'user_id';
18+
public const USER_GROUP_ID = 'user_group_id';
19+
20+
// Attributes name
21+
public const ROLE = 'role';
22+
}

app/Http/Resources/Rights/SettingsRightsResource.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use App\Policies\SettingsPolicy;
1414
use App\Policies\UserGroupPolicy;
1515
use Illuminate\Support\Facades\Gate;
16+
use LycheeVerify\Verify;
1617
use Spatie\LaravelData\Data;
1718
use Spatie\TypeScriptTransformer\Attributes\TypeScript;
1819

@@ -33,6 +34,6 @@ public function __construct()
3334
$this->can_see_diagnostics = Gate::check(SettingsPolicy::CAN_SEE_DIAGNOSTICS, [Configs::class]);
3435
$this->can_update = Gate::check(SettingsPolicy::CAN_UPDATE, [Configs::class]);
3536
$this->can_access_dev_tools = Gate::check(SettingsPolicy::CAN_ACCESS_DEV_TOOLS, [Configs::class]);
36-
$this->can_acess_user_groups = Gate::check(UserGroupPolicy::CAN_LIST, [UserGroup::class]) && config('features.user-groups') === true;
37+
$this->can_acess_user_groups = resolve(Verify::class)->check() && Gate::check(UserGroupPolicy::CAN_LIST, [UserGroup::class]) && config('features.user-groups') === true;
3738
}
3839
}

app/Models/AccessPermission.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
*
2323
* @property int $id
2424
* @property int|null $user_id
25+
* @property int|null $user_group_id
2526
* @property string|null $base_album_id
2627
* @property bool $is_link_required
2728
* @property string|null $password
@@ -120,6 +121,16 @@ public function user(): BelongsTo
120121
return $this->belongsTo(User::class, 'user_id', 'id');
121122
}
122123

124+
/**
125+
* Return the relationship between an AccessPermission and its associated UserGroup.
126+
*
127+
* @return BelongsTo<UserGroup,$this>
128+
*/
129+
public function user_group(): BelongsTo
130+
{
131+
return $this->belongsTo(UserGroup::class, 'user_group_id', 'id');
132+
}
133+
123134
/**
124135
* Given an AccessPermission, duplicate its reccord.
125136
* - Password is NOT transfered

app/Models/BaseAlbumImpl.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,15 +252,22 @@ public function access_permissions(): hasMany
252252
*/
253253
public function current_user_permissions(): AccessPermission|null
254254
{
255-
return $this->access_permissions->first(fn (AccessPermission $p) => $p->user_id !== null && $p->user_id === Auth::id());
255+
if (Auth::guest()) {
256+
return null; // No permissions for guests
257+
}
258+
259+
$user = Auth::user();
260+
261+
return $this->access_permissions->first(fn (AccessPermission $p) => $p->user_id === $user->id)
262+
?? $this->access_permissions->first(fn (AccessPermission $p) => in_array($p->user_group_id, $user->user_groups->map(fn ($g) => $g->id)->all(), true));
256263
}
257264

258265
/**
259266
* Returns the relationship between an album and its associated public permissions.
260267
*/
261268
public function public_permissions(): AccessPermission|null
262269
{
263-
return $this->access_permissions->first(fn (AccessPermission $p) => $p->user_id === null);
270+
return $this->access_permissions->first(fn (AccessPermission $p) => $p->user_id === null && $p->user_group_id === null);
264271
}
265272

266273
/**

app/Models/User.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ class User extends Authenticatable implements WebAuthnAuthenticatable
120120

121121
protected $hidden = [];
122122

123+
protected $with = [
124+
'user_groups',
125+
];
126+
123127
/**
124128
* Create a new Eloquent query builder for the model.
125129
*

app/Policies/AlbumQueryPolicy.php

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use App\Models\Builders\AlbumBuilder;
2020
use App\Models\Builders\TagAlbumBuilder;
2121
use App\Models\TagAlbum;
22+
use App\Models\User;
2223
use Illuminate\Database\Eloquent\Builder;
2324
use Illuminate\Database\Query\Builder as BaseBuilder;
2425
use Illuminate\Support\Facades\Auth;
@@ -519,26 +520,49 @@ private function getComputedAccessPermissionSubQuery(bool $full = false): BaseBu
519520
$select[] = APC::GRANTS_UPLOAD;
520521
$select[] = APC::USER_ID;
521522
}
522-
$user_id = Auth::id();
523+
if (Auth::guest()) {
524+
return DB::table('access_permissions', APC::COMPUTED_ACCESS_PERMISSIONS)->select($select)->whereNull(APC::USER_ID)->whereNull(APC::USER_GROUP_ID);
525+
}
523526

524-
return DB::table('access_permissions', APC::COMPUTED_ACCESS_PERMISSIONS)->select($select)
525-
->when(
526-
Auth::check(),
527-
fn ($q1) => $q1
528-
->where(APC::USER_ID, '=', $user_id)
529-
->orWhere(
530-
fn ($q2) => $q2->whereNull(APC::USER_ID)
531-
->whereNotIn(
532-
APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::BASE_ALBUM_ID,
533-
fn ($q3) => $q3->select('acc_per.' . APC::BASE_ALBUM_ID)
534-
->from('access_permissions', 'acc_per')
535-
->where(APC::USER_ID, '=', $user_id)
536-
)
527+
/** @var User $user */
528+
$user = Auth::user();
529+
// Collect the user groups of the current user.
530+
/** @var int[] $user_groups */
531+
$user_groups = $user->user_groups->map(fn ($g) => $g->id)->all();
532+
533+
return DB::table('access_permissions', APC::COMPUTED_ACCESS_PERMISSIONS)
534+
->select($select)
535+
// First select the permissions based on the user.
536+
->where(APC::USER_ID, '=', $user->id)
537+
// Then select the permissions based on the user groups.
538+
->orWhere(
539+
fn ($q2) => $q2->whereIn(
540+
APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::USER_GROUP_ID,
541+
$user_groups
542+
)
543+
// and ensure that we already have not selected the user permissions.
544+
// This is important to avoid selecting the user permissions twice.
545+
->whereNotIn(
546+
APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::BASE_ALBUM_ID,
547+
fn ($q3) => $q3->select('acc_per.' . APC::BASE_ALBUM_ID)
548+
->from('access_permissions', 'acc_per')
549+
->where(APC::USER_ID, '=', $user->id)
537550
)
538551
)
539-
->when(
540-
!Auth::check(),
541-
fn ($q1) => $q1->whereNull(APC::USER_ID)
552+
// Then select the public permissions.
553+
->orWhere(
554+
fn ($q2) => $q2->whereNull(APC::USER_ID)->whereNull(APC::USER_GROUP_ID)
555+
->whereNotIn(
556+
APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::BASE_ALBUM_ID,
557+
// Ensure that we already have not selected the user or group permissions.
558+
fn ($q3) => $q3->select('acc_per.' . APC::BASE_ALBUM_ID)
559+
->from('access_permissions', 'acc_per')
560+
->where(APC::USER_ID, '=', $user->id)
561+
->orWhereIn(
562+
APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::USER_GROUP_ID,
563+
$user_groups
564+
)
565+
)
542566
);
543567
}
544568

database/factories/AccessPermissionFactory.php

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use App\Models\AccessPermission;
1212
use App\Models\Album;
1313
use App\Models\User;
14+
use App\Models\UserGroup;
1415
use Illuminate\Database\Eloquent\Factories\Factory;
1516
use Illuminate\Support\Facades\Hash;
1617

@@ -49,6 +50,7 @@ public function public()
4950
return $this->state(function (array $attributes) {
5051
return [
5152
'user_id' => null,
53+
'user_group_id' => null,
5254
];
5355
});
5456
}
@@ -67,9 +69,22 @@ public function for_user(User $user)
6769
return $this->state(function (array $attributes) use ($user) {
6870
return [
6971
'user_id' => $user->id,
72+
'user_group_id' => null,
7073
];
7174
})->afterCreating(function (AccessPermission $perm) {
72-
$perm->load('album', 'user');
75+
$perm->load('album', 'user', 'user_group');
76+
});
77+
}
78+
79+
public function for_user_group(UserGroup $userGroup)
80+
{
81+
return $this->state(function (array $attributes) use ($userGroup) {
82+
return [
83+
'user_id' => null,
84+
'user_group_id' => $userGroup->id,
85+
];
86+
})->afterCreating(function (AccessPermission $perm) {
87+
$perm->load('album', 'user', 'user_group');
7388
});
7489
}
7590

@@ -134,7 +149,7 @@ public function for_album(Album $album)
134149
'base_album_id' => $album->id,
135150
];
136151
})->afterCreating(function (AccessPermission $perm) {
137-
$perm->load('album', 'user');
152+
$perm->load('album', 'user', 'user_group');
138153
});
139154
}
140155
}

tests/Feature_v2/Album/AlbumTest.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,37 @@ public function testGetAnon(): void
6565
]);
6666
}
6767

68+
public function testGetAsGroup(): void
69+
{
70+
$response = $this->actingAs($this->userWithGroup1)->getJsonWithData('Album', ['album_id' => $this->album1->id]);
71+
$this->assertOk($response);
72+
$response->assertJson([
73+
'config' => [
74+
'is_base_album' => true,
75+
'is_model_album' => true,
76+
'is_accessible' => true,
77+
'is_password_protected' => false,
78+
'is_search_accessible' => true,
79+
],
80+
'resource' => [
81+
'id' => $this->album1->id,
82+
'title' => $this->album1->title,
83+
'albums' => [],
84+
'photos' => [
85+
[
86+
'id' => $this->photo1->id,
87+
],
88+
[
89+
'id' => $this->photo1b->id,
90+
],
91+
],
92+
],
93+
]);
94+
95+
$response->assertJsonCount(0, 'resource.albums');
96+
$response->assertJsonCount(2, 'resource.photos');
97+
}
98+
6899
public function testGetAsOwner(): void
69100
{
70101
$response = $this->actingAs($this->userMayUpload1)->getJsonWithData('Album', ['album_id' => $this->tagAlbum1->id]);

tests/Feature_v2/Album/SharingTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ public function testOverride(): void
206206
'grants_upload' => true,
207207
]);
208208
$this->assertOk($response);
209-
self::assertEquals(2, AccessPermission::where(APC::BASE_ALBUM_ID, '=', $this->album1->id)->count());
209+
self::assertEquals(3, AccessPermission::where(APC::BASE_ALBUM_ID, '=', $this->album1->id)->count());
210210

211211
// Update sub album permission.
212212
$response = $this->actingAs($this->userMayUpload1)->putJson('Sharing', [

tests/Feature_v2/Base/BaseApiWithDataTest.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ abstract class BaseApiWithDataTest extends BaseApiTest
8383
protected AccessPermission $perm1;
8484
protected AccessPermission $perm4;
8585
protected AccessPermission $perm44;
86+
protected AccessPermission $perm11;
8687

8788
protected UserGroup $group1;
8889
protected UserGroup $group2;
@@ -147,6 +148,17 @@ public function setUp(): void
147148
->grants_full_photo()
148149
->create();
149150

151+
$this->perm11 = AccessPermission::factory()
152+
->for_user_group($this->group1)
153+
->for_album($this->album1)
154+
->visible()
155+
->grants_edit()
156+
->grants_delete()
157+
->grants_upload()
158+
->grants_download()
159+
->grants_full_photo()
160+
->create();
161+
150162
$this->album5 = Album::factory()->as_root()->owned_by($this->admin)->create();
151163

152164
$this->withoutVite();

0 commit comments

Comments
 (0)