From a3ee5fd1fa9b5799e3cdc002247af838b91b42e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9ia=20Bohner?= Date: Thu, 16 Jul 2026 22:20:09 -0300 Subject: [PATCH 1/3] Restricts Library General Access settings to admin --- src/FilamentLibraryPlugin.php | 6 +- src/Policies/LibraryItemPolicy.php | 25 ++-- src/Resources/Pages/EditFile.php | 4 +- src/Resources/Pages/EditFolder.php | 4 +- src/Resources/Pages/EditLibraryItemPage.php | 5 + src/Resources/Pages/EditLink.php | 4 +- src/Services/PermissionService.php | 11 +- src/Traits/LibraryUser.php | 4 +- .../PermissionServiceGeneralAccessTest.php | 112 ++++++++++++++++++ 9 files changed, 147 insertions(+), 28 deletions(-) create mode 100644 tests/Unit/PermissionServiceGeneralAccessTest.php diff --git a/src/FilamentLibraryPlugin.php b/src/FilamentLibraryPlugin.php index 9f43841..d431687 100644 --- a/src/FilamentLibraryPlugin.php +++ b/src/FilamentLibraryPlugin.php @@ -35,9 +35,11 @@ public function getId(): string /** * Set a custom callback to determine if a user is a library admin. * - * @param callable $callback Function that receives a user and returns bool + * Pass null to clear a previously registered callback. + * + * @param (callable(Authenticatable): bool)|null $callback */ - public static function setLibraryAdminCallback(callable $callback): void + public static function setLibraryAdminCallback(?callable $callback): void { static::$libraryAdminCallback = $callback; } diff --git a/src/Policies/LibraryItemPolicy.php b/src/Policies/LibraryItemPolicy.php index e818fae..89f2d03 100644 --- a/src/Policies/LibraryItemPolicy.php +++ b/src/Policies/LibraryItemPolicy.php @@ -3,6 +3,7 @@ namespace Tapp\FilamentLibrary\Policies; use Illuminate\Foundation\Auth\User; +use Tapp\FilamentLibrary\FilamentLibraryPlugin; use Tapp\FilamentLibrary\Models\LibraryItem; class LibraryItemPolicy @@ -12,8 +13,7 @@ class LibraryItemPolicy */ public function viewAny(User $user): bool { - // Check if user is library admin - if (method_exists($user, 'isLibraryAdmin') && $user->isLibraryAdmin()) { + if (FilamentLibraryPlugin::isLibraryAdmin($user)) { return true; } @@ -26,8 +26,7 @@ public function viewAny(User $user): bool */ public function view(User $user, LibraryItem $libraryItem): bool { - // Check if user is library admin - if (method_exists($user, 'isLibraryAdmin') && $user->isLibraryAdmin()) { + if (FilamentLibraryPlugin::isLibraryAdmin($user)) { return true; } @@ -50,8 +49,7 @@ public function create(User $user): bool */ public function update(User $user, LibraryItem $libraryItem): bool { - // Check if user is library admin - if (method_exists($user, 'isLibraryAdmin') && $user->isLibraryAdmin()) { + if (FilamentLibraryPlugin::isLibraryAdmin($user)) { return true; } @@ -64,8 +62,7 @@ public function update(User $user, LibraryItem $libraryItem): bool */ public function delete(User $user, LibraryItem $libraryItem): bool { - // Check if user is library admin - if (method_exists($user, 'isLibraryAdmin') && $user->isLibraryAdmin()) { + if (FilamentLibraryPlugin::isLibraryAdmin($user)) { return true; } @@ -87,8 +84,7 @@ public function deleteAny(User $user): bool */ public function forceDelete(User $user, LibraryItem $libraryItem): bool { - // Check if user is library admin - if (method_exists($user, 'isLibraryAdmin') && $user->isLibraryAdmin()) { + if (FilamentLibraryPlugin::isLibraryAdmin($user)) { return true; } @@ -110,8 +106,7 @@ public function forceDeleteAny(User $user): bool */ public function restore(User $user, LibraryItem $libraryItem): bool { - // Check if user is library admin - if (method_exists($user, 'isLibraryAdmin') && $user->isLibraryAdmin()) { + if (FilamentLibraryPlugin::isLibraryAdmin($user)) { return true; } @@ -133,8 +128,7 @@ public function restoreAny(User $user): bool */ public function replicate(User $user, LibraryItem $libraryItem): bool { - // Check if user is library admin - if (method_exists($user, 'isLibraryAdmin') && $user->isLibraryAdmin()) { + if (FilamentLibraryPlugin::isLibraryAdmin($user)) { return true; } @@ -155,8 +149,7 @@ public function reorder(User $user): bool */ public function managePermissions(User $user, LibraryItem $libraryItem): bool { - // Check if user is library admin - if (method_exists($user, 'isLibraryAdmin') && $user->isLibraryAdmin()) { + if (FilamentLibraryPlugin::isLibraryAdmin($user)) { return true; } diff --git a/src/Resources/Pages/EditFile.php b/src/Resources/Pages/EditFile.php index fb32821..5b039d5 100644 --- a/src/Resources/Pages/EditFile.php +++ b/src/Resources/Pages/EditFile.php @@ -95,7 +95,7 @@ public function form(Schema $schema): Schema $record = $this->getRecord(); $inherited = $record->getInheritedGeneralAccessDisplay(); - $baseText = 'Set the baseline access level for this file. User-level permissions can override this setting.'; + $baseText = 'Only library admins can change general access (e.g. make an item visible to everyone). Share with specific users via User Permissions instead.'; if ($inherited) { return $baseText . "\n\nCurrently inheriting: {$inherited}"; @@ -103,7 +103,7 @@ public function form(Schema $schema): Schema return $baseText; }) - ->visible(fn () => $this->getRecord()->hasPermission(auth()->user(), 'share')), + ->visible(fn () => FilamentLibraryPlugin::isLibraryAdmin(auth()->user())), // Creator select field $this->getCreatorSelectField(), diff --git a/src/Resources/Pages/EditFolder.php b/src/Resources/Pages/EditFolder.php index 4744d67..e2442db 100644 --- a/src/Resources/Pages/EditFolder.php +++ b/src/Resources/Pages/EditFolder.php @@ -77,7 +77,7 @@ public function form(Schema $schema): Schema $record = $this->getRecord(); $inherited = $record->getInheritedGeneralAccessDisplay(); - $baseText = 'Set the baseline access level for this folder. User-level permissions can override this setting.'; + $baseText = 'Only library admins can change general access (e.g. make an item visible to everyone). Share with specific users via User Permissions instead.'; if ($inherited) { return $baseText . "\n\nCurrently inheriting: {$inherited}"; @@ -85,7 +85,7 @@ public function form(Schema $schema): Schema return $baseText; }) - ->visible(fn () => $this->getRecord()->hasPermission(auth()->user(), 'share')), + ->visible(fn () => FilamentLibraryPlugin::isLibraryAdmin(auth()->user())), // Creator select field $this->getCreatorSelectField(), diff --git a/src/Resources/Pages/EditLibraryItemPage.php b/src/Resources/Pages/EditLibraryItemPage.php index e03901b..2394693 100644 --- a/src/Resources/Pages/EditLibraryItemPage.php +++ b/src/Resources/Pages/EditLibraryItemPage.php @@ -79,6 +79,11 @@ protected function mutateFormDataBeforeSave(array $data): array // Set the updated_by field $data['updated_by'] = auth()->user()?->id; + // Only library admins may change general access (e.g. anyone_can_view). + if (! FilamentLibraryPlugin::isLibraryAdmin(auth()->user())) { + unset($data['general_access']); + } + return $data; } diff --git a/src/Resources/Pages/EditLink.php b/src/Resources/Pages/EditLink.php index f6ecf1c..253cb76 100644 --- a/src/Resources/Pages/EditLink.php +++ b/src/Resources/Pages/EditLink.php @@ -82,7 +82,7 @@ public function form(Schema $schema): Schema $record = $this->getRecord(); $inherited = $record->getInheritedGeneralAccessDisplay(); - $baseText = 'Set the baseline access level for this link. User-level permissions can override this setting.'; + $baseText = 'Only library admins can change general access (e.g. make an item visible to everyone). Share with specific users via User Permissions instead.'; if ($inherited) { return $baseText . "\n\nCurrently inheriting: {$inherited}"; @@ -90,7 +90,7 @@ public function form(Schema $schema): Schema return $baseText; }) - ->visible(fn () => $this->getRecord()->hasPermission(auth()->user(), 'share')), + ->visible(fn () => FilamentLibraryPlugin::isLibraryAdmin(auth()->user())), // Creator select field $this->getCreatorSelectField(), diff --git a/src/Services/PermissionService.php b/src/Services/PermissionService.php index 0708ff2..e86258a 100644 --- a/src/Services/PermissionService.php +++ b/src/Services/PermissionService.php @@ -90,11 +90,16 @@ public function bulkAssignPermissions($items, array $data): void { $userIds = $data['user_ids'] ?? []; $permission = $data['permission'] ?? 'view'; - $generalAccess = $data['general_access'] ?? 'private'; + $canManageGeneralAccess = FilamentLibraryPlugin::isLibraryAdmin(auth()->user()); + $generalAccess = $canManageGeneralAccess && array_key_exists('general_access', $data) + ? $data['general_access'] + : null; foreach ($items as $item) { - // Update the general access level for the item - $item->update(['general_access' => $generalAccess]); + // Only library admins may change general access (e.g. anyone_can_view). + if ($generalAccess !== null) { + $item->update(['general_access' => $generalAccess]); + } // Assign permissions to users foreach ($userIds as $userId) { diff --git a/src/Traits/LibraryUser.php b/src/Traits/LibraryUser.php index f0f24bb..325d372 100644 --- a/src/Traits/LibraryUser.php +++ b/src/Traits/LibraryUser.php @@ -44,9 +44,11 @@ public function favoriteLibraryItems(): BelongsToMany * - Edit any library item * - Delete any library item * - Manage permissions on any item + * - Change general access (e.g. make items visible to everyone) * - Access all library functionality * - * Override this method to add role-based logic. + * Prefer FilamentLibraryPlugin::isLibraryAdmin() / setLibraryAdminCallback() for + * authorization. Override this method when host-app code calls it directly. */ public function isLibraryAdmin(): bool { diff --git a/tests/Unit/PermissionServiceGeneralAccessTest.php b/tests/Unit/PermissionServiceGeneralAccessTest.php new file mode 100644 index 0000000..64f3f78 --- /dev/null +++ b/tests/Unit/PermissionServiceGeneralAccessTest.php @@ -0,0 +1,112 @@ +insertGetId([ + 'name' => 'Test User', + 'email' => $email, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $user = new LibraryPermissionTestUser; + $user->id = $id; + $user->name = 'Test User'; + $user->email = $email; + $user->exists = true; + + return $user; +} + +function createPrivateLibraryItem(int $createdBy): LibraryItem +{ + return LibraryItem::query()->create([ + 'name' => 'Shared doc', + 'slug' => 'shared-doc-' . uniqid('', true), + 'type' => 'file', + 'created_by' => $createdBy, + 'updated_by' => $createdBy, + 'general_access' => 'private', + ]); +} + +test('bulk assign permissions does not change general access for non-admins', function (): void { + config()->set('auth.providers.users.model', LibraryPermissionTestUser::class); + + $owner = createLibraryTestUser('owner@example.com'); + $viewer = createLibraryTestUser('viewer@example.com'); + $item = createPrivateLibraryItem($owner->id); + + Auth::login($owner); + FilamentLibraryPlugin::setLibraryAdminCallback(fn ($user): bool => false); + + app(PermissionService::class)->bulkAssignPermissions([$item], [ + 'user_ids' => [$viewer->id], + 'permission' => 'view', + 'general_access' => 'anyone_can_view', + ]); + + expect($item->fresh()->general_access)->toBe('private') + ->and($item->fresh()->permissions()->where('user_id', $viewer->id)->exists())->toBeTrue(); +}); + +test('bulk assign permissions allows library admins to change general access', function (): void { + config()->set('auth.providers.users.model', LibraryPermissionTestUser::class); + + $admin = createLibraryTestUser('admin@example.com'); + $viewer = createLibraryTestUser('viewer-admin@example.com'); + $item = createPrivateLibraryItem($admin->id); + + Auth::login($admin); + FilamentLibraryPlugin::setLibraryAdminCallback( + fn ($user): bool => $user && (int) $user->id === (int) $admin->id + ); + + app(PermissionService::class)->bulkAssignPermissions([$item], [ + 'user_ids' => [$viewer->id], + 'permission' => 'view', + 'general_access' => 'anyone_can_view', + ]); + + expect($item->fresh()->general_access)->toBe('anyone_can_view') + ->and($item->fresh()->permissions()->where('user_id', $viewer->id)->exists())->toBeTrue(); +}); + +test('bulk assign permissions skips general access update when key is omitted', function (): void { + config()->set('auth.providers.users.model', LibraryPermissionTestUser::class); + + $admin = createLibraryTestUser('admin-omit@example.com'); + $viewer = createLibraryTestUser('viewer-omit@example.com'); + $item = createPrivateLibraryItem($admin->id); + + Auth::login($admin); + FilamentLibraryPlugin::setLibraryAdminCallback( + fn ($user): bool => $user && (int) $user->id === (int) $admin->id + ); + + app(PermissionService::class)->bulkAssignPermissions([$item], [ + 'user_ids' => [$viewer->id], + 'permission' => 'view', + ]); + + expect($item->fresh()->general_access)->toBe('private'); +}); + +final class LibraryPermissionTestUser extends Authenticatable +{ + protected $table = 'users'; + + protected $guarded = []; +} From 71477be10cc95ebd29ac8efb018a33bbb1cb2f5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9ia=20Bohner?= Date: Thu, 16 Jul 2026 22:25:04 -0300 Subject: [PATCH 2/3] PHPstan --- phpstan-baseline.neon | 4 ---- src/Support/UserAssignmentFilters.php | 4 +++- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 13b857e..6d1df1e 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -93,10 +93,6 @@ parameters: message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:getInheritedGeneralAccessDisplay\\(\\)\\.$#" path: src/Resources/Pages - - - message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:hasPermission\\(\\)\\.$#" - path: src/Resources/Pages - - message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:getSecureUrl\\(\\)\\.$#" path: src/Resources/Pages diff --git a/src/Support/UserAssignmentFilters.php b/src/Support/UserAssignmentFilters.php index 1427421..1054255 100644 --- a/src/Support/UserAssignmentFilters.php +++ b/src/Support/UserAssignmentFilters.php @@ -41,7 +41,9 @@ public static function filterFields(string $usersField = 'user_ids'): array ->map(function (Field $field) use ($usersField): Field { return $field ->live() - ->afterStateUpdated(fn (Get $get, Set $set): mixed => static::refreshSelectedUsersIfSelectingAll($get, $set, $usersField)); + ->afterStateUpdated(function (Get $get, Set $set) use ($usersField): void { + static::refreshSelectedUsersIfSelectingAll($get, $set, $usersField); + }); }) ->all(); } From e0d58d368cb4a77bbb51408570d37358133fb82b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9ia=20Bohner?= Date: Tue, 21 Jul 2026 21:33:53 -0300 Subject: [PATCH 3/3] Remove isLibraryAdmin() and update docs --- PERMISSIONS.md | 40 ++++++++++++++++++++++++++------------ README.md | 2 -- src/Traits/LibraryUser.php | 21 -------------------- 3 files changed, 28 insertions(+), 35 deletions(-) diff --git a/PERMISSIONS.md b/PERMISSIONS.md index 30ffead..1168b9a 100644 --- a/PERMISSIONS.md +++ b/PERMISSIONS.md @@ -23,6 +23,8 @@ You can assign permissions to users in several ways: 4. Choose users and permission level (view/edit) 5. Optionally cascade permissions to child items +**General Access** (`private` / `inherit` / `anyone_can_view`) can only be changed by library admins. Owners and creators can still share items with specific users via User Permissions. + #### Via Code ```php use Tapp\FilamentLibrary\Services\PermissionService; @@ -59,23 +61,37 @@ if ($libraryItem->hasPermission($user, 'edit')) { ## Advanced Configuration -### Custom User Model Integration +### Library Admin Access + +Library admin checks go through `FilamentLibraryPlugin::isLibraryAdmin()`. Configure them with a callback (recommended) or via config: + +```php +// In your AppServiceProvider +use Tapp\FilamentLibrary\FilamentLibraryPlugin; + +public function boot(): void +{ + FilamentLibraryPlugin::setLibraryAdminCallback(function ($user): bool { + return $user->hasRole('admin') || $user->hasRole('library-admin'); + }); +} +``` + +Or in `config/filament-library.php`: + +```php +'admin_role' => 'Admin', +'admin_callback' => null, // or a callable +``` -If you want to use the `HasLibraryAccess` trait for additional functionality: +Add the `LibraryUser` trait to your User model for personal folders and favorites (it does not control admin authorization): ```php -// In your User model -use Tapp\FilamentLibrary\Traits\HasLibraryAccess; +use Tapp\FilamentLibrary\Traits\LibraryUser; class User extends Authenticatable { - use HasLibraryAccess; - - // Override to add role-based logic - public function isLibraryAdmin(): bool - { - return $this->hasRole('admin') || $this->hasRole('library-admin'); - } + use LibraryUser; } ``` @@ -124,7 +140,7 @@ The library items table includes a toggleable "Permissions" column that shows: - All permission checks go through Laravel's authorization system - Policies ensure consistent permission enforcement -- Fallback implementations work even without the `HasLibraryAccess` trait +- Fallback implementations work even without the `LibraryUser` trait - Creator permissions are always respected ## Troubleshooting diff --git a/README.md b/README.md index 7127d5d..cb5d1cf 100644 --- a/README.md +++ b/README.md @@ -204,8 +204,6 @@ public function boot() } ``` -**Note:** By default, users have an `isLibraryAdmin()` method that returns `false`. You can override this in your User model for custom logic. - ## Events The package dispatches events your application can listen for to extend behavior (for example, search indexing, webhooks after uploads, ...). diff --git a/src/Traits/LibraryUser.php b/src/Traits/LibraryUser.php index 325d372..eea46d4 100644 --- a/src/Traits/LibraryUser.php +++ b/src/Traits/LibraryUser.php @@ -35,25 +35,4 @@ public function favoriteLibraryItems(): BelongsToMany return $this->belongsToMany(FilamentLibraryPlugin::libraryItemModelClass(), 'library_item_favorites') ->withTimestamps(); } - - /** - * Check if the user is a library admin. - * - * Library admins can: - * - View all library items (including root items) - * - Edit any library item - * - Delete any library item - * - Manage permissions on any item - * - Change general access (e.g. make items visible to everyone) - * - Access all library functionality - * - * Prefer FilamentLibraryPlugin::isLibraryAdmin() / setLibraryAdminCallback() for - * authorization. Override this method when host-app code calls it directly. - */ - public function isLibraryAdmin(): bool - { - // Default implementation - override this method to add role-based logic - // For example: return $this->hasRole('admin') || $this->hasRole('library-admin'); - return false; - } }