fix(api-tokens): allow token creation for MCP when REST API is disabled

Closes #11281
This commit is contained in:
Andras Bacsai
2026-10-16 21:29:51 +02:00
parent 412e0e5863
commit 528754043e
6 changed files with 45 additions and 24 deletions
+1
View File
@@ -11,6 +11,7 @@
- Files written on a server with `tee` get the server umask (027/077 on hardened hosts). Set the mode explicitly (`chmod 644`) for files that a non-root container user reads through a read-only bind mount.
- `sshd` keeps the login shell of an open multiplexed SSH connection. Call `SshMultiplexingHelper::removeMuxFile()` before a live test of SSH or login-shell behavior.
- SSH retries replay the full command batch. Only exit code 255 is an SSH transport failure; other exit codes come from the remote command, so do not retry them. Keep batches safe to run twice (for example `docker rm -f <name> || true` before `docker run --name <name>`).
- Do not put `pkill -f "playwright run-server"` in the same shell command as other work; the pattern matches that shell and kills it. Run it as its own command.
- Redirect browser test output to a file (`> /tmp/x.log 2>&1`); piping it (`| tail`) hangs because the Playwright server keeps the pipe open.
- Browser `click('Text')` can match a hidden element first (for example inactive modal steps) and wait forever; use `button:visible:has-text("Text")`.
- Never run `php artisan test` inside a dev container (`./scripts/dev exec` or `docker exec coolify-dev-*`). The container sets `DB_CONNECTION=pgsql` as a real environment variable, which wins over `phpunit.xml`, so `RefreshDatabase` runs `migrate:fresh` on the dev database and wipes it. Run tests on the host only.
-4
View File
@@ -2,7 +2,6 @@
namespace App\Livewire\Security;
use App\Models\InstanceSettings;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Validation\Rule;
use Laravel\Sanctum\PersonalAccessToken;
@@ -35,8 +34,6 @@ class ApiTokens extends Component
365 => '1 year',
];
public $isApiEnabled;
#[Locked]
public bool $canUseRootPermissions = false;
@@ -56,7 +53,6 @@ class ApiTokens extends Component
public function mount()
{
$this->isApiEnabled = InstanceSettings::get()->is_api_enabled;
$this->canUseRootPermissions = auth()->user()->can('useRootPermissions', PersonalAccessToken::class);
$this->canUseWritePermissions = auth()->user()->can('useWritePermissions', PersonalAccessToken::class);
$this->canUseDeployPermissions = auth()->user()->can('useDeployPermissions', PersonalAccessToken::class);
@@ -5,24 +5,6 @@
<x-security.settings-layout>
@if (!$isApiEnabled)
<div class="application-settings-form">
<x-application.settings-section title="API disabled"
description="Enable the Coolify API before creating access tokens.">
<x-empty title="API access is turned off"
description="{{ isInstanceAdmin() ? 'Enable API access in instance settings to issue tokens.' : 'Ask an instance admin to enable API access.' }}"
icon-name="keys" size="sm">
@if (isInstanceAdmin())
<x-slot:actions>
<a href="{{ route('settings.advanced') }}" class="button" {{ wireNavigate() }}>
Enable API access
</a>
</x-slot:actions>
@endif
</x-empty>
</x-application.settings-section>
</div>
@else
@php
$expirationList = collect($expirationOptions)
->map(fn ($label, $days) => ['value' => (string) $days, 'label' => $label])
@@ -32,6 +14,9 @@
@endphp
<div class="application-settings-form flex flex-col gap-6">
<x-api-disabled-callout
message="REST API requests are rejected. Tokens still authenticate the MCP endpoint when the MCP server is enabled." />
@can('create', App\Models\PersonalAccessToken::class)
<form wire:submit="addNewToken">
<x-application.settings-section title="New API token">
@@ -262,6 +247,5 @@
</div>
</x-application.settings-section>
</div>
@endif
</x-security.settings-layout>
</div>
@@ -101,7 +101,12 @@
@endif
@if ($is_mcp_server_enabled)
<x-callout type="info" title="MCP endpoint" class="mt-4">
<code>{{ url('/mcp') }}</code> uses Sanctum bearer tokens from Security → API Tokens.
<div class="flex items-center gap-1">
<code>{{ url('/mcp') }}</code>
<x-copy-button :value="url('/mcp')" label="Copy MCP endpoint" />
</div>
Authenticate with a bearer token from
<a href="{{ route('security.api-tokens') }}" class="font-medium underline" {{ wireNavigate() }}>Security → API Tokens</a>.
</x-callout>
@endif
</x-application.settings-section>
+14
View File
@@ -77,3 +77,17 @@ it('does not show the API notice on the webhooks page when the API is enabled',
Livewire::test(Webhooks::class, ['resource' => $this->application])
->assertDontSee('API access is turned off');
});
it('lets users create API tokens for MCP while the REST API is disabled', function () {
$this->actingAs($this->teamAdmin);
session(['currentTeam' => $this->team]);
Livewire::test(ApiTokens::class)
->assertSee('API access is turned off')
->assertSee('Create token')
->set('description', 'MCP client')
->call('addNewToken')
->assertHasNoErrors();
expect($this->teamAdmin->tokens()->where('name', 'MCP client')->exists())->toBeTrue();
});
@@ -127,3 +127,24 @@ test('open API allowlist warning is shown when API access is enabled without all
->assertSet('is_api_enabled', true)
->assertSee('API access is open to every source');
});
test('MCP endpoint callout links to API token management', function () {
$rootTeam = Team::find(0) ?? Team::factory()->create(['id' => 0]);
Server::factory()->create(['id' => 0, 'team_id' => $rootTeam->id]);
InstanceSettings::forceCreate([
'id' => 0,
'is_api_enabled' => false,
'is_mcp_server_enabled' => true,
]);
Once::flush();
$user = User::factory()->create();
$rootTeam->members()->attach($user->id, ['role' => 'admin']);
$this->actingAs($user);
session(['currentTeam' => ['id' => $rootTeam->id]]);
Livewire::test(Advanced::class)
->assertSee(url('/mcp'))
->assertSee('href="'.route('security.api-tokens').'"', false);
});