Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('library_item_role_permissions', function (Blueprint $table) {
$table->id();
$table->foreignId('library_item_id')->constrained()->cascadeOnDelete();
$table->string('role_name');
$table->timestamps();

$table->unique(['library_item_id', 'role_name'], 'library_item_role_perms_unique');
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('library_item_role_permissions');
}
};
75 changes: 75 additions & 0 deletions src/FilamentLibraryPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ class FilamentLibraryPlugin implements Plugin
{
protected static $libraryAdminCallback = null;

protected static $roleCheckerCallback = null;

protected static $availableRolesCallback = null;

public function getId(): string
{
return 'filament-library';
Expand Down Expand Up @@ -56,6 +60,77 @@ public static function isLibraryAdmin($user): bool
return false;
}

/**
* Set a custom callback to check if a user has a specific role.
*
* @param callable $callback Function that receives a user and role name, returns bool
*/
public static function setRoleCheckerCallback(callable $callback): void
{
static::$roleCheckerCallback = $callback;
}

/**
* Check if a user has a specific role.
*
* @param \Illuminate\Contracts\Auth\Authenticatable|null $user
*/
public static function hasRole($user, string $roleName): bool
{
if (! $user) {
return false;
}

// Use custom callback if set
if (static::$roleCheckerCallback) {
return call_user_func(static::$roleCheckerCallback, $user, $roleName);
}

// Check for config-based callback
$configCallback = config('filament-library.role_checker_callback');
if ($configCallback && is_callable($configCallback)) {
return call_user_func($configCallback, $user, $roleName);
}

// Default implementation - check if user has hasRole method
if (method_exists($user, 'hasRole')) {
return $user->hasRole($roleName);
}

return false;
}

/**
* Set a custom callback to get available roles for selection.
*
* @param callable $callback Function that returns an array of role names => labels
*/
public static function setAvailableRolesCallback(callable $callback): void
{
static::$availableRolesCallback = $callback;
}

/**
* Get available roles for selection.
*
* @return array<string, string> Array of role names => labels
*/
public static function getAvailableRoles(): array
{
// Use custom callback if set
if (static::$availableRolesCallback) {
return call_user_func(static::$availableRolesCallback);
}

// Check for config-based callback
$configCallback = config('filament-library.available_roles_callback');
if ($configCallback && is_callable($configCallback)) {
return call_user_func($configCallback);
}

return [];
}

public function register(Panel $panel): void
{
$panel
Expand Down
1 change: 1 addition & 0 deletions src/FilamentLibraryServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ protected function getMigrations(): array
'2024_01_01_000001_create_library_item_permissions_table',
'2024_01_01_000002_create_library_item_tags_table',
'2024_01_01_000003_create_library_item_favorites_table',
'2024_01_01_000004_create_library_item_role_permissions_table',
];
}
}
78 changes: 78 additions & 0 deletions src/Models/LibraryItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,14 @@ public function permissions(): HasMany
return $this->hasMany(LibraryItemPermission::class);
}

/**
* Get the role-based permissions for this item.
*/
public function rolePermissions(): HasMany
{
return $this->hasMany(LibraryItemRolePermission::class);
}

/**
* Scope to get only folders.
*/
Expand Down Expand Up @@ -174,6 +182,66 @@ public function scopeForUser($query, $user)
});
}

/**
* Check if this item (or its parent folders) has role-based restrictions.
*/
public function hasRoleRestrictions(): bool
{
// Check if this item has role restrictions
if ($this->rolePermissions()->exists()) {
return true;
}

// Check parent folders recursively
if ($this->parent_id) {
return $this->parent->hasRoleRestrictions();
}

return false;
}

/**
* Get all required role names for this item (including inherited from parents).
*/
public function getRequiredRoleNames(): array
{
$roleNames = $this->rolePermissions()->pluck('role_name')->toArray();

// Check parent folders recursively
if ($this->parent_id) {
$parentRoles = $this->parent->getRequiredRoleNames();
$roleNames = array_merge($roleNames, $parentRoles);
}

return array_unique($roleNames);
}

/**
* Check if a user has access based on role restrictions.
*/
public function hasRoleAccess($user): bool
{
if (! $user) {
return false;
}

$requiredRoles = $this->getRequiredRoleNames();

// If no role restrictions, allow access (will be checked by normal permissions)
if (empty($requiredRoles)) {
return true;
}

// User must have at least one of the required roles
foreach ($requiredRoles as $roleName) {
if (\Tapp\FilamentLibrary\FilamentLibraryPlugin::hasRole($user, $roleName)) {
return true;
}
}

return false;
}

/**
* Get the effective role for a user on this item.
*/
Expand All @@ -183,6 +251,11 @@ public function getEffectiveRole($user): ?string
return null;
}

// Check role-based restrictions first
if ($this->hasRoleRestrictions() && ! $this->hasRoleAccess($user)) {
return null; // Denied by role restrictions
}

// Check if user is the creator (always has access)
if ($this->created_by === $user->id) {
// Creator always has access, but check if they're also the owner
Expand Down Expand Up @@ -392,6 +465,11 @@ public function hasPermission($user, string $permission): bool
return true;
}

// Check role-based restrictions first
if ($this->hasRoleRestrictions() && ! $this->hasRoleAccess($user)) {
return false; // Denied by role restrictions
}

$effectiveRole = $this->getEffectiveRole($user);

if (! $effectiveRole) {
Expand Down
32 changes: 32 additions & 0 deletions src/Models/LibraryItemRolePermission.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace Tapp\FilamentLibrary\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class LibraryItemRolePermission extends Model
{
use HasFactory;

protected $table = 'library_item_role_permissions';

protected $fillable = [
'library_item_id',
'role_name',
];

protected $casts = [
'created_at' => 'datetime',
'updated_at' => 'datetime',
];

/**
* Get the library item this role permission belongs to.
*/
public function libraryItem(): BelongsTo
{
return $this->belongsTo(LibraryItem::class);
}
}
37 changes: 37 additions & 0 deletions src/Resources/Pages/CreateFolder.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

namespace Tapp\FilamentLibrary\Resources\Pages;

use Filament\Forms\Components\Select;
use Filament\Resources\Pages\CreateRecord;
use Tapp\FilamentLibrary\FilamentLibraryPlugin;
use Tapp\FilamentLibrary\Resources\LibraryItemResource;
use Tapp\FilamentLibrary\Traits\HasParentFolder;

Expand All @@ -12,6 +14,24 @@ class CreateFolder extends CreateRecord

protected static string $resource = LibraryItemResource::class;

public function form(\Filament\Schemas\Schema $schema): \Filament\Schemas\Schema
{
return $schema
->schema([
...LibraryItemResource::folderForm($schema)->getComponents(),
Select::make('role_permissions')
->label('Required Roles')
->helperText('Only users with at least one of these roles can access this folder. Leave empty to allow all users (subject to other permissions).')
->options(function () {
return FilamentLibraryPlugin::getAvailableRoles();
})
->multiple()
->searchable()
->dehydrated(false) // Don't save to model directly, we handle it in afterCreate
->visible(fn () => auth()->user() && \Tapp\FilamentLibrary\FilamentLibraryPlugin::isLibraryAdmin(auth()->user())),
]);
}

protected function mutateFormDataBeforeCreate(array $data): array
{
$data['type'] = 'folder';
Expand All @@ -20,4 +40,21 @@ protected function mutateFormDataBeforeCreate(array $data): array

return $data;
}

protected function afterCreate(): void
{
parent::afterCreate();

$record = $this->getRecord();
$rolePermissions = $this->form->getState()['role_permissions'] ?? [];

// Sync role permissions
if (is_array($rolePermissions) && ! empty($rolePermissions)) {
foreach ($rolePermissions as $roleName) {
$record->rolePermissions()->create([
'role_name' => $roleName,
]);
}
}
}
}
42 changes: 42 additions & 0 deletions src/Resources/Pages/EditFolder.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Tapp\FilamentLibrary\Resources\Pages;

use Filament\Forms\Components\Select;
use Tapp\FilamentLibrary\FilamentLibraryPlugin;
use Tapp\FilamentLibrary\Resources\LibraryItemResource;

class EditFolder extends EditLibraryItemPage
Expand Down Expand Up @@ -58,8 +59,49 @@ public function form(\Filament\Schemas\Schema $schema): \Filament\Schemas\Schema
})
->visible(fn () => $this->getRecord()->hasPermission(auth()->user(), 'share')),

Select::make('role_permissions')
->label('Required Roles')
->helperText('Only users with at least one of these roles can access this folder. Leave empty to allow all users (subject to other permissions).')
->options(function () {
return FilamentLibraryPlugin::getAvailableRoles();
})
->multiple()
->searchable()
->default(function () {
return $this->getRecord()->rolePermissions()->pluck('role_name')->toArray();
})
->dehydrated(false) // Don't save to model directly, we handle it in mutateFormDataBeforeSave
->visible(fn () => $this->getRecord()->hasPermission(auth()->user(), 'share')),

// Creator select field
$this->getCreatorSelectField(),
]);
}

protected function mutateFormDataBeforeSave(array $data): array
{
// Handle role permissions separately
$rolePermissions = $data['role_permissions'] ?? [];
unset($data['role_permissions']);

return $data;
}

protected function afterSave(): void
{
parent::afterSave();

$record = $this->getRecord();
$rolePermissions = $this->form->getState()['role_permissions'] ?? [];

// Sync role permissions
$record->rolePermissions()->delete();
if (is_array($rolePermissions) && ! empty($rolePermissions)) {
foreach ($rolePermissions as $roleName) {
$record->rolePermissions()->create([
'role_name' => $roleName,
]);
}
}
}
}