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
This commit is contained in:
= 2026-05-27 10:43:44 +00:00
parent 4849736c60
commit a37b6a91be
6 changed files with 92 additions and 6 deletions

View File

@ -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
);
}
}

View File

@ -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 = '';
}

View File

@ -18,7 +18,7 @@
*
* @method static Builder<Role> visible()
*/
#[Fillable(['priority'])]
#[Fillable(['name', 'guard_name', 'priority'])]
class Role extends SpatieRole
{
use LogsActivity;

View File

@ -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)

View File

@ -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)
<div class="mt-5 border-t border-base-300 pt-4">
<h3 class="font-semibold text-sm text-sky-400 mb-3">SAML SSO Settings</h3>
<x-mary-input
wire:model="form.immutableId"
:label="__('SAML Immutable ID')"
placeholder="e.g. user-123-immutable (leave blank to auto-generate)"
/>
</div>
@endif
@if ($roleWithPermission)
<fieldset class="mt-5 space-y-4 w-full">
<legend class="flex w-full items-center justify-between gap-4">

View File

@ -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);
});