diff --git a/app/Livewire/Project/Service/StackForm.php b/app/Livewire/Project/Service/StackForm.php
index cc6a994b36..d410ffeda1 100644
--- a/app/Livewire/Project/Service/StackForm.php
+++ b/app/Livewire/Project/Service/StackForm.php
@@ -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
diff --git a/resources/views/livewire/project/service/stack-form.blade.php b/resources/views/livewire/project/service/stack-form.blade.php
index 59405e03f1..4e0ecaf185 100644
--- a/resources/views/livewire/project/service/stack-form.blade.php
+++ b/resources/views/livewire/project/service/stack-form.blade.php
@@ -79,7 +79,7 @@
@if (data_get($field, 'isLocked'))
- @elseif ($isPasswordHiddenForMember && data_get($field, 'isPassword'))
+ @elseif ($isPasswordHiddenForMember)
@else
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');
+});