= aaa258903c Feat: Add enhanced SAML configuration and attribute mapping support
- Implemented `EnumMorphsToOptionsContract` interface to enable enums with options and labels for dropdowns.
- Created `SamlNameIdFormatEnum` and `SamlNameIdSourceEnum` for standardized SAML NameID configuration.
- Added reusable `SAML Configuration` Blade component for easier integration into connected app forms.
- Enabled custom SAML Attribute mapping with dynamic add/remove functionality and validation.
- Improved `SamlIdpController` to enforce ACS URL matching and user authorization checks.
- Refactored SAML-related tests and added scenarios for role-based SAML access and custom configurations.
2026-06-08 12:56:28 +00:00

192 lines
6.0 KiB
PHP

<?php
use App\Concerns\HandlesOperations;
use App\Data\ConnectedApp\ConnectAppRequest;
use App\Data\ConnectedApp\ConnectionStatusData;
use App\Enums\ConnectionStatusEnum;
use App\Livewire\Forms\StoreConnectedAppFrom;
use App\Models\ConnectionProtocol;
use App\Models\ConnectionProvider;
use App\Models\ConnectionStatus;
use App\Services\ConnectedAppService;
use App\Services\ConnectionProtocolService;
use App\Services\ConnectionProviderService;
use App\Services\ConnectionStatusService;
use Illuminate\Support\Collection;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Lazy;
use Livewire\Component;
use Livewire\Attributes\Title;
use Mary\Traits\Toast;
use Spatie\LaravelData\DataCollection;
/**
* @property $protocols
*/
new #[Lazy]
#[Title("Edit a connected app")]
class extends Component {
use HandlesOperations;
public StoreConnectedAppFrom $form;
protected ConnectedAppService $service;
public int $recordId = 0;
public function boot(ConnectedAppService $service): void
{
$this->service = $service;
}
public function mount(int $id): void
{
$this->attempt(
action: function () use ($id) {
$record = $this->service->getConnectedApp($id);
$this->form->set($record);
$this->recordId = $id;
},
onError: function () {
$this->redirectRoute("apps.index", navigate: true);
},
showSuccess: false,
);
}
#[Computed]
public function providers()
{
return ConnectionProvider::query()->get();
}
#[Computed]
public function protocols(): Collection
{
$protocolService = app(ConnectionProtocolService::class);
return $protocolService
->getAll()
->map(function (ConnectionProtocol $protocol) {
$protocol->name = strtoupper($protocol->name);
return $protocol;
});
}
#[Computed]
public function isSaml(): bool
{
$protocol = $this->protocols()->firstWhere(
"id",
(int) $this->form->protocolId,
);
return $protocol && strtolower($protocol->name) === "saml";
}
/**
* @returns DataCollection<int, ConnectionStatus>
*/
#[Computed]
public function connectionStatuses(): DataCollection
{
$statusService = app(ConnectionStatusService::class);
return $statusService->getAll();
}
public function save(): void
{
$this->attempt(
action: function () {
$this->form->validate();
$settings = null;
if ($this->isSaml()) {
$attributes = [];
foreach ($this->form->samlAttributes as $attr) {
if (!empty($attr['saml_name']) && !empty($attr['user_field'])) {
$attributes[$attr['saml_name']] = $attr['user_field'];
}
}
$settings = [
"saml" => [
"entity_id" => $this->form->samlEntityId,
"acs_url" => $this->form->samlAcsUrl,
"nameid_format" => $this->form->samlNameIdFormat,
"nameid_source" => $this->form->samlNameIdSource,
"attributes" => $attributes,
],
];
}
$serviceData = new ConnectAppRequest(
name: $this->form->name,
connectionProviderId: $this->form->providerId,
connectionProtocolId: $this->form->protocolId,
slug: str($this->form->name)->slug()->toString(),
connectionStatusId: $this->form->statusId,
settings: $settings,
);
$this->service->update($this->recordId, $serviceData);
$this->redirectRoute("apps.index", navigate: true);
},
);
}
public function addSamlAttribute(): void
{
$this->form->addSamlAttribute();
}
public function removeSamlAttribute(int $index): void
{
$this->form->removeSamlAttribute($index);
}
};
?>
<div>
<x-shared.card :title="__('Edit an connected app')">
<x-mary-form wire:submit="save" class="pb-2">
<div class="px-4 grid grid-cols-1 gap-4 md:grid-cols-2">
<x-mary-input wire:model.live.blur="form.name" required :label="__('Name')"/>
<x-mary-choices
required
wire:model.live="form.protocolId"
:label="__('Protocol')"
:single="true"
:options="$this->protocols"
placeholder="Select"
/>
<x-mary-choices
required
wire:model="form.providerId"
:label="__('Provider')"
placeholder="Select"
:single="true"
:options="$this->providers"
/>
<x-mary-choices
required
wire:model="form.statusId"
:label="__('Status')"
:single="true"
:options="$this->connectionStatuses->toArray()"
placeholder="Select"
/>
@if($this->isSaml)
<x-dashboard.connected-apps.saml-configuration :form="$this->form"/>
@endif
</div>
<x-slot:actions class="pr-2">
<x-mary-button :link="route('apps.index')" wire:navigate>
{{ __('Cancel') }}
</x-mary-button>
<x-mary-button type="submit" spinner="save" class="btn-primary">
{{ __('Save') }}
</x-mary-button>
</x-slot:actions>
</x-mary-form>
</x-shared.card>
</div>