mirror of
https://github.com/tiennm99/coolify.git
synced 2026-08-20 12:23:48 +00:00
Move V5 source, migrations, UI, scripts, and tests into documentation, then remove V5 routes, models, jobs, configuration, dependencies, and application hooks.
168 lines
6.0 KiB
Plaintext
168 lines
6.0 KiB
Plaintext
<?php
|
|
|
|
use App\Actions\V5\Server\RemoveBootstrapMarker;
|
|
use App\Models\V5\RevokedAgentToken;
|
|
use App\Models\V5\Server as V5Server;
|
|
use App\Services\Flux\AgentTokenIssuer;
|
|
use App\Services\Flux\FluxClient;
|
|
use Firebase\JWT\JWT;
|
|
use Firebase\JWT\Key;
|
|
use Illuminate\Support\Facades\Config;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Tests\Support\V5TestSchema;
|
|
|
|
/**
|
|
* @return array{0: string, 1: string}
|
|
*/
|
|
function createRevocationJwtKeypair(): array
|
|
{
|
|
$directory = storage_path('framework/testing/revocation-keys-'.bin2hex(random_bytes(4)));
|
|
mkdir($directory, 0777, true);
|
|
|
|
$privateKeyPath = $directory.'/jwt.priv';
|
|
$publicKeyPath = $directory.'/jwt.pub';
|
|
|
|
exec('openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out '.escapeshellarg($privateKeyPath), $out, $code);
|
|
expect($code)->toBe(0);
|
|
chmod($privateKeyPath, 0600);
|
|
|
|
exec('openssl pkey -in '.escapeshellarg($privateKeyPath).' -pubout -out '.escapeshellarg($publicKeyPath), $out, $code);
|
|
expect($code)->toBe(0);
|
|
|
|
return [$privateKeyPath, $publicKeyPath];
|
|
}
|
|
|
|
function createRevocationServer(): V5Server
|
|
{
|
|
return V5Server::query()->create([
|
|
'team_id' => 1,
|
|
'created_by_user_id' => 1,
|
|
'name' => 'edge-01',
|
|
'host' => '203.0.113.10',
|
|
'ssh_user' => 'root',
|
|
'ssh_port' => 22,
|
|
'status' => 'installed',
|
|
'wireguard_management_ip' => '100.64.0.5',
|
|
]);
|
|
}
|
|
|
|
beforeEach(function () {
|
|
Config::set('broadcasting.default', 'log');
|
|
Config::set('cache.default', 'array');
|
|
|
|
Schema::dropIfExists('v5_revoked_agent_tokens');
|
|
Schema::dropIfExists('v5_servers');
|
|
V5TestSchema::createServersTable();
|
|
V5TestSchema::createRevokedAgentTokensTable();
|
|
|
|
[$this->privateKeyPath, $this->publicKeyPath] = createRevocationJwtKeypair();
|
|
Config::set('flux.jwt_private_key_path', $this->privateKeyPath);
|
|
});
|
|
|
|
afterEach(function () {
|
|
Schema::dropIfExists('v5_revoked_agent_tokens');
|
|
Schema::dropIfExists('v5_servers');
|
|
});
|
|
|
|
it('persists the issued jti on the server and reports it as not revoked', function () {
|
|
$server = createRevocationServer();
|
|
|
|
$token = app(AgentTokenIssuer::class)->issueForServer($server);
|
|
$claims = JWT::decode($token, new Key(file_get_contents($this->publicKeyPath), 'ES256'));
|
|
|
|
expect($server->fresh()->agent_token_jti)->toBe($claims->jti)
|
|
->and(app(AgentTokenIssuer::class)->isRevoked($claims->jti))->toBeFalse();
|
|
});
|
|
|
|
it('revokes the currently-issued token and clears the server jti', function () {
|
|
$issuer = app(AgentTokenIssuer::class);
|
|
$server = createRevocationServer();
|
|
|
|
$token = $issuer->issueForServer($server);
|
|
$jti = JWT::decode($token, new Key(file_get_contents($this->publicKeyPath), 'ES256'))->jti;
|
|
|
|
$issuer->revoke($server->fresh());
|
|
|
|
expect($issuer->isRevoked($jti))->toBeTrue()
|
|
->and(RevokedAgentToken::query()->where('jti', $jti)->where('server_id', $server->id)->exists())->toBeTrue()
|
|
->and($server->fresh()->agent_token_jti)->toBeNull();
|
|
});
|
|
|
|
it('is idempotent when revoking a server with no issued token', function () {
|
|
$server = createRevocationServer();
|
|
|
|
app(AgentTokenIssuer::class)->revoke($server);
|
|
|
|
expect(RevokedAgentToken::query()->count())->toBe(0);
|
|
});
|
|
|
|
it('pushes the revocation to flux with the jti and expires_at', function () {
|
|
$issuer = app(AgentTokenIssuer::class);
|
|
$server = createRevocationServer();
|
|
|
|
$token = $issuer->issueForServer($server);
|
|
$server = $server->fresh();
|
|
$jti = JWT::decode($token, new Key(file_get_contents($this->publicKeyPath), 'ES256'))->jti;
|
|
$expiresAtUnix = $server->agent_token_expires_at->getTimestamp();
|
|
|
|
$flux = Mockery::mock(FluxClient::class);
|
|
$flux->shouldReceive('revokeToken')->once()->with($jti, $expiresAtUnix);
|
|
app()->instance(FluxClient::class, $flux);
|
|
|
|
$issuer->revoke($server);
|
|
|
|
expect($issuer->isRevoked($jti))->toBeTrue()
|
|
->and(RevokedAgentToken::query()->where('jti', $jti)->exists())->toBeTrue();
|
|
});
|
|
|
|
it('does not throw and keeps the local revocation record when the flux push fails', function () {
|
|
$issuer = app(AgentTokenIssuer::class);
|
|
$server = createRevocationServer();
|
|
|
|
$token = $issuer->issueForServer($server);
|
|
$jti = JWT::decode($token, new Key(file_get_contents($this->publicKeyPath), 'ES256'))->jti;
|
|
|
|
$flux = Mockery::mock(FluxClient::class);
|
|
$flux->shouldReceive('revokeToken')->once()->andThrow(new RuntimeException('flux socket unreachable'));
|
|
app()->instance(FluxClient::class, $flux);
|
|
|
|
// Flux is down: revoke must not throw, and the local record still stands.
|
|
$issuer->revoke($server->fresh());
|
|
|
|
expect($issuer->isRevoked($jti))->toBeTrue()
|
|
->and(RevokedAgentToken::query()->where('jti', $jti)->exists())->toBeTrue()
|
|
->and($server->fresh()->agent_token_jti)->toBeNull();
|
|
});
|
|
|
|
it('revokeForServer revokes the token and pushes to flux like revoke', function () {
|
|
$issuer = app(AgentTokenIssuer::class);
|
|
$server = createRevocationServer();
|
|
|
|
$token = $issuer->issueForServer($server);
|
|
$jti = JWT::decode($token, new Key(file_get_contents($this->publicKeyPath), 'ES256'))->jti;
|
|
|
|
$flux = Mockery::mock(FluxClient::class);
|
|
$flux->shouldReceive('revokeToken')->once()->with($jti, Mockery::type('int'));
|
|
app()->instance(FluxClient::class, $flux);
|
|
|
|
$issuer->revokeForServer($server->fresh());
|
|
|
|
expect($issuer->isRevoked($jti))->toBeTrue()
|
|
->and($server->fresh()->agent_token_jti)->toBeNull();
|
|
});
|
|
|
|
it('revokes the host token when the bootstrap marker is removed', function () {
|
|
$issuer = app(AgentTokenIssuer::class);
|
|
$server = createRevocationServer();
|
|
|
|
$token = $issuer->issueForServer($server);
|
|
$jti = JWT::decode($token, new Key(file_get_contents($this->publicKeyPath), 'ES256'))->jti;
|
|
|
|
// The server has no private key, so the SSH cleanup returns false, but the
|
|
// revocation (a pure DB write) still runs first.
|
|
$result = RemoveBootstrapMarker::run($server->fresh());
|
|
|
|
expect($result)->toBeFalse()
|
|
->and($issuer->isRevoked($jti))->toBeTrue();
|
|
});
|