fix(access): clearer credential message and backup S3 defaults

- Show a clear message when team-wide write access is needed to use
  team credentials.
- Reset the S3 selection on the backup page when a change is not allowed.
- Do not save database backups to S3 when the API request does not
  send save_s3, as the API documentation says.
This commit is contained in:
Andras Bacsai
2026-11-07 22:04:44 +01:00
parent 8011d0d6ab
commit 3b64cbc379
5 changed files with 42 additions and 3 deletions
@@ -1244,6 +1244,7 @@ class DatabasesController extends Controller
if (! isset($backupData['enabled'])) {
$backupData['enabled'] = true;
}
$backupData['save_s3'] ??= false;
if ($s3Storages?->isNotEmpty()) {
$this->authorize('useTeamCredentials', $database);
@@ -192,6 +192,10 @@ class BackupEdit extends Component
// The team of the schedule, because the instance database has no environment to read it from.
if ($this->backup->newS3StorageIds($storageIds)->isNotEmpty()
&& ! app(AccessResolver::class)->allowsTeamWide(Auth::user(), AccessLevel::Write, (int) $this->backup->team_id)) {
$this->backup->refresh();
$this->saveS3 = (bool) $this->backup->save_s3;
$this->s3StorageIds = $this->backup->selectedS3Storages()->pluck('id')->all();
throw new AuthorizationException('You need write access on the whole team to add an S3 storage to this backup.');
}
$changedFields = auditChangedFields($this->backup);
+5 -2
View File
@@ -6,6 +6,7 @@ use App\Enums\AccessLevel;
use App\Models\Environment;
use App\Models\User;
use App\Services\Access\AccessResolver;
use Illuminate\Auth\Access\Response;
use Illuminate\Database\Eloquent\Model;
class ResourceCreatePolicy
@@ -30,10 +31,12 @@ class ResourceCreatePolicy
* yet: private keys, git sources, integration tokens, and backup files of other resources.
* Needs write access on the whole team of the resource, or of the current team without a resource.
*/
public function useTeamCredentials(User $user, ?Model $resource = null): bool
public function useTeamCredentials(User $user, ?Model $resource = null): Response
{
$teamId = $resource !== null ? $resource->team()?->id : $user->currentTeam()?->id;
return $teamId !== null && app(AccessResolver::class)->allowsTeamWide($user, AccessLevel::Write, (int) $teamId);
return $teamId !== null && app(AccessResolver::class)->allowsTeamWide($user, AccessLevel::Write, (int) $teamId)
? Response::allow()
: Response::deny('You need write access on the whole team to use team credentials, such as private keys, git sources, and S3 storages.');
}
}
+22 -1
View File
@@ -236,7 +236,8 @@ describe('team credentials in the API', function () {
];
placementTestApi($this->developerToken, 'POST', '/api/v1/applications/private-deploy-key', [...$request, 'private_key_uuid' => $privateKey->uuid])
->assertForbidden();
->assertForbidden()
->assertJsonPath('message', 'You need write access on the whole team to use team credentials, such as private keys, git sources, and S3 storages.');
placementTestApi($this->developerToken, 'POST', '/api/v1/applications/private-github-app', [...$request, 'github_app_uuid' => 'any-github-app'])
->assertForbidden();
@@ -314,6 +315,26 @@ describe('team S3 storages for backups', function () {
->and((bool) $backup->fresh()->save_s3)->toBeFalse();
});
test('the backup page shows one error when a project-scoped member selects a team S3 storage', function () {
$backup = ScheduledDatabaseBackup::create([
'frequency' => '0 0 * * *',
'save_s3' => false,
'database_type' => $this->database->getMorphClass(),
'database_id' => $this->database->id,
'team_id' => $this->team->id,
]);
$this->actingAs($this->developer);
session(['currentTeam' => $this->team]);
$component = Livewire::test(BackupEdit::class, ['backup' => $backup->fresh(), 'availableS3Storages' => collect()])
->set('s3StorageIds', [$this->firstStorage->id]);
$errors = collect($component->effects['dispatches'] ?? [])->where('name', 'error');
expect($errors)->toHaveCount(1)
->and($backup->fresh()->s3Storages()->count())->toBe(0);
$component->assertSet('s3StorageIds', [])->assertSet('saveS3', false);
});
test('a member with team-wide write can send a backup schedule to a team S3 storage', function () {
$operator = placementTestMember($this->team, [['scope' => 'team', 'uuid' => null, 'levels' => ['write', 'deploy', 'delete']]]);
@@ -80,6 +80,16 @@ function backupHeaders(): array
}
describe('POST /api/v1/databases/{uuid}/backups', function () {
test('does not save to S3 when save_s3 is not sent', function () {
$this->withHeaders(backupHeaders())
->postJson("/api/v1/databases/{$this->database->uuid}/backups", ['frequency' => 'daily'])
->assertCreated();
$backup = ScheduledDatabaseBackup::firstOrFail();
expect((bool) $backup->save_s3)->toBeFalse()
->and($backup->s3_storage_id)->toBeNull();
});
test('rejects backup configurations for unsupported database types', function () {
$database = StandaloneRedis::create([
'uuid' => (string) Str::uuid(),