mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(access): hide shared variables and actions that a member cannot use
- Check read access on the project or environment for shared variables pages, the canvas, and the sidebar, not only navigation access - Require team-wide read for the server shared variables canvas API - Open the environment variable editor read-only when the value is hidden - Show the integration tokens button only to users who can open it - Show disabled confirmation buttons as disabled, not as danger buttons
This commit is contained in:
@@ -37,7 +37,7 @@ class SharedVariableCanvasController extends Controller
|
||||
if ($owner instanceof JsonResponse) {
|
||||
return $owner;
|
||||
}
|
||||
$this->authorize('view', $owner);
|
||||
$this->authorize('viewSharedVariables', $owner);
|
||||
|
||||
return response()->json($this->canvas->payload($scope, $owner, $this->canEdit($request, $scope, $owner)));
|
||||
}
|
||||
@@ -49,7 +49,7 @@ class SharedVariableCanvasController extends Controller
|
||||
if ($owner instanceof JsonResponse) {
|
||||
return $owner;
|
||||
}
|
||||
$this->authorize('view', $owner);
|
||||
$this->authorize('viewSharedVariables', $owner);
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof JsonResponse) {
|
||||
@@ -108,7 +108,7 @@ class SharedVariableCanvasController extends Controller
|
||||
if ($owner instanceof JsonResponse) {
|
||||
return $owner;
|
||||
}
|
||||
$this->authorize('view', $owner);
|
||||
$this->authorize('viewSharedVariables', $owner);
|
||||
|
||||
[$source, $target] = array_pad(explode('|', (string) $request->route('link_id'), 2), 2, '');
|
||||
$resource = $this->canvas->resources($scope, $owner)->get($target);
|
||||
|
||||
@@ -164,9 +164,10 @@ class Show extends Component
|
||||
*/
|
||||
public function loadValues(): void
|
||||
{
|
||||
$this->authorize('viewSensitive', $this->env);
|
||||
$this->authorize('view', $this->env);
|
||||
|
||||
if ($this->valuesLoaded) {
|
||||
// Users without viewSensitive open the editor with the value hidden; nothing to decrypt.
|
||||
if ($this->valuesLoaded || $this->valuesHiddenForUser()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ class Canvas extends Component
|
||||
default => 'team',
|
||||
};
|
||||
$owner = $this->owner();
|
||||
$this->authorize('view', $owner);
|
||||
$this->authorize('viewSharedVariables', $owner);
|
||||
|
||||
$this->endpoint = $this->scope === 'team'
|
||||
? '/shared-variables/team/canvas'
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Livewire\SharedVariables\Environment;
|
||||
|
||||
use App\Models\Environment;
|
||||
use App\Models\Project;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
@@ -12,7 +13,12 @@ class Index extends Component
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->projects = Project::ownedByCurrentTeamCached();
|
||||
$this->projects = Project::ownedByCurrentTeamCached()
|
||||
->each(fn (Project $project) => $project->setRelation('environments', $project->environments
|
||||
->filter(fn (Environment $environment) => auth()->user()->can('viewSharedVariables', $environment))
|
||||
->values()))
|
||||
->filter(fn (Project $project) => $project->environments->isNotEmpty())
|
||||
->values();
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
||||
@@ -62,6 +62,7 @@ class Show extends Component
|
||||
|
||||
$this->project = Project::ownedByCurrentTeam()->where('uuid', $projectUuid)->firstOrFail();
|
||||
$this->environment = $this->project->environments()->where('uuid', $environmentUuid)->firstOrFail();
|
||||
$this->authorize('viewSharedVariables', $this->environment);
|
||||
if ($this->prefersCanvasView('environment_variables')) {
|
||||
return $this->redirectRoute('shared-variables.environment.canvas', ['project_uuid' => $this->project->uuid, 'environment_uuid' => $this->environment->uuid], navigate: true);
|
||||
}
|
||||
@@ -70,7 +71,7 @@ class Show extends Component
|
||||
|
||||
public function switch()
|
||||
{
|
||||
$this->authorize('view', $this->environment);
|
||||
$this->authorize('viewSharedVariables', $this->environment);
|
||||
$this->view = $this->view === 'normal' ? 'dev' : 'normal';
|
||||
$this->getDevView();
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ class Index extends Component
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->projects = Project::ownedByCurrentTeamCached();
|
||||
$this->projects = Project::ownedByCurrentTeamCached()
|
||||
->filter(fn (Project $project) => auth()->user()->can('viewSharedVariables', $project))
|
||||
->values();
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
||||
@@ -55,6 +55,7 @@ class Show extends Component
|
||||
if (! $project) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
$this->authorize('viewSharedVariables', $project);
|
||||
$this->project = $project;
|
||||
if ($this->prefersCanvasView('project_variables')) {
|
||||
return $this->redirectRoute('shared-variables.project.canvas', ['project_uuid' => $project->uuid], navigate: true);
|
||||
@@ -65,7 +66,7 @@ class Show extends Component
|
||||
public function switch()
|
||||
{
|
||||
try {
|
||||
$this->authorize('view', $this->project);
|
||||
$this->authorize('viewSharedVariables', $this->project);
|
||||
$this->view = $this->view === 'normal' ? 'dev' : 'normal';
|
||||
$this->getDevView();
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
@@ -26,6 +26,15 @@ class EnvironmentPolicy
|
||||
return app(AccessResolver::class)->canOpen($user, $environment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can read the shared variables of the environment.
|
||||
* Opening an environment for navigation is not enough; this needs read access on the environment.
|
||||
*/
|
||||
public function viewSharedVariables(User $user, Environment $environment): bool
|
||||
{
|
||||
return $user->canAccess(AccessLevel::Read, $environment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create environments, in the project when one is given.
|
||||
*/
|
||||
|
||||
@@ -25,6 +25,15 @@ class ProjectPolicy
|
||||
return app(AccessResolver::class)->canOpen($user, $project);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can read the shared variables of the project.
|
||||
* Opening a project for navigation is not enough; this needs read access on the project.
|
||||
*/
|
||||
public function viewSharedVariables(User $user, Project $project): bool
|
||||
{
|
||||
return $user->canAccess(AccessLevel::Read, $project);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create projects in the current team.
|
||||
*/
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Enums\AccessLevel;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use App\Services\Access\AccessResolver;
|
||||
|
||||
class ServerPolicy
|
||||
{
|
||||
@@ -23,6 +25,14 @@ class ServerPolicy
|
||||
return $user->teams->contains('id', $server->team_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can read the shared variables of the server (team-wide read).
|
||||
*/
|
||||
public function viewSharedVariables(User $user, Server $server): bool
|
||||
{
|
||||
return app(AccessResolver::class)->allowsTeamWide($user, AccessLevel::Read, $server->team_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create models.
|
||||
*/
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Enums\AccessLevel;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use App\Services\Access\AccessResolver;
|
||||
|
||||
class TeamPolicy
|
||||
{
|
||||
@@ -23,6 +25,14 @@ class TeamPolicy
|
||||
return $user->teams->contains('id', $team->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can read the team shared variables (team-wide read).
|
||||
*/
|
||||
public function viewSharedVariables(User $user, Team $team): bool
|
||||
{
|
||||
return app(AccessResolver::class)->allowsTeamWide($user, AccessLevel::Read, $team->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create models.
|
||||
*/
|
||||
|
||||
@@ -515,7 +515,7 @@ option {
|
||||
}
|
||||
|
||||
.button.button-error:disabled {
|
||||
@apply text-red-800 dark:text-red-300 bg-red-50 dark:bg-red-900/30 border-red-300 dark:border-red-800;
|
||||
@apply text-neutral-300 dark:text-fg-faint bg-transparent dark:bg-transparent border-neutral-200 dark:border-white/[0.06];
|
||||
}
|
||||
|
||||
.button-error:not(:disabled) {
|
||||
|
||||
@@ -173,11 +173,11 @@
|
||||
@else
|
||||
@if ($disabled)
|
||||
@if ($buttonFullWidth)
|
||||
<x-forms.button class="w-full" isError disabled :authDisabled="$authDisabled" :tooltip="$disabledTooltip" wire:target>
|
||||
<x-forms.button class="w-full" :isError="$isErrorButton" disabled :authDisabled="$authDisabled" :tooltip="$disabledTooltip" wire:target>
|
||||
{{ $resolvedButtonTitle }}
|
||||
</x-forms.button>
|
||||
@else
|
||||
<x-forms.button isError disabled :authDisabled="$authDisabled" :tooltip="$disabledTooltip" wire:target>
|
||||
<x-forms.button :isError="$isErrorButton" disabled :authDisabled="$authDisabled" :tooltip="$disabledTooltip" wire:target>
|
||||
{{ $resolvedButtonTitle }}
|
||||
</x-forms.button>
|
||||
@endif
|
||||
|
||||
@@ -109,6 +109,10 @@
|
||||
@php
|
||||
// Members limited to some projects only see project pages; team-level pages return 403 for them.
|
||||
$teamWide = app(App\Services\Access\AccessResolver::class)->coversWholeTeam(auth()->user(), currentTeam()->id);
|
||||
// Project and environment variables need read access there; opening a parent of a granted resource is not enough.
|
||||
$readableProjects = $teamWide ? null : App\Models\Project::ownedByCurrentTeamCached();
|
||||
$canReadProjectVariables = $teamWide || $readableProjects->contains(fn ($project) => auth()->user()->can('viewSharedVariables', $project));
|
||||
$canReadEnvironmentVariables = $teamWide || $readableProjects->contains(fn ($project) => $project->environments->contains(fn ($environment) => auth()->user()->can('viewSharedVariables', $environment)));
|
||||
// An entry with children opens a submenu in place of the main menu (back button + its pages).
|
||||
$navSections = collect([
|
||||
'Workspace' => [
|
||||
@@ -131,8 +135,8 @@
|
||||
['label' => 'Shared Variables', 'icon' => 'variables', 'children' => [
|
||||
['label' => 'Overview', 'icon' => 'dashboard', 'href' => route('shared-variables.index'), 'active' => request()->is('shared-variables'), 'visible' => $teamWide],
|
||||
['label' => 'Team', 'icon' => 'teams', 'href' => route('shared-variables.team.index'), 'active' => request()->is('shared-variables/team*'), 'visible' => $teamWide],
|
||||
['label' => 'Projects', 'icon' => 'projects', 'href' => route('shared-variables.project.index'), 'active' => request()->is('shared-variables/projects', 'shared-variables/project/*')],
|
||||
['label' => 'Environments', 'icon' => 'layers', 'href' => route('shared-variables.environment.index'), 'active' => request()->is('shared-variables/environments*')],
|
||||
['label' => 'Projects', 'icon' => 'projects', 'href' => route('shared-variables.project.index'), 'active' => request()->is('shared-variables/projects', 'shared-variables/project/*'), 'visible' => $canReadProjectVariables],
|
||||
['label' => 'Environments', 'icon' => 'layers', 'href' => route('shared-variables.environment.index'), 'active' => request()->is('shared-variables/environments*'), 'visible' => $canReadEnvironmentVariables],
|
||||
['label' => 'Servers', 'icon' => 'servers', 'href' => route('shared-variables.server.index'), 'active' => request()->is('shared-variables/servers', 'shared-variables/server/*'), 'visible' => $teamWide],
|
||||
]],
|
||||
],
|
||||
@@ -178,9 +182,13 @@
|
||||
}
|
||||
$children = array_values(array_filter($item['children'], fn (array $child): bool => (bool) ($child['visible'] ?? true)));
|
||||
|
||||
return [...$item, 'children' => $children, 'href' => $children[0]['href'], 'active' => collect($children)->contains('active', true)];
|
||||
return [...$item, 'children' => $children, 'href' => $children[0]['href'] ?? null, 'active' => collect($children)->contains('active', true)];
|
||||
})
|
||||
->all());
|
||||
// A parent whose children are all hidden has nothing to open.
|
||||
->reject(fn (array $item): bool => isset($item['children']) && $item['children'] === [])
|
||||
->values()
|
||||
->all())
|
||||
->reject(fn (array $items): bool => $items === []);
|
||||
$activeSubmenu = $navSections->flatten(1)->first(fn (array $item): bool => isset($item['children']) && $item['active']);
|
||||
// Resource menus by depth, registered by registerMainNavContext() (e.g. from
|
||||
// components/main-nav-menu.blade.php). Each one teleports itself into #main-nav-context-{depth}
|
||||
|
||||
@@ -6,15 +6,20 @@
|
||||
<x-application.settings-section title="Secret manager" :description="$secretManagerDescription">
|
||||
|
||||
@if (! $link && $availableTokens->isEmpty())
|
||||
<x-empty title="No secret manager tokens"
|
||||
description="Add a Doppler, Infisical, or HashiCorp Vault token first."
|
||||
icon-name="keys" size="sm">
|
||||
<x-slot:actions>
|
||||
<a class="button" href="{{ route('security.integration-tokens') }}" {{ wireNavigate() }}>
|
||||
Integration tokens
|
||||
</a>
|
||||
</x-slot:actions>
|
||||
</x-empty>
|
||||
@can('viewAny', \App\Models\IntegrationToken::class)
|
||||
<x-empty title="No secret manager tokens"
|
||||
description="Add a Doppler, Infisical, or HashiCorp Vault token first."
|
||||
icon-name="keys" size="sm">
|
||||
<x-slot:actions>
|
||||
<a class="button" href="{{ route('security.integration-tokens') }}" {{ wireNavigate() }}>
|
||||
Integration tokens
|
||||
</a>
|
||||
</x-slot:actions>
|
||||
</x-empty>
|
||||
@else
|
||||
<x-empty title="No secret manager source" description="This resource does not use a secret manager."
|
||||
icon-name="keys" size="sm" />
|
||||
@endcan
|
||||
@else
|
||||
@can('update', $resource)
|
||||
<div class="application-settings-form flex w-full flex-col gap-4">
|
||||
@@ -130,6 +135,9 @@
|
||||
{{ $link->integrationToken->name }} · {{ $link->sourceSummary() }}
|
||||
</span>
|
||||
</div>
|
||||
@else
|
||||
<x-empty title="No secret manager source" description="This resource does not use a secret manager."
|
||||
icon-name="keys" size="sm" />
|
||||
@endif
|
||||
@endcan
|
||||
@endif
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
}
|
||||
}">
|
||||
@if ($projects->isEmpty())
|
||||
<x-empty title="No environments yet" description="Create a project environment before adding environment-wide variables." icon-name="layers" />
|
||||
<x-empty title="No environments" description="Environments that you can read show here. Create an environment to add environment-wide variables." icon-name="layers" />
|
||||
@else
|
||||
<x-shared-variables.view-controls label="environments" storage-key="shared-variables-environments-view" />
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
}
|
||||
}">
|
||||
@if ($projects->isEmpty())
|
||||
<x-empty title="No projects yet" description="Create a project before adding project-wide variables."
|
||||
<x-empty title="No projects" description="Projects that you can read show here. Create a project to add project-wide variables."
|
||||
icon-name="projects" />
|
||||
@else
|
||||
<x-shared-variables.view-controls label="projects" storage-key="shared-variables-projects-view" />
|
||||
|
||||
@@ -215,7 +215,9 @@ it('shows secret values in the variable editor only with read-sensitive access',
|
||||
if ($expectedValue === null) {
|
||||
expect($component->get('isValueHidden'))->toBeTrue()
|
||||
->and($component->get('value'))->toBeNull();
|
||||
$component->call('loadValues')->assertForbidden();
|
||||
$component->call('loadValues')->assertOk();
|
||||
expect($component->get('value'))->toBeNull()
|
||||
->and($component->get('valuesLoaded'))->toBeFalse();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -220,8 +220,9 @@ test('member cannot load application environment variable values', function () {
|
||||
'type' => 'application',
|
||||
])
|
||||
->call('loadValues')
|
||||
->assertForbidden()
|
||||
->assertOk()
|
||||
->assertSet('valuesLoaded', false)
|
||||
->assertSet('isValueHidden', true)
|
||||
->assertSet('value', null);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user