fix(livewire): authorize exposed resource mutations (#11596)
This commit is contained in:
parent
e2e91fbb85
commit
b8866b87e8
11 changed files with 179 additions and 1 deletions
|
|
@ -651,6 +651,8 @@ public function generateNginxConfiguration($type = 'static')
|
|||
|
||||
public function resetDefaultLabels($manualReset = false)
|
||||
{
|
||||
$this->authorize('update', $this->application);
|
||||
|
||||
try {
|
||||
if (! $this->isContainerLabelReadonlyEnabled && ! $manualReset) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -35,6 +35,12 @@ public function getListeners()
|
|||
|
||||
public function activityFinished()
|
||||
{
|
||||
if (auth()->user()->cannot('update', $this->database)) {
|
||||
$this->dispatch('refresh');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Only set started_at if database is actually running
|
||||
if ($this->database->isRunning()) {
|
||||
|
|
|
|||
|
|
@ -145,6 +145,8 @@ public function refresh()
|
|||
*/
|
||||
public function loadValues(): void
|
||||
{
|
||||
$this->authorize('update', $this->env);
|
||||
|
||||
if ($this->valuesLoaded) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,12 +17,15 @@
|
|||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Component;
|
||||
|
||||
class GetLogs extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public const MAX_LOG_LINES = 50000;
|
||||
|
||||
public const MAX_DISPLAY_SIZE_BYTES = 5 * 1024 * 1024;
|
||||
|
|
@ -82,6 +85,10 @@ public function mount()
|
|||
public function instantSave()
|
||||
{
|
||||
if (! is_null($this->resource)) {
|
||||
if (auth()->user()->cannot('update', $this->resource)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->resource->getMorphClass() === Application::class) {
|
||||
$this->resource->settings->is_include_timestamps = $this->showTimeStamps;
|
||||
$this->resource->settings->save();
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ public function getListeners()
|
|||
|
||||
public function loadScripts()
|
||||
{
|
||||
$this->authorize('viewAny', CloudInitScript::class);
|
||||
|
||||
CloudInitScript::ownedByCurrentTeam()
|
||||
->whereNull('uuid')
|
||||
->get()
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ private function validateToken(string $provider, string $token): bool
|
|||
|
||||
public function addToken()
|
||||
{
|
||||
$this->authorize('create', CloudProviderToken::class);
|
||||
$this->validate();
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ class ValidateAndInstall extends Component
|
|||
|
||||
public function init(int $data = 0)
|
||||
{
|
||||
$this->authorize('update', $this->server);
|
||||
|
||||
if (! $this->server->canBeValidated()) {
|
||||
$this->error = 'This server was transferred to another Coolify instance and cannot be revalidated here.';
|
||||
$this->server->update([
|
||||
|
|
@ -160,6 +162,8 @@ public function validateConnection()
|
|||
|
||||
public function validateOS()
|
||||
{
|
||||
$this->authorize('update', $this->server);
|
||||
|
||||
$this->supported_os_type = $this->server->validateOS();
|
||||
if (! $this->supported_os_type) {
|
||||
$this->error = 'Server OS type is not supported. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
|
||||
|
|
@ -174,6 +178,8 @@ public function validateOS()
|
|||
|
||||
public function validatePrerequisites()
|
||||
{
|
||||
$this->authorize('update', $this->server);
|
||||
|
||||
$validationResult = $this->server->validatePrerequisites();
|
||||
$this->prerequisites_installed = $validationResult['success'];
|
||||
if (! $validationResult['success']) {
|
||||
|
|
@ -212,6 +218,8 @@ public function validatePrerequisites()
|
|||
|
||||
public function validateDockerEngine()
|
||||
{
|
||||
$this->authorize('update', $this->server);
|
||||
|
||||
$this->docker_installed = $this->server->validateDockerEngine();
|
||||
$this->docker_compose_installed = $this->server->validateDockerCompose();
|
||||
if (! $this->docker_installed || ! $this->docker_compose_installed) {
|
||||
|
|
@ -248,6 +256,8 @@ public function validateDockerEngine()
|
|||
|
||||
public function validateDockerVersion()
|
||||
{
|
||||
$this->authorize('update', $this->server);
|
||||
|
||||
if ($this->server->isSwarm()) {
|
||||
$swarmInstalled = $this->server->validateDockerSwarm();
|
||||
if ($swarmInstalled) {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
<?php
|
||||
|
||||
use App\Livewire\Project\Application\Advanced as ApplicationAdvanced;
|
||||
use App\Livewire\Project\Application\General as ApplicationGeneral;
|
||||
use App\Livewire\Project\Application\Heading as ApplicationHeading;
|
||||
use App\Livewire\Project\Application\Rollback as ApplicationRollback;
|
||||
use App\Livewire\Project\Shared\EnvironmentVariable\Show as EnvironmentVariableShow;
|
||||
use App\Livewire\Project\Shared\GetLogs;
|
||||
use App\Models\Application;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
|
|
@ -10,6 +13,7 @@
|
|||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
|
@ -169,6 +173,60 @@
|
|||
expect($this->member->can('update', $this->application))->toBeFalse();
|
||||
});
|
||||
|
||||
test('member cannot reset application labels', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$originalLabels = $this->application->custom_labels;
|
||||
|
||||
$component = app(ApplicationGeneral::class);
|
||||
$component->application = $this->application;
|
||||
$component->isContainerLabelReadonlyEnabled = true;
|
||||
|
||||
expect(fn () => $component->resetDefaultLabels(true))
|
||||
->toThrow(AuthorizationException::class);
|
||||
|
||||
expect($this->application->fresh()->custom_labels)->toBe($originalLabels);
|
||||
});
|
||||
|
||||
test('member cannot load application environment variable values', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$environmentVariable = $this->application->environment_variables()->create([
|
||||
'key' => 'SECRET_KEY',
|
||||
'value' => 'super-secret-value',
|
||||
'is_preview' => false,
|
||||
]);
|
||||
|
||||
Livewire::test(EnvironmentVariableShow::class, [
|
||||
'env' => $environmentVariable,
|
||||
'type' => 'application',
|
||||
])
|
||||
->call('loadValues')
|
||||
->assertForbidden()
|
||||
->assertSet('valuesLoaded', false)
|
||||
->assertSet('value', null);
|
||||
});
|
||||
|
||||
test('member cannot save application log timestamp settings', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$originalValue = $this->application->settings->is_include_timestamps;
|
||||
|
||||
Livewire::test(GetLogs::class, [
|
||||
'resource' => $this->application,
|
||||
'server' => $this->server,
|
||||
'container' => $this->application->uuid,
|
||||
])
|
||||
->set('showTimeStamps', ! $originalValue)
|
||||
->call('instantSave')
|
||||
->assertSuccessful();
|
||||
|
||||
expect($this->application->settings->fresh()->is_include_timestamps)->toBe($originalValue);
|
||||
});
|
||||
|
||||
// --- Application Advanced Livewire actions ---
|
||||
|
||||
test('member cannot save application advanced settings', function () {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
|
||||
use App\Livewire\Project\Database\Heading;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
|
|
@ -10,11 +11,12 @@
|
|||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::updateOrCreate(['id' => 0]);
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::updateOrCreate(['id' => 0], ['id' => 0]));
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
|
||||
|
|
@ -89,6 +91,18 @@
|
|||
expect($this->member->can('update', $this->database))->toBeFalse();
|
||||
});
|
||||
|
||||
test('member cannot persist database activity status', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(Heading::class, ['database' => $this->database])
|
||||
->call('activityFinished')
|
||||
->assertSuccessful()
|
||||
->assertDispatched('refresh');
|
||||
|
||||
expect($this->database->fresh()->started_at)->toBeNull();
|
||||
});
|
||||
|
||||
// --- Database Policy: delete ---
|
||||
|
||||
test('admin can delete database', function () {
|
||||
|
|
|
|||
57
tests/Feature/Authorization/SecurityAuthorizationTest.php
Normal file
57
tests/Feature/Authorization/SecurityAuthorizationTest.php
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
use App\Livewire\Security\CloudInitScripts;
|
||||
use App\Livewire\Security\CloudProviderTokenForm;
|
||||
use App\Models\CloudInitScript;
|
||||
use App\Models\CloudProviderToken;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->withoutVite();
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::updateOrCreate(['id' => 0], ['id' => 0]));
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
$this->member = User::factory()->create();
|
||||
$this->member->teams()->attach($this->team, ['role' => 'member']);
|
||||
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
});
|
||||
|
||||
test('member cannot create a cloud provider token through the public action', function () {
|
||||
Http::fake(['*' => Http::response([], 200)]);
|
||||
|
||||
Livewire::test(CloudProviderTokenForm::class)
|
||||
->set('provider', 'hetzner')
|
||||
->set('token', 'secret-token')
|
||||
->set('name', 'Unauthorized token')
|
||||
->call('addToken')
|
||||
->assertForbidden();
|
||||
|
||||
expect(CloudProviderToken::query()->count())->toBe(0);
|
||||
});
|
||||
|
||||
test('member cannot load cloud-init scripts through the public action', function () {
|
||||
$script = CloudInitScript::query()->create([
|
||||
'team_id' => $this->team->id,
|
||||
'name' => 'Protected script',
|
||||
'script' => '#cloud-config',
|
||||
]);
|
||||
|
||||
CloudInitScript::query()->whereKey($script)->update(['uuid' => null]);
|
||||
|
||||
$component = app(CloudInitScripts::class);
|
||||
|
||||
expect(fn () => $component->loadScripts())
|
||||
->toThrow(AuthorizationException::class);
|
||||
|
||||
expect($script->refresh()->uuid)->toBeNull();
|
||||
});
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
use App\Livewire\Server\Index as ServerIndex;
|
||||
use App\Livewire\Server\LogDrains;
|
||||
use App\Livewire\Server\Navbar as ServerNavbar;
|
||||
use App\Livewire\Server\ValidateAndInstall;
|
||||
use App\Models\CloudProviderToken;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
|
|
@ -61,6 +62,24 @@
|
|||
expect($this->member->can('update', $this->server))->toBeFalse();
|
||||
});
|
||||
|
||||
test('member cannot invoke server validation write steps', function (string $method) {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
$before = $this->server->fresh()->getAttributes();
|
||||
|
||||
Livewire::test(ValidateAndInstall::class, ['server' => $this->server])
|
||||
->call($method)
|
||||
->assertForbidden();
|
||||
|
||||
expect($this->server->fresh()->getAttributes())->toBe($before);
|
||||
})->with([
|
||||
'init',
|
||||
'validateOS',
|
||||
'validatePrerequisites',
|
||||
'validateDockerEngine',
|
||||
'validateDockerVersion',
|
||||
]);
|
||||
|
||||
// --- Server Policy: delete ---
|
||||
|
||||
test('admin can delete server', function () {
|
||||
|
|
|
|||
Loading…
Reference in a new issue