From a37b6a91be10ed9a1801447e98c8035189b55749 Mon Sep 17 00:00:00 2001 From: = Date: Wed, 27 May 2026 10:43:44 +0000 Subject: [PATCH] Feat: Show Immutable Id field if user has a SAML App - added a ImmuatableId field in manage roles - Generate ImmutableId using user Id if not available --- app/Data/User/UserIndexData.php | 4 +- app/Livewire/Forms/AssignRolesForm.php | 6 +++ app/Models/Role.php | 2 +- app/Services/UserService.php | 9 ++-- .../access-manager/users/⚡index.blade.php | 31 ++++++++++++- tests/Feature/SamlIdpTest.php | 46 +++++++++++++++++++ 6 files changed, 92 insertions(+), 6 deletions(-) diff --git a/app/Data/User/UserIndexData.php b/app/Data/User/UserIndexData.php index a1068db..8bf75d5 100644 --- a/app/Data/User/UserIndexData.php +++ b/app/Data/User/UserIndexData.php @@ -19,6 +19,7 @@ public function __construct( #[DataCollectionOf(class: RoleData::class)] public ?DataCollection $roles = null, public ?array $restrictedPermissionIds = null, + public ?string $immutableId = null, ) {} public static function fromModel(User $user) @@ -29,7 +30,8 @@ public static function fromModel(User $user) email: $user->email, initials: $user->initials(), roles: RoleData::collect($user->roles, DataCollection::class), - restrictedPermissionIds: $user->restrictedPermissions->pluck('id')->toArray() + restrictedPermissionIds: $user->restrictedPermissions->pluck('id')->toArray(), + immutableId: $user->immutable_id ); } } diff --git a/app/Livewire/Forms/AssignRolesForm.php b/app/Livewire/Forms/AssignRolesForm.php index b719907..05054ed 100644 --- a/app/Livewire/Forms/AssignRolesForm.php +++ b/app/Livewire/Forms/AssignRolesForm.php @@ -24,4 +24,10 @@ class AssignRolesForm extends Form */ #[Validate('nullable|array')] public array $permissions = []; + + /** + * Holds the SSO Immutable ID for the user. + */ + #[Validate('nullable|string|max:255')] + public string $immutableId = ''; } diff --git a/app/Models/Role.php b/app/Models/Role.php index 00d613b..b164ce6 100644 --- a/app/Models/Role.php +++ b/app/Models/Role.php @@ -18,7 +18,7 @@ * * @method static Builder visible() */ -#[Fillable(['priority'])] +#[Fillable(['name', 'guard_name', 'priority'])] class Role extends SpatieRole { use LogsActivity; diff --git a/app/Services/UserService.php b/app/Services/UserService.php index 4eb3367..a50ba08 100644 --- a/app/Services/UserService.php +++ b/app/Services/UserService.php @@ -54,17 +54,20 @@ public function getRestrictedPermissionIds(int $userId): array } /** - * Assign roles to a user. + * Assign roles to a user and update their SSO settings. */ - public function assignRoles(int $userId, array $roleIds): void + public function assignRoles(int $userId, array $roleIds, ?string $immutableId = null): void { if ($userId <= 0) { throw new InvalidArgumentException('Invalid id'); } - DB::transaction(function () use ($userId, $roleIds): void { + DB::transaction(function () use ($userId, $roleIds, $immutableId): void { $user = User::query()->findOrFail($userId); + $user->immutable_id = $immutableId ?: null; + $user->save(); + $userPriority = auth()->user()?->roles()->min('priority') ?? 999999; // Get user's existing roles that have priority <= $userPriority (which are forbidden to the logged in user) diff --git a/resources/views/pages/access-manager/users/⚡index.blade.php b/resources/views/pages/access-manager/users/⚡index.blade.php index 953453a..9af8dec 100644 --- a/resources/views/pages/access-manager/users/⚡index.blade.php +++ b/resources/views/pages/access-manager/users/⚡index.blade.php @@ -53,6 +53,23 @@ public function getUsers(): PaginatedDataCollection return $this->userService->getAll(); } + #[Computed] + public function showSamlSettings(): bool + { + if (empty($this->form->roleIds)) { + return false; + } + + return \App\Models\ConnectedApp::query() + ->whereHas('roles', function ($query) { + $query->whereIn('roles.id', $this->form->roleIds); + }) + ->whereHas('protocol', function ($query) { + $query->where('name', 'saml'); + }) + ->exists(); + } + public function delete(int $userId): void { $this->attempt( @@ -80,6 +97,7 @@ public function openAssignRolesModal(int $userId): void $this->form->reset(); $this->searchRoles(); $this->form->roleIds = $user->roles()->pluck('id')->toArray(); + $this->form->immutableId = $user->immutable_id ?? ''; $this->hydratePermissionInForm(); $this->showAssignRolesModal = true; }, @@ -185,7 +203,7 @@ public function saveRoles(): void action: function () { $this->form->validate(); $deactivatedPermissions = $this->getDeactivatedPermissions(); - $this->userService->assignRoles($this->currentUserId, $this->form->roleIds); + $this->userService->assignRoles($this->currentUserId, $this->form->roleIds, $this->form->immutableId); $this->userService->syncRestrictedPermissions($this->currentUserId, $deactivatedPermissions); $this->showAssignRolesModal = false; $this->roleWithPermission = null; @@ -314,6 +332,17 @@ class="btn-sm btn-circle btn-error btn-soft" label="Roles" /> + @if($this->showSamlSettings) +
+

SAML SSO Settings

+ +
+ @endif + @if ($roleWithPermission)
diff --git a/tests/Feature/SamlIdpTest.php b/tests/Feature/SamlIdpTest.php index 9d2c89e..2a51043 100644 --- a/tests/Feature/SamlIdpTest.php +++ b/tests/Feature/SamlIdpTest.php @@ -4,6 +4,7 @@ use App\Models\{ConnectedApp, ConnectionProtocol, ConnectionProvider, ConnectionStatus, User}; use Illuminate\Support\Facades\File; +use Livewire\Livewire; beforeEach(function (): void { // Generate testing keys if not already present @@ -111,3 +112,48 @@ // Verify session was cleared expect(session('saml_pending_request'))->toBeNull(); }); + +test('user sso settings and immutable id can be assigned via service and mapped to DTO', function (): void { + $userService = app(App\Services\UserService::class); + + // Assign roles and update immutable ID + $userService->assignRoles($this->user->id, [], 'new-custom-sso-immutable-id'); + + // Retrieve fresh user and assert + $freshUser = $this->user->fresh(); + expect($freshUser->immutable_id)->toBe('new-custom-sso-immutable-id'); + + // Assert UserIndexData maps it correctly + $dto = App\Data\User\UserIndexData::fromModel($freshUser); + expect($dto->immutableId)->toBe('new-custom-sso-immutable-id'); +}); + +test('saml settings field is only visible if selected roles contain a SAML app', function (): void { + // Create a role with a SAML connected app + $samlRole = App\Models\Role::findOrCreate('SAML User'); + $samlRole->update(['priority' => 10]); + $samlRole->apps()->attach($this->samlApp->id, ['duration' => 365]); + + // Create a role with no connected apps + $emptyRole = App\Models\Role::findOrCreate('Empty User'); + $emptyRole->update(['priority' => 20]); + + // Log in the administrator so they can view users + $admin = User::where('email', 'admin@example.com')->firstOrFail(); + $this->actingAs($admin); + + // 1. Livewire component with no roles selected should return false for showSamlSettings + Livewire::test('pages::access-manager.users.index') + ->set('form.roleIds', []) + ->assertSet('showSamlSettings', false); + + // 2. Livewire component with emptyRole selected should return false + Livewire::test('pages::access-manager.users.index') + ->set('form.roleIds', [$emptyRole->id]) + ->assertSet('showSamlSettings', false); + + // 3. Livewire component with samlRole selected should return true + Livewire::test('pages::access-manager.users.index') + ->set('form.roleIds', [$samlRole->id]) + ->assertSet('showSamlSettings', true); +});