mirror of
https://github.com/tiennm99/coolify.git
synced 2026-08-20 02:27:57 +00:00
GitlabAppPolicy previously allowed any authenticated user to update, delete, and create GitLab sources. Align it with GithubAppPolicy, require Application create authorization on the private-repo wizard, and reject OAuth callbacks from non-admins so members cannot escalate privileges.
130 lines
4.7 KiB
PHP
130 lines
4.7 KiB
PHP
<?php
|
|
|
|
use App\Livewire\Source\Gitlab\Change as GitlabSource;
|
|
use App\Models\GitlabApp;
|
|
use App\Models\Team;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
$this->team = Team::factory()->create();
|
|
$this->user = User::factory()->create();
|
|
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
|
|
|
$this->actingAs($this->user);
|
|
session(['currentTeam' => $this->team]);
|
|
|
|
$this->gitlabApp = GitlabApp::create([
|
|
'name' => 'Self-hosted GitLab',
|
|
'api_url' => 'https://gitlab.example.com/api/v4',
|
|
'html_url' => 'https://gitlab.example.com',
|
|
'client_id' => 'client-id',
|
|
'client_secret' => 'client-secret',
|
|
'redirect_uri' => 'https://coolify.example.com/webhooks/source/gitlab/redirect',
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
'is_public' => false,
|
|
]);
|
|
});
|
|
|
|
describe('GitLab OAuth callback state validation', function () {
|
|
test('rejects a callback whose state is the source UUID (the old attack vector)', function () {
|
|
Http::fake();
|
|
|
|
$response = $this->get('/webhooks/source/gitlab/redirect?code=any&state='.$this->gitlabApp->uuid);
|
|
|
|
$response->assertRedirect(route('source.all'));
|
|
Http::assertNothingSent();
|
|
expect($this->gitlabApp->refresh()->access_token)->toBeNull();
|
|
});
|
|
|
|
test('rejects a callback with an unknown / expired state and never exchanges the code', function () {
|
|
Http::fake();
|
|
|
|
$response = $this->get('/webhooks/source/gitlab/redirect?code=any&state=not-a-real-state');
|
|
|
|
$response->assertRedirect(route('source.all'));
|
|
Http::assertNothingSent();
|
|
expect($this->gitlabApp->refresh()->access_token)->toBeNull();
|
|
});
|
|
|
|
test('rejects a state issued for a different team', function () {
|
|
Http::fake();
|
|
$otherTeam = Team::factory()->create();
|
|
$state = 'state-for-other-team';
|
|
Cache::put(GitlabSource::oauthStateCacheKey($state), [
|
|
'gitlab_app_id' => $this->gitlabApp->id,
|
|
'team_id' => $otherTeam->id,
|
|
], now()->addMinutes(60));
|
|
|
|
$response = $this->get('/webhooks/source/gitlab/redirect?code=any&state='.$state);
|
|
|
|
$response->assertRedirect(route('source.all'));
|
|
Http::assertNothingSent();
|
|
expect($this->gitlabApp->refresh()->access_token)->toBeNull();
|
|
});
|
|
|
|
test('accepts a valid one-time state, exchanges the code, and consumes the state', function () {
|
|
Http::fake([
|
|
'*/oauth/token' => Http::response([
|
|
'access_token' => 'new-access-token',
|
|
'refresh_token' => 'new-refresh-token',
|
|
'expires_in' => 7200,
|
|
]),
|
|
]);
|
|
|
|
$state = 'a-valid-server-issued-state';
|
|
$key = GitlabSource::oauthStateCacheKey($state);
|
|
Cache::put($key, [
|
|
'gitlab_app_id' => $this->gitlabApp->id,
|
|
'team_id' => $this->team->id,
|
|
], now()->addMinutes(60));
|
|
|
|
$response = $this->get('/webhooks/source/gitlab/redirect?code=valid-code&state='.$state);
|
|
|
|
$response->assertRedirect(route('source.gitlab.show', ['gitlab_app_uuid' => $this->gitlabApp->uuid]));
|
|
|
|
$fresh = $this->gitlabApp->refresh();
|
|
$fresh->makeVisible(['access_token', 'refresh_token']);
|
|
expect($fresh->access_token)->toBe('new-access-token');
|
|
expect($fresh->refresh_token)->toBe('new-refresh-token');
|
|
|
|
// State must be single-use.
|
|
expect(Cache::get($key))->toBeNull();
|
|
});
|
|
|
|
test('requires authentication', function () {
|
|
auth()->logout();
|
|
session()->forget('currentTeam');
|
|
|
|
$response = $this->get('/webhooks/source/gitlab/redirect?code=any&state=any');
|
|
|
|
$response->assertRedirect(route('login'));
|
|
});
|
|
|
|
test('rejects a callback from a team member who cannot administer the source', function () {
|
|
Http::fake();
|
|
|
|
$member = User::factory()->create();
|
|
$this->team->members()->attach($member->id, ['role' => 'member']);
|
|
$this->actingAs($member);
|
|
session(['currentTeam' => $this->team]);
|
|
|
|
$state = 'member-state';
|
|
Cache::put(GitlabSource::oauthStateCacheKey($state), [
|
|
'gitlab_app_id' => $this->gitlabApp->id,
|
|
'team_id' => $this->team->id,
|
|
], now()->addMinutes(60));
|
|
|
|
$response = $this->get('/webhooks/source/gitlab/redirect?code=any&state='.$state);
|
|
|
|
$response->assertRedirect(route('source.all'));
|
|
Http::assertNothingSent();
|
|
expect($this->gitlabApp->refresh()->access_token)->toBeNull();
|
|
});
|
|
});
|