Fix: Associate App Permission with Roles

- 'Use' permission of apps now synced with Role
- Added `PermissionData` DTO for standardized permission transformation.
- Expanded `AppsPermissionService` with `getAllPermissions` method for retrieving connected app permissions.
- Updated `AppRoleService` with methods for assigning and revoking app permissions to roles (`assignAppPermissionsRole`, `removeAppPermissionsRole`).
- Introduced `AppPermission::type` method for extracting permission type from names.
- Refined role and app interaction logic, including proper slug generation and app-role association.
This commit is contained in:
= 2026-05-20 09:06:55 +00:00
parent 5a593ffdb5
commit 562b4a220d
7 changed files with 106 additions and 8 deletions

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Data\Permissions;
use App\Enums\ConnectedAppPermissonEnum;
use App\Helpers\AppPermission;
use Spatie\LaravelData\Data;
use Spatie\Permission\Models\Permission;
class PermissionData extends Data
{
public function __construct(
public int $id,
public string $name,
public ConnectedAppPermissonEnum $type,
) {}
public static function fromModel(Permission $permission): self
{
return new self(
id: $permission->id,
name: $permission->name,
type: AppPermission::type($permission->name),
);
}
}

View File

@ -18,4 +18,14 @@ public static function name(ConnectedAppPermissonEnum $permission, string|Connec
return "{$permission->value}-{$slug}"; return "{$permission->value}-{$slug}";
} }
/**
* @param string $permissionName This should be the name constructed by the {@see self::name()} method.
*/
public static function type(string $permissionName): ConnectedAppPermissonEnum
{
$permission = explode('-', $permissionName);
return ConnectedAppPermissonEnum::from($permission[0]);
}
} }

View File

@ -13,8 +13,13 @@
use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\DataCollection;
use Throwable; use Throwable;
final class AppRoleService final readonly class AppRoleService
{ {
public function __construct(
private ConnectedAppService $connectedAppService,
private AppsPermissionService $appsPermissionService,
) {}
/** /**
* Assign connected apps to a role, updating the duration if already assigned. * Assign connected apps to a role, updating the duration if already assigned.
* *
@ -25,6 +30,9 @@ public function assign(AssignAppsToRoleData $data): void
if (empty($data->appIds)) { if (empty($data->appIds)) {
return; return;
} }
$role = Role::query()->findOrFail($data->roleId);
$this->assignAppPermissionsRole($role, $data->appIds);
$records = array_map(fn (int $appId) => [ $records = array_map(fn (int $appId) => [
'role_id' => $data->roleId, 'role_id' => $data->roleId,
@ -37,6 +45,21 @@ public function assign(AssignAppsToRoleData $data): void
['role_id', 'app_id'], ['role_id', 'app_id'],
['duration'] ['duration']
); );
}
/**
* @param array<int, int> $appIds Apps, of which all permissions will be assigned to the role.
*/
private function assignAppPermissionsRole(Role $role, array $appIds): void
{
array_map(function ($appId) use ($role): void {
$app = $this->connectedAppService->getConnectedApp($appId);
$permissions = $this->appsPermissionService->getAllPermissions($app);
$permissions
->toCollection()
->map(fn ($permission) => $role->givePermissionTo($permission->id));
}, $appIds);
} }
/** /**
@ -52,10 +75,27 @@ public function detach(int $id): void
} }
DB::transaction(function () use ($id): void { DB::transaction(function () use ($id): void {
AppRole::query()->findOrFail($id)->delete(); $appRole = AppRole::query()->findOrFail($id);
$role = Role::query()->findOrFail($appRole->role_id);
$this->removeAppPermissionsRole($role, $appRole->app_id);
$appRole->delete();
}); });
} }
/**
* @param array<int,int>|int $appIds
*/
private function removeAppPermissionsRole(Role $role, ...$appIds): void
{
array_map(function ($appId) use ($role): void {
$app = $this->connectedAppService->getConnectedApp($appId);
$permissions = $this->appsPermissionService->getAllPermissions($app);
$permissions
->toCollection()
->map(fn ($permission) => $role->revokePermissionTo($permission->name));
}, $appIds);
}
/** /**
* Get all apps assigned to a given role. * Get all apps assigned to a given role.
* *
@ -74,7 +114,7 @@ public function getAppsForRole(int $roleId): DataCollection
$assignments->map(fn (AppRole $row): array => [ $assignments->map(fn (AppRole $row): array => [
'id' => $row->id, 'id' => $row->id,
'appId' => $row->app_id, 'appId' => $row->app_id,
'appName' => $row->app->name, 'appName' => $row->app->name ?? '',
'duration' => $row->duration, 'duration' => $row->duration,
])->all(), ])->all(),
DataCollection::class, DataCollection::class,

View File

@ -5,10 +5,12 @@
namespace App\Services; namespace App\Services;
use App\Data\ConnectedApp\ConnectedAppData; use App\Data\ConnectedApp\ConnectedAppData;
use App\Data\Permissions\PermissionData;
use App\Enums\ConnectedAppPermissonEnum; use App\Enums\ConnectedAppPermissonEnum;
use App\Helpers\AppPermission; use App\Helpers\AppPermission;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Spatie\LaravelData\DataCollection;
use Spatie\Permission\Models\Permission; use Spatie\Permission\Models\Permission;
use Throwable; use Throwable;
@ -29,4 +31,20 @@ public function add(ConnectedAppData $connectedApp, ConnectedAppPermissonEnum $p
Permission::firstOrCreate(['name' => $permissionName]); Permission::firstOrCreate(['name' => $permissionName]);
}); });
} }
/**
* Returns all permissions for the connected app.
*
* @return DataCollection<int, PermissionData>
*/
public function getAllPermissions(ConnectedAppData $connectedApp): DataCollection
{
if (! $connectedApp->slug) {
$connectedApp->slug = Str::slug($connectedApp->name);
}
$permissionName = AppPermission::name(ConnectedAppPermissonEnum::Use, $connectedApp);
$permissions = Permission::where('name', 'like', $permissionName)->get();
return PermissionData::collect($permissions, DataCollection::class);
}
} }

View File

@ -22,6 +22,7 @@ public function __construct(
/** /**
* Create a new connected app. * Create a new connected app.
* By Default creates a use permission for the app.
* *
* @throws Throwable when an error occurs during the creation. * @throws Throwable when an error occurs during the creation.
*/ */

View File

@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
use app\Models\Role; use App\Models\Role;
use Spatie\Permission\DefaultTeamResolver; use Spatie\Permission\DefaultTeamResolver;
use Spatie\Permission\Models\Permission; use Spatie\Permission\Models\Permission;

View File

@ -64,7 +64,7 @@ public function mount(int $id): void
$this->roleName = $role->name; $this->roleName = $role->name;
}, },
onError: function (): void { onError: function (): void {
$this->redirectRoute('access-manager.roles-and-apps', navigate: true); $this->redirectRoute('access-manager.roles-and-apps.index', navigate: true);
}, },
showSuccess: false, showSuccess: false,
); );
@ -192,11 +192,11 @@ public function updateApp(): void
); );
} }
public function detachApp(int $assignmentId): void public function detachApp(int $appId): void
{ {
$this->attempt( $this->attempt(
action: function () use ($assignmentId): void { action: function () use ($appId): void {
$this->appRoleService->detach($assignmentId); $this->appRoleService->detach($appId);
unset($this->apps); unset($this->apps);
}, },
successMessage: 'App removed from role.', successMessage: 'App removed from role.',