Skip to content

Commit 2bbce04

Browse files
committed
Add user-group permissions to query
1 parent 58caba6 commit 2bbce04

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
@@ -254,15 +254,22 @@ public function access_permissions(): hasMany
254254
*/
255255
public function current_user_permissions(): AccessPermission|null
256256
{
257-
return $this->access_permissions->first(fn (AccessPermission $p) => $p->user_id !== null && $p->user_id === Auth::id());
257+
if (Auth::guest()) {
258+
return null; // No permissions for guests
259+
}
260+
261+
$user = Auth::user();
262+
263+
return $this->access_permissions->first(fn (AccessPermission $p) => $p->user_id === $user->id)
264+
?? $this->access_permissions->first(fn (AccessPermission $p) => in_array($p->user_group_id, $user->user_groups->map(fn ($g) => $g->id)->all(), true));
258265
}
259266

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

268275
/**

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;
@@ -517,26 +518,49 @@ private function getComputedAccessPermissionSubQuery(bool $full = false): BaseBu
517518
$select[] = APC::GRANTS_UPLOAD;
518519
$select[] = APC::USER_ID;
519520
}
520-
$user_id = Auth::id();
521+
if (Auth::guest()) {
522+
return DB::table('access_permissions', APC::COMPUTED_ACCESS_PERMISSIONS)->select($select)->whereNull(APC::USER_ID)->whereNull(APC::USER_GROUP_ID);
523+
}
521524

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

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
@@ -87,6 +87,7 @@ abstract class BaseApiWithDataTest extends BaseApiTest
8787
protected AccessPermission $perm1;
8888
protected AccessPermission $perm4;
8989
protected AccessPermission $perm44;
90+
protected AccessPermission $perm11;
9091

9192
protected UserGroup $group1;
9293
protected UserGroup $group2;
@@ -152,6 +153,17 @@ public function setUp(): void
152153
->grants_full_photo()
153154
->create();
154155

156+
$this->perm11 = AccessPermission::factory()
157+
->for_user_group($this->group1)
158+
->for_album($this->album1)
159+
->visible()
160+
->grants_edit()
161+
->grants_delete()
162+
->grants_upload()
163+
->grants_download()
164+
->grants_full_photo()
165+
->create();
166+
155167
$this->album5 = Album::factory()->as_root()->owned_by($this->admin)->create();
156168

157169
Configs::set('owner_id', $this->admin->id);

0 commit comments

Comments
 (0)