fix(services): align service configuration form with role permissions (#12356)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andras BacsaiandClaude Opus 5.5 authored and GitHub committed 2026-10-27 22:39:52 +01:00
1 parent eaabed5c4b
commit 5145aa53dc
3 files changed
+64 -6

No files matched your search

+7 -5
View File
@@ -135,7 +135,7 @@ class StackForm extends Component
$lockedKeys = $this->lockedFieldKeys();
$this->fields = $this->fields->map(function ($field) use ($lockedKeys) {
$field['isLocked'] = $lockedKeys->contains(data_get($field, 'key'));
if ($field['isLocked'] || ($this->isPasswordHiddenForMember && data_get($field, 'isPassword'))) {
if ($field['isLocked'] || $this->isPasswordHiddenForMember) {
$field['value'] = null;
}
@@ -190,10 +190,12 @@ class StackForm extends Component
// Use transaction to ensure atomicity - if parse fails, save is rolled back
DB::transaction(function () {
$this->service->save();
$lockedKeys = $this->lockedFieldKeys();
$this->service->saveExtraFields(
$this->fields->reject(fn ($field) => $lockedKeys->contains(data_get($field, 'key')))
);
if (auth()->user()->can('manageEnvironment', $this->service)) {
$lockedKeys = $this->lockedFieldKeys();
$this->service->saveExtraFields(
$this->fields->reject(fn ($field) => $lockedKeys->contains(data_get($field, 'key')))
);
}
$this->service->parse();
});
// Refresh and write files after a successful commit
@@ -79,7 +79,7 @@
</div>
@if (data_get($field, 'isLocked'))
<x-forms.input disabled value="Locked (shown once)" />
@elseif ($isPasswordHiddenForMember && data_get($field, 'isPassword'))
@elseif ($isPasswordHiddenForMember)
<x-forms.input disabled value="Hidden (only admins can view)" />
@else
<x-forms.input canGate="update" :canResource="$service"
@@ -109,3 +109,59 @@ it('hides an extra field that references a locked variable and keeps both on sav
expect($this->masterPassword->fresh()->value)->toBe('$SERVICE_PASSWORD_64_DRIZZLE')
->and($lockedTarget->fresh()->value)->toBe('referenced-locked-secret');
});
function addVaultwardenDatabaseUrl(): void
{
ServiceApplication::create([
'uuid' => (string) Str::uuid(),
'service_id' => test()->service->id,
'name' => 'vaultwarden',
'image' => 'vaultwarden/server:latest',
]);
test()->service->environment_variables()->create([
'key' => 'DATABASE_URL',
'value' => 'postgresql://vw:vaultwarden-db-secret@db:5432/vw',
]);
}
it('hides extra-field values without a password flag from a member', function () {
addVaultwardenDatabaseUrl();
$ownTeam = Team::factory()->create();
$ownTeam->members()->attach($this->member, ['role' => 'owner']);
$this->actingAs($this->member);
foreach ([$this->team, $ownTeam] as $sessionTeam) {
session(['currentTeam' => $sessionTeam]);
$component = mountStackForm();
expect($component->get('fields')['DATABASE_URL']['value'])->toBeNull()
->and(json_encode($component->snapshot))->not->toContain('vaultwarden-db-secret')
->and($component->html())->not->toContain('vaultwarden-db-secret')
->and($component->html())->toContain('Hidden (only admins can view)');
}
});
it('does not let a member save overwrite stored extra-field values', function () {
addVaultwardenDatabaseUrl();
$this->actingAs($this->member);
session(['currentTeam' => $this->team]);
mountStackForm()
->set('name', 'renamed-by-member')
->set('fields.DATABASE_URL.value', 'member-value')
->call('submit');
expect($this->service->fresh()->name)->not->toBe('renamed-by-member')
->and($this->service->environment_variables()->where('key', 'DATABASE_URL')->value('value'))
->toBe('postgresql://vw:vaultwarden-db-secret@db:5432/vw')
->and($this->masterPassword->fresh()->value)->toBe('drizzle-master-secret');
});
it('shows extra-field values without a password flag to an owner of the service team', function () {
addVaultwardenDatabaseUrl();
$this->actingAs($this->owner);
session(['currentTeam' => $this->team]);
expect(mountStackForm()->get('fields')['DATABASE_URL']['value'])
->toBe('postgresql://vw:vaultwarden-db-secret@db:5432/vw');
});