From 2bbce040b66c5a5aecb3bb5b350c2ce973cdfed3 Mon Sep 17 00:00:00 2001 From: ildyria Date: Wed, 11 Jun 2025 15:54:31 +0200 Subject: [PATCH 1/2] Add user-group permissions to query --- app/Constants/UsersUserGroupsConstants.php | 22 +++++++ .../Rights/SettingsRightsResource.php | 3 +- app/Models/AccessPermission.php | 11 ++++ app/Models/BaseAlbumImpl.php | 11 +++- app/Models/User.php | 4 ++ app/Policies/AlbumQueryPolicy.php | 58 +++++++++++++------ .../factories/AccessPermissionFactory.php | 19 +++++- tests/Feature_v2/Album/AlbumTest.php | 31 ++++++++++ tests/Feature_v2/Album/SharingTest.php | 2 +- tests/Feature_v2/Base/BaseApiWithDataTest.php | 12 ++++ tests/Traits/CatchFailures.php | 3 +- 11 files changed, 152 insertions(+), 24 deletions(-) create mode 100644 app/Constants/UsersUserGroupsConstants.php diff --git a/app/Constants/UsersUserGroupsConstants.php b/app/Constants/UsersUserGroupsConstants.php new file mode 100644 index 00000000000..02b7e9d8d35 --- /dev/null +++ b/app/Constants/UsersUserGroupsConstants.php @@ -0,0 +1,22 @@ +can_see_diagnostics = Gate::check(SettingsPolicy::CAN_SEE_DIAGNOSTICS, [Configs::class]); $this->can_update = Gate::check(SettingsPolicy::CAN_UPDATE, [Configs::class]); $this->can_access_dev_tools = Gate::check(SettingsPolicy::CAN_ACCESS_DEV_TOOLS, [Configs::class]); - $this->can_acess_user_groups = Gate::check(UserGroupPolicy::CAN_LIST, [UserGroup::class]) && config('features.user-groups') === true; + $this->can_acess_user_groups = resolve(Verify::class)->check() && Gate::check(UserGroupPolicy::CAN_LIST, [UserGroup::class]) && config('features.user-groups') === true; } } diff --git a/app/Models/AccessPermission.php b/app/Models/AccessPermission.php index 75e2512e8d0..856b7248562 100644 --- a/app/Models/AccessPermission.php +++ b/app/Models/AccessPermission.php @@ -22,6 +22,7 @@ * * @property int $id * @property int|null $user_id + * @property int|null $user_group_id * @property string|null $base_album_id * @property bool $is_link_required * @property string|null $password @@ -120,6 +121,16 @@ public function user(): BelongsTo return $this->belongsTo(User::class, 'user_id', 'id'); } + /** + * Return the relationship between an AccessPermission and its associated UserGroup. + * + * @return BelongsTo + */ + public function user_group(): BelongsTo + { + return $this->belongsTo(UserGroup::class, 'user_group_id', 'id'); + } + /** * Given an AccessPermission, duplicate its reccord. * - Password is NOT transfered diff --git a/app/Models/BaseAlbumImpl.php b/app/Models/BaseAlbumImpl.php index 807e447d6e9..121ae5bc620 100644 --- a/app/Models/BaseAlbumImpl.php +++ b/app/Models/BaseAlbumImpl.php @@ -254,7 +254,14 @@ public function access_permissions(): hasMany */ public function current_user_permissions(): AccessPermission|null { - return $this->access_permissions->first(fn (AccessPermission $p) => $p->user_id !== null && $p->user_id === Auth::id()); + if (Auth::guest()) { + return null; // No permissions for guests + } + + $user = Auth::user(); + + return $this->access_permissions->first(fn (AccessPermission $p) => $p->user_id === $user->id) + ?? $this->access_permissions->first(fn (AccessPermission $p) => in_array($p->user_group_id, $user->user_groups->map(fn ($g) => $g->id)->all(), true)); } /** @@ -262,7 +269,7 @@ public function current_user_permissions(): AccessPermission|null */ public function public_permissions(): AccessPermission|null { - return $this->access_permissions->first(fn (AccessPermission $p) => $p->user_id === null); + return $this->access_permissions->first(fn (AccessPermission $p) => $p->user_id === null && $p->user_group_id === null); } /** diff --git a/app/Models/User.php b/app/Models/User.php index e04240b6aa6..3978e3522b4 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -120,6 +120,10 @@ class User extends Authenticatable implements WebAuthnAuthenticatable protected $hidden = []; + protected $with = [ + 'user_groups', + ]; + /** * Create a new Eloquent query builder for the model. * diff --git a/app/Policies/AlbumQueryPolicy.php b/app/Policies/AlbumQueryPolicy.php index 69695e42d63..adc323ba7a5 100644 --- a/app/Policies/AlbumQueryPolicy.php +++ b/app/Policies/AlbumQueryPolicy.php @@ -19,6 +19,7 @@ use App\Models\Builders\AlbumBuilder; use App\Models\Builders\TagAlbumBuilder; use App\Models\TagAlbum; +use App\Models\User; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Query\Builder as BaseBuilder; use Illuminate\Support\Facades\Auth; @@ -517,26 +518,49 @@ private function getComputedAccessPermissionSubQuery(bool $full = false): BaseBu $select[] = APC::GRANTS_UPLOAD; $select[] = APC::USER_ID; } - $user_id = Auth::id(); + if (Auth::guest()) { + return DB::table('access_permissions', APC::COMPUTED_ACCESS_PERMISSIONS)->select($select)->whereNull(APC::USER_ID)->whereNull(APC::USER_GROUP_ID); + } - return DB::table('access_permissions', APC::COMPUTED_ACCESS_PERMISSIONS)->select($select) - ->when( - Auth::check(), - fn ($q1) => $q1 - ->where(APC::USER_ID, '=', $user_id) - ->orWhere( - fn ($q2) => $q2->whereNull(APC::USER_ID) - ->whereNotIn( - APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::BASE_ALBUM_ID, - fn ($q3) => $q3->select('acc_per.' . APC::BASE_ALBUM_ID) - ->from('access_permissions', 'acc_per') - ->where(APC::USER_ID, '=', $user_id) - ) + /** @var User $user */ + $user = Auth::user(); + // Collect the user groups of the current user. + /** @var int[] $user_groups */ + $user_groups = $user->user_groups->map(fn ($g) => $g->id)->all(); + + return DB::table('access_permissions', APC::COMPUTED_ACCESS_PERMISSIONS) + ->select($select) + // First select the permissions based on the user. + ->where(APC::USER_ID, '=', $user->id) + // Then select the permissions based on the user groups. + ->orWhere( + fn ($q2) => $q2->whereIn( + APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::USER_GROUP_ID, + $user_groups + ) + // and ensure that we already have not selected the user permissions. + // This is important to avoid selecting the user permissions twice. + ->whereNotIn( + APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::BASE_ALBUM_ID, + fn ($q3) => $q3->select('acc_per.' . APC::BASE_ALBUM_ID) + ->from('access_permissions', 'acc_per') + ->where(APC::USER_ID, '=', $user->id) ) ) - ->when( - !Auth::check(), - fn ($q1) => $q1->whereNull(APC::USER_ID) + // Then select the public permissions. + ->orWhere( + fn ($q2) => $q2->whereNull(APC::USER_ID)->whereNull(APC::USER_GROUP_ID) + ->whereNotIn( + APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::BASE_ALBUM_ID, + // Ensure that we already have not selected the user or group permissions. + fn ($q3) => $q3->select('acc_per.' . APC::BASE_ALBUM_ID) + ->from('access_permissions', 'acc_per') + ->where(APC::USER_ID, '=', $user->id) + ->orWhereIn( + APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::USER_GROUP_ID, + $user_groups + ) + ) ); } diff --git a/database/factories/AccessPermissionFactory.php b/database/factories/AccessPermissionFactory.php index 6f3886f7e70..bb3a46cb2da 100644 --- a/database/factories/AccessPermissionFactory.php +++ b/database/factories/AccessPermissionFactory.php @@ -11,6 +11,7 @@ use App\Models\AccessPermission; use App\Models\Album; use App\Models\User; +use App\Models\UserGroup; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Facades\Hash; @@ -49,6 +50,7 @@ public function public() return $this->state(function (array $attributes) { return [ 'user_id' => null, + 'user_group_id' => null, ]; }); } @@ -67,9 +69,22 @@ public function for_user(User $user) return $this->state(function (array $attributes) use ($user) { return [ 'user_id' => $user->id, + 'user_group_id' => null, ]; })->afterCreating(function (AccessPermission $perm) { - $perm->load('album', 'user'); + $perm->load('album', 'user', 'user_group'); + }); + } + + public function for_user_group(UserGroup $userGroup) + { + return $this->state(function (array $attributes) use ($userGroup) { + return [ + 'user_id' => null, + 'user_group_id' => $userGroup->id, + ]; + })->afterCreating(function (AccessPermission $perm) { + $perm->load('album', 'user', 'user_group'); }); } @@ -134,7 +149,7 @@ public function for_album(Album $album) 'base_album_id' => $album->id, ]; })->afterCreating(function (AccessPermission $perm) { - $perm->load('album', 'user'); + $perm->load('album', 'user', 'user_group'); }); } } \ No newline at end of file diff --git a/tests/Feature_v2/Album/AlbumTest.php b/tests/Feature_v2/Album/AlbumTest.php index 7fe47f6411a..5344c4f8e6d 100644 --- a/tests/Feature_v2/Album/AlbumTest.php +++ b/tests/Feature_v2/Album/AlbumTest.php @@ -65,6 +65,37 @@ public function testGetAnon(): void ]); } + public function testGetAsGroup(): void + { + $response = $this->actingAs($this->userWithGroup1)->getJsonWithData('Album', ['album_id' => $this->album1->id]); + $this->assertOk($response); + $response->assertJson([ + 'config' => [ + 'is_base_album' => true, + 'is_model_album' => true, + 'is_accessible' => true, + 'is_password_protected' => false, + 'is_search_accessible' => true, + ], + 'resource' => [ + 'id' => $this->album1->id, + 'title' => $this->album1->title, + 'albums' => [], + 'photos' => [ + [ + 'id' => $this->photo1->id, + ], + [ + 'id' => $this->photo1b->id, + ], + ], + ], + ]); + + $response->assertJsonCount(0, 'resource.albums'); + $response->assertJsonCount(2, 'resource.photos'); + } + public function testGetAsOwner(): void { $response = $this->actingAs($this->userMayUpload1)->getJsonWithData('Album', ['album_id' => $this->tagAlbum1->id]); diff --git a/tests/Feature_v2/Album/SharingTest.php b/tests/Feature_v2/Album/SharingTest.php index 38acd37d98d..9240489c3e5 100644 --- a/tests/Feature_v2/Album/SharingTest.php +++ b/tests/Feature_v2/Album/SharingTest.php @@ -206,7 +206,7 @@ public function testOverride(): void 'grants_upload' => true, ]); $this->assertOk($response); - self::assertEquals(2, AccessPermission::where(APC::BASE_ALBUM_ID, '=', $this->album1->id)->count()); + self::assertEquals(3, AccessPermission::where(APC::BASE_ALBUM_ID, '=', $this->album1->id)->count()); // Update sub album permission. $response = $this->actingAs($this->userMayUpload1)->putJson('Sharing', [ diff --git a/tests/Feature_v2/Base/BaseApiWithDataTest.php b/tests/Feature_v2/Base/BaseApiWithDataTest.php index 730a5a9401c..edfb6b5fed2 100644 --- a/tests/Feature_v2/Base/BaseApiWithDataTest.php +++ b/tests/Feature_v2/Base/BaseApiWithDataTest.php @@ -87,6 +87,7 @@ abstract class BaseApiWithDataTest extends BaseApiTest protected AccessPermission $perm1; protected AccessPermission $perm4; protected AccessPermission $perm44; + protected AccessPermission $perm11; protected UserGroup $group1; protected UserGroup $group2; @@ -152,6 +153,17 @@ public function setUp(): void ->grants_full_photo() ->create(); + $this->perm11 = AccessPermission::factory() + ->for_user_group($this->group1) + ->for_album($this->album1) + ->visible() + ->grants_edit() + ->grants_delete() + ->grants_upload() + ->grants_download() + ->grants_full_photo() + ->create(); + $this->album5 = Album::factory()->as_root()->owned_by($this->admin)->create(); Configs::set('owner_id', $this->admin->id); diff --git a/tests/Traits/CatchFailures.php b/tests/Traits/CatchFailures.php index 0a266ff42d4..782a7e04e17 100644 --- a/tests/Traits/CatchFailures.php +++ b/tests/Traits/CatchFailures.php @@ -65,9 +65,10 @@ protected function assertStatus(TestResponse $response, int|array $expectedStatu } $this->trimException($exception); dump($exception); + // We remove 204 as it does not have content + // We remove 302 because it does not have json data. } elseif (!in_array($response->getStatusCode(), [204, 302, ...$expectedStatusCodeArray], true)) { $this->trimException($exception); - dump($exception); } PHPUnit::assertContains($response->getStatusCode(), $expectedStatusCodeArray); } From fcfb6263de7bfd603d2c9f4114175508137e821a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Viguier?= Date: Fri, 4 Jul 2025 13:57:09 +0200 Subject: [PATCH 2/2] Support User groups in permissions listing (#3430) --- app/Actions/Sharing/Propagate.php | 11 +- app/Actions/Sharing/Share.php | 12 +- .../Http/Requests/HasUserGroupIds.php | 17 +++ .../Http/Requests/RequestAttribute.php | 1 + .../Controllers/Gallery/SharingController.php | 54 ++++++--- .../Requests/Sharing/AddSharingRequest.php | 19 +++- .../Requests/Sharing/EditSharingRequest.php | 2 +- .../Requests/Traits/HasUserGroupIdsTrait.php | 25 +++++ .../Models/AccessPermissionResource.php | 8 +- .../Rights/SettingsRightsResource.php | 2 +- app/Models/AccessPermission.php | 2 + app/Models/User.php | 8 ++ config/features.php | 10 -- .../forms/album/AlbumCreateShareDialog.vue | 34 +++--- .../js/components/forms/album/AlbumShare.vue | 16 ++- .../components/forms/album/AlbumTransfer.vue | 9 +- .../forms/album/SearchTargetUser.vue | 72 +++++------- .../forms/sharing/BulkSharingModal.vue | 44 +++++--- .../forms/sharing/CreateSharing.vue | 37 ++++--- .../js/components/forms/sharing/ShareLine.vue | 5 +- .../search/searchUserGroupComputed.ts | 103 ++++++++++++++++++ resources/js/lychee.d.ts | 2 + tests/Feature_v2/Album/SharingTest.php | 72 +++++++++++- tests/Feature_v2/Photo/PhotoRotateTest.php | 1 + tests/Traits/CatchFailures.php | 9 +- 25 files changed, 432 insertions(+), 143 deletions(-) create mode 100644 app/Contracts/Http/Requests/HasUserGroupIds.php create mode 100644 app/Http/Requests/Traits/HasUserGroupIdsTrait.php create mode 100644 resources/js/composables/search/searchUserGroupComputed.ts diff --git a/app/Actions/Sharing/Propagate.php b/app/Actions/Sharing/Propagate.php index 8d278a0c633..cdf0afafa46 100644 --- a/app/Actions/Sharing/Propagate.php +++ b/app/Actions/Sharing/Propagate.php @@ -49,7 +49,7 @@ private function applyUpdate(Album $album): void // for each descendant, create a new permission if it does not exist. // or update the existing permission. $descendants = $album->descendants()->getQuery()->select('id')->pluck('id'); - $permissions = $album->access_permissions()->whereNotNull('user_id')->get(); + $permissions = $album->access_permissions()->whereNotNull(APC::USER_ID)->orWhereNotNull(APC::USER_GROUP_ID)->get(); // This is super inefficient. // It would be better to do it in a single query... @@ -59,6 +59,7 @@ private function applyUpdate(Album $album): void $perm = AccessPermission::updateOrCreate([ APC::BASE_ALBUM_ID => $descendant, APC::USER_ID => $permission->user_id, + APC::USER_GROUP_ID => $permission->user_group_id, ], [ APC::GRANTS_FULL_PHOTO_ACCESS => $permission->grants_full_photo_access, APC::GRANTS_DOWNLOAD => $permission->grants_download, @@ -106,7 +107,10 @@ private function applyOverwrite(Album $album): void // 2. applying the new permissions. DB::table(APC::ACCESS_PERMISSIONS) - ->whereNotNull('user_id') + ->where(fn ($q) => $q + ->whereNotNull(APC::USER_ID) + ->orWhereNotNull(APC::USER_GROUP_ID) + ) ->whereIn( 'base_album_id', DB::table('albums') @@ -122,7 +126,7 @@ private function applyOverwrite(Album $album): void ->where('_rgt', '<', $album->_rgt) ->pluck('id'); - $access_permissions = $album->access_permissions()->whereNotNull('user_id')->get(); + $access_permissions = $album->access_permissions()->whereNotNull('user_id')->orWhereNotNull('user_group_id')->get(); $new_perm = $access_permissions->reduce( fn (?array $acc, AccessPermission $permission) => array_merge( @@ -131,6 +135,7 @@ private function applyOverwrite(Album $album): void fn ($descendant_id) => [ APC::BASE_ALBUM_ID => $descendant_id, APC::USER_ID => $permission->user_id, + APC::USER_GROUP_ID => $permission->user_group_id, APC::GRANTS_FULL_PHOTO_ACCESS => $permission->grants_full_photo_access, APC::GRANTS_DOWNLOAD => $permission->grants_download, APC::GRANTS_UPLOAD => $permission->grants_upload, diff --git a/app/Actions/Sharing/Share.php b/app/Actions/Sharing/Share.php index 17e1e70f04f..3fba5a78d19 100644 --- a/app/Actions/Sharing/Share.php +++ b/app/Actions/Sharing/Share.php @@ -17,14 +17,21 @@ class Share * Create an access permission from a resource. * * @param AccessPermissionResource $access_permission_resource + * @param int|null $user_id + * @param int|null $user_group_id * @param string $base_album_id * * @return AccessPermission */ - public function do(AccessPermissionResource $access_permission_resource, int $user_id, string $base_album_id): AccessPermission - { + public function do( + AccessPermissionResource $access_permission_resource, + string $base_album_id, + ?int $user_id = null, + ?int $user_group_id = null, + ): AccessPermission { $perm = new AccessPermission(); $perm->user_id = $user_id; + $perm->user_group_id = $user_group_id; $perm->base_album_id = $base_album_id; $perm->grants_full_photo_access = $access_permission_resource->grants_full_photo_access; $perm->grants_download = $access_permission_resource->grants_download; @@ -33,6 +40,7 @@ public function do(AccessPermissionResource $access_permission_resource, int $us $perm->grants_delete = $access_permission_resource->grants_delete; $perm->load('user'); $perm->load('album'); + $perm->load('user_group'); $perm->save(); return $perm; diff --git a/app/Contracts/Http/Requests/HasUserGroupIds.php b/app/Contracts/Http/Requests/HasUserGroupIds.php new file mode 100644 index 00000000000..43c8170f452 --- /dev/null +++ b/app/Contracts/Http/Requests/HasUserGroupIds.php @@ -0,0 +1,17 @@ +userIds()) - ->whereIn('base_album_id', $request->albumIds()) + AccessPermission::whereIn(APC::BASE_ALBUM_ID, $request->albumIds()) + ->where(fn ($q) => $q->whereIn(APC::USER_ID, $request->userIds()) + ->orWhereIn(APC::USER_GROUP_ID, $request->userGroupIds())) ->delete(); $access_permissions = []; + // Not optimal, but this is barely used, so who cares. // A better approach would be to do a massive insert in a single SQL query from the cross product. - foreach ($request->userIds() as $user_id) { - foreach ($request->albumIds() as $album_id) { - $access_permissions[] = $share->do($request->permResource(), $user_id, $album_id); + foreach ($request->albumIds() as $album_id) { + foreach ($request->userIds() as $user_id) { + // Create a new sharing permission for each user and album combination. + // This is not optimal, but it is simple and works. + // A better approach would be to do a massive insert in a single SQL query from the cross product. + $access_permissions[] = $share->do( + access_permission_resource: $request->permResource(), + user_id: $user_id, + base_album_id: $album_id + ); + } + foreach ($request->userGroupIds() as $user_group_id) { + // Create a new sharing permission for each user group and album combination. + // This is not optimal, but it is simple and works. + // A better approach would be to do a massive insert in a single SQL query from the cross product. + $access_permissions[] = $share->do( + access_permission_resource: $request->permResource(), + user_group_id: $user_group_id, + base_album_id: $album_id + ); } } @@ -92,9 +111,10 @@ public function edit(EditSharingRequest $request): AccessPermissionResource */ public function list(ListSharingRequest $request): Collection { - $query = AccessPermission::with(['album', 'user']); - $query = $query->whereNotNull(APC::USER_ID); + $query = AccessPermission::with(['album', 'user', 'user_group']); $query = $query->where(APC::BASE_ALBUM_ID, '=', $request->album()->id); + $query = $query->where(fn ($q) => $q->whereNotNull(APC::USER_ID) + ->orWhereNotNull(APC::USER_GROUP_ID)); return AccessPermissionResource::collect($query->get()); } @@ -108,12 +128,14 @@ public function list(ListSharingRequest $request): Collection */ public function listAll(ListAllSharingRequest $request): Collection { - $query = AccessPermission::with(['album', 'user']); + $query = AccessPermission::with(['album', 'user', 'user_group']); $query = $query->when( !Auth::user()->may_administrate, fn ($q) => $q->whereIn('base_album_id', BaseAlbumImpl::select('id') - ->where('owner_id', '=', Auth::id()))); - $query = $query->whereNotNull('user_id'); + ->where('owner_id', '=', Auth::id())) + ); + $query = $query->whereNotNull(APC::USER_ID); + $query = $query->orWhereNotNull(APC::USER_GROUP_ID); $query = $query->orderBy('base_album_id', 'asc'); return AccessPermissionResource::collect($query->get()); @@ -134,10 +156,12 @@ public function listAlbums(ListAllSharingRequest $request, ListAlbums $list_albu $owner_id = $user->id; } - return TargetAlbumResource::collect($list_albums->do( - albums_filtering: resolve(Collection::class), - parent_id: null, - owner_id: $owner_id) + return TargetAlbumResource::collect( + $list_albums->do( + albums_filtering: resolve(Collection::class), + parent_id: null, + owner_id: $owner_id + ) ); } @@ -173,4 +197,4 @@ public function propagate(PropagateSharingRequest $request, Propagate $propagate $propagate->update($album); } } -} \ No newline at end of file +} diff --git a/app/Http/Requests/Sharing/AddSharingRequest.php b/app/Http/Requests/Sharing/AddSharingRequest.php index e2a6ba8eb20..d2016abc4c7 100644 --- a/app/Http/Requests/Sharing/AddSharingRequest.php +++ b/app/Http/Requests/Sharing/AddSharingRequest.php @@ -10,28 +10,32 @@ use App\Contracts\Http\Requests\HasAccessPermissionResource; use App\Contracts\Http\Requests\HasAlbumIds; +use App\Contracts\Http\Requests\HasUserGroupIds; use App\Contracts\Http\Requests\HasUserIds; use App\Contracts\Http\Requests\RequestAttribute; use App\Contracts\Models\AbstractAlbum; use App\Http\Requests\BaseApiRequest; use App\Http\Requests\Traits\HasAccessPermissionResourceTrait; use App\Http\Requests\Traits\HasAlbumIdsTrait; +use App\Http\Requests\Traits\HasUserGroupIdsTrait; use App\Http\Requests\Traits\HasUserIdsTrait; use App\Http\Resources\Models\AccessPermissionResource; use App\Policies\AlbumPolicy; use App\Rules\IntegerIDRule; use App\Rules\RandomIDRule; use Illuminate\Support\Facades\Gate; +use Illuminate\Validation\ValidationException; /** * Represents a request for setting the shares of specific albums. * * Only the owner of the album (or the admin) can set the shares. */ -class AddSharingRequest extends BaseApiRequest implements HasAlbumIds, HasUserIds, HasAccessPermissionResource +class AddSharingRequest extends BaseApiRequest implements HasAlbumIds, HasUserIds, HasAccessPermissionResource, HasUserGroupIds { use HasAlbumIdsTrait; use HasUserIdsTrait; + use HasUserGroupIdsTrait; use HasAccessPermissionResourceTrait; /** @@ -50,8 +54,10 @@ public function rules(): array return [ RequestAttribute::ALBUM_IDS_ATTRIBUTE => 'required|array|min:1', RequestAttribute::ALBUM_IDS_ATTRIBUTE . '.*' => ['required', new RandomIDRule(false)], - RequestAttribute::USER_IDS_ATTRIBUTE => 'required|array|min:1', + RequestAttribute::USER_IDS_ATTRIBUTE => 'present|array', RequestAttribute::USER_IDS_ATTRIBUTE . '.*' => ['required', new IntegerIDRule(false)], + RequestAttribute::USER_GROUP_IDS_ATTRIBUTE => 'present|array', + RequestAttribute::USER_GROUP_IDS_ATTRIBUTE . '.*' => ['required', new IntegerIDRule(false)], RequestAttribute::GRANTS_DOWNLOAD_ATTRIBUTE => ['required', 'boolean'], RequestAttribute::GRANTS_FULL_PHOTO_ACCESS_ATTRIBUTE => ['required', 'boolean'], RequestAttribute::GRANTS_UPLOAD_ATTRIBUTE => ['required', 'boolean'], @@ -62,11 +68,20 @@ public function rules(): array /** * {@inheritDoc} + * + * @throws ValidationException if no users or groups are specified */ protected function processValidatedValues(array $values, array $files): void { $this->album_ids = $values[RequestAttribute::ALBUM_IDS_ATTRIBUTE]; $this->user_ids = $values[RequestAttribute::USER_IDS_ATTRIBUTE]; + $this->user_group_ids = $values[RequestAttribute::USER_GROUP_IDS_ATTRIBUTE]; + + if ($this->user_ids === [] && $this->user_group_ids === []) { + // If no users or groups are specified, we do not create a permission. + throw new ValidationException('You must specify at least one user or group to share with.', 422); + } + $this->perm_resource = new AccessPermissionResource( grants_edit: static::toBoolean($values[RequestAttribute::GRANTS_EDIT_ATTRIBUTE]), grants_delete: static::toBoolean($values[RequestAttribute::GRANTS_DELETE_ATTRIBUTE]), diff --git a/app/Http/Requests/Sharing/EditSharingRequest.php b/app/Http/Requests/Sharing/EditSharingRequest.php index b121b4f51d9..6a5d8cdfbf6 100644 --- a/app/Http/Requests/Sharing/EditSharingRequest.php +++ b/app/Http/Requests/Sharing/EditSharingRequest.php @@ -64,7 +64,7 @@ protected function processValidatedValues(array $values, array $files): void { /** @var int $id */ $id = $values[RequestAttribute::PERMISSION_ID]; - $this->perm = AccessPermission::with(['album', 'user'])->findOrFail($id); + $this->perm = AccessPermission::with(['album', 'user', 'user_group'])->findOrFail($id); $this->perm_resource = new AccessPermissionResource( grants_edit: static::toBoolean($values[RequestAttribute::GRANTS_EDIT_ATTRIBUTE]), diff --git a/app/Http/Requests/Traits/HasUserGroupIdsTrait.php b/app/Http/Requests/Traits/HasUserGroupIdsTrait.php new file mode 100644 index 00000000000..dba49c81746 --- /dev/null +++ b/app/Http/Requests/Traits/HasUserGroupIdsTrait.php @@ -0,0 +1,25 @@ + + */ + protected array $user_group_ids = []; + + /** + * @return array + */ + public function userGroupIds(): array + { + return $this->user_group_ids; + } +} diff --git a/app/Http/Resources/Models/AccessPermissionResource.php b/app/Http/Resources/Models/AccessPermissionResource.php index 1af9e821c16..064e3717bd8 100644 --- a/app/Http/Resources/Models/AccessPermissionResource.php +++ b/app/Http/Resources/Models/AccessPermissionResource.php @@ -18,7 +18,9 @@ class AccessPermissionResource extends Data public function __construct( public ?int $id = null, public ?int $user_id = null, + public ?int $user_group_id = null, public ?string $username = null, + public ?string $user_group_name = null, public ?string $album_title = null, public ?string $album_id = null, public bool $grants_full_photo_access = false, @@ -33,8 +35,10 @@ public static function fromModel(AccessPermission $access_permission): AccessPer { return new AccessPermissionResource( id: $access_permission->id, - user_id: $access_permission->user_id, - username: $access_permission->user->name, + user_id: $access_permission->user?->id, + username: $access_permission->user?->name, + user_group_id: $access_permission->user_group?->id, + user_group_name: $access_permission->user_group?->name, album_title: $access_permission->album->title, album_id: $access_permission->base_album_id, grants_full_photo_access: $access_permission->grants_full_photo_access, diff --git a/app/Http/Resources/Rights/SettingsRightsResource.php b/app/Http/Resources/Rights/SettingsRightsResource.php index c225768c65d..ff69a323347 100644 --- a/app/Http/Resources/Rights/SettingsRightsResource.php +++ b/app/Http/Resources/Rights/SettingsRightsResource.php @@ -34,6 +34,6 @@ public function __construct() $this->can_see_diagnostics = Gate::check(SettingsPolicy::CAN_SEE_DIAGNOSTICS, [Configs::class]); $this->can_update = Gate::check(SettingsPolicy::CAN_UPDATE, [Configs::class]); $this->can_access_dev_tools = Gate::check(SettingsPolicy::CAN_ACCESS_DEV_TOOLS, [Configs::class]); - $this->can_acess_user_groups = resolve(Verify::class)->check() && Gate::check(UserGroupPolicy::CAN_LIST, [UserGroup::class]) && config('features.user-groups') === true; + $this->can_acess_user_groups = resolve(Verify::class)->check() && Gate::check(UserGroupPolicy::CAN_LIST, [UserGroup::class]); } } diff --git a/app/Models/AccessPermission.php b/app/Models/AccessPermission.php index 856b7248562..ddb74c11396 100644 --- a/app/Models/AccessPermission.php +++ b/app/Models/AccessPermission.php @@ -73,6 +73,7 @@ class AccessPermission extends Model 'created_at' => 'datetime', 'updated_at' => 'datetime', APC::USER_ID => 'integer', + APC::USER_GROUP_ID => 'integer', APC::IS_LINK_REQUIRED => 'boolean', APC::GRANTS_FULL_PHOTO_ACCESS => 'boolean', APC::GRANTS_DOWNLOAD => 'boolean', @@ -86,6 +87,7 @@ class AccessPermission extends Model */ protected $fillable = [ APC::USER_ID, + APC::USER_GROUP_ID, APC::BASE_ALBUM_ID, APC::IS_LINK_REQUIRED, APC::GRANTS_FULL_PHOTO_ACCESS, diff --git a/app/Models/User.php b/app/Models/User.php index 3978e3522b4..e6ad8655a68 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -120,6 +120,14 @@ class User extends Authenticatable implements WebAuthnAuthenticatable protected $hidden = []; + /** + * We always want to load the user groups when loading a user. + * So that we can use the groups to determine the permissions without having to do intersection in the db. + * + * Furthermore, that way it is also provided when using Auth::user() + * + * @var list + */ protected $with = [ 'user_groups', ]; diff --git a/config/features.php b/config/features.php index 58b836518d6..5291d0e8fbe 100644 --- a/config/features.php +++ b/config/features.php @@ -84,16 +84,6 @@ */ 'require-content-type' => (bool) env('REQUIRE_CONTENT_TYPE_ENABLED', true), - /* - |-------------------------------------------------------------------------- - | Require the API requests to have the header "content-type: application/json" - | or "content-type: multipart/form-data" depending on the type. - | - | Note that this prevents the use of the API from the API documentation page. - |-------------------------------------------------------------------------- - */ - 'user-groups' => (bool) env('USER_GROUPS_ENABLED', false), - /* |-------------------------------------------------------------------------- | Vite http proxy diff --git a/resources/js/components/forms/album/AlbumCreateShareDialog.vue b/resources/js/components/forms/album/AlbumCreateShareDialog.vue index fca935458b6..adae4def405 100644 --- a/resources/js/components/forms/album/AlbumCreateShareDialog.vue +++ b/resources/js/components/forms/album/AlbumCreateShareDialog.vue @@ -17,11 +17,14 @@
- {{ newShareUser.username }} + + + {{ newShareUser.name }} +
- +
@@ -57,16 +60,12 @@ import { ref } from "vue"; import SearchTargetUser from "./SearchTargetUser.vue"; import Checkbox from "primevue/checkbox"; import Button from "primevue/button"; +import { UserOrGroup, UserOrGroupId } from "@/composables/search/searchUserGroupComputed"; -const props = withDefaults( - defineProps<{ - album: App.Http.Resources.Models.AlbumResource | App.Http.Resources.Models.TagAlbumResource; - filteredUsersIds?: number[]; - }>(), - { - filteredUsersIds: () => [], - }, -); +const props = defineProps<{ + album: App.Http.Resources.Models.AlbumResource | App.Http.Resources.Models.TagAlbumResource; + filteredUsersIds?: UserOrGroupId[]; +}>(); const visible = defineModel("visible", { type: Boolean, required: true }); @@ -75,7 +74,7 @@ const emits = defineEmits<{ createdPermission: []; }>(); -const newShareUser = ref(undefined); +const newShareUser = ref(undefined); const grantsFullPhotoAccess = ref(false); const grantsDownload = ref(false); const grantsUpload = ref(false); @@ -83,7 +82,7 @@ const grantsEdit = ref(false); const grantsDelete = ref(false); const grantsReadAccess = ref(true); -function selectUser(target: App.Http.Resources.Models.LightUserResource) { +function selectUser(target: UserOrGroup) { newShareUser.value = target; } @@ -102,7 +101,8 @@ function create() { } const data = { album_ids: [props.album.id], - user_ids: [newShareUser.value.id], + user_ids: [] as number[], + group_ids: [] as number[], grants_download: grantsDownload.value, grants_full_photo_access: grantsFullPhotoAccess.value, grants_upload: grantsUpload.value, @@ -110,6 +110,12 @@ function create() { grants_delete: grantsDelete.value, }; + if (newShareUser.value.type === "group") { + data.group_ids = [newShareUser.value.id]; + } else { + data.user_ids = [newShareUser.value.id]; + } + SharingService.add(data).then(() => { toast.add({ severity: "success", summary: trans("toasts.success"), detail: trans("sharing.permission_created"), life: 3000 }); visible.value = false; diff --git a/resources/js/components/forms/album/AlbumShare.vue b/resources/js/components/forms/album/AlbumShare.vue index 23b5a7fa419..0c3960cfcb6 100644 --- a/resources/js/components/forms/album/AlbumShare.vue +++ b/resources/js/components/forms/album/AlbumShare.vue @@ -56,6 +56,7 @@ import AlbumCreateShareDialog from "./AlbumCreateShareDialog.vue"; import Button from "primevue/button"; import ProgressSpinner from "primevue/progressspinner"; import ConfirmSharingDialog from "./ConfirmSharingDialog.vue"; +import { UserOrGroupId } from "@/composables/search/searchUserGroupComputed"; const props = defineProps<{ album: App.Http.Resources.Models.AlbumResource | App.Http.Resources.Models.TagAlbumResource; @@ -74,11 +75,22 @@ function load() { }); } -const sharedUserIds = computed((): number[] => { +const sharedUserIds = computed((): UserOrGroupId[] => { if (perms.value === undefined) { return []; } - return perms.value.map((perm) => perm.user_id) as number[]; + return perms.value.map((perm) => { + if (perm.user_group_id !== null) { + return { + id: perm.user_group_id, + type: "group", + }; + } + return { + id: perm.user_id, + type: "user", + }; + }) as UserOrGroupId[]; }); function deletePermission(id: number) { diff --git a/resources/js/components/forms/album/AlbumTransfer.vue b/resources/js/components/forms/album/AlbumTransfer.vue index bf30d7dea9c..06b88f5b097 100644 --- a/resources/js/components/forms/album/AlbumTransfer.vue +++ b/resources/js/components/forms/album/AlbumTransfer.vue @@ -3,7 +3,7 @@ diff --git a/resources/js/components/forms/sharing/BulkSharingModal.vue b/resources/js/components/forms/sharing/BulkSharingModal.vue index 071e664fbaf..c25250d3fd0 100644 --- a/resources/js/components/forms/sharing/BulkSharingModal.vue +++ b/resources/js/components/forms/sharing/BulkSharingModal.vue @@ -40,11 +40,10 @@ {{ $t("sharing.users") }} + @@ -83,7 +88,7 @@ $t("dialogs.button.cancel") }}