mirror of
https://github.com/coollabsio/coolify.git
synced 2026-06-13 19:09:50 +00:00
fix(webhook): match manual webhook repositories exactly
The manual webhook handlers selected target applications with a `git_repository LIKE %full_name%` substring query, so a payload repository name could match unintended applications when repository names overlap. Add a `MatchesManualWebhookApplications` trait that validates the incoming `owner/repo` value and matches `Application.git_repository` by exact normalized path. Github, Gitlab, Gitea and Bitbucket manual handlers now use it, reject invalid repository input early, and return a consistent generic webhook failure payload. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Webhook;
|
||||
use App\Actions\Application\CleanupPreviewDeployment;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Webhook\Concerns\DetectsSkipDeployCommits;
|
||||
use App\Http\Controllers\Webhook\Concerns\MatchesManualWebhookApplications;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use Exception;
|
||||
@@ -14,6 +15,7 @@ use Visus\Cuid2\Cuid2;
|
||||
class Bitbucket extends Controller
|
||||
{
|
||||
use DetectsSkipDeployCommits;
|
||||
use MatchesManualWebhookApplications;
|
||||
|
||||
public function manual(Request $request)
|
||||
{
|
||||
@@ -62,8 +64,14 @@ class Bitbucket extends Controller
|
||||
$skip_deploy_pr = self::shouldSkipDeployAny([$pull_request_title]);
|
||||
$commit = data_get($payload, 'pullrequest.source.commit.hash');
|
||||
}
|
||||
$applications = Application::where('git_repository', 'like', "%$full_name%");
|
||||
$applications = $applications->where('git_branch', $branch)->get();
|
||||
$full_name = $this->manualWebhookRepositoryFullName($full_name);
|
||||
if ($full_name === null) {
|
||||
return response([
|
||||
'status' => 'failed',
|
||||
'message' => 'Nothing to do. Invalid repository.',
|
||||
]);
|
||||
}
|
||||
$applications = $this->manualWebhookApplications(Application::query()->where('git_branch', $branch), $full_name);
|
||||
if ($applications->isEmpty()) {
|
||||
return response([
|
||||
'status' => 'failed',
|
||||
@@ -79,11 +87,7 @@ class Bitbucket extends Controller
|
||||
'repository' => $full_name ?? null,
|
||||
'event' => $x_bitbucket_event,
|
||||
]);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
'message' => 'Webhook secret not configured.',
|
||||
]);
|
||||
$return_payloads->push($this->unauthenticatedManualWebhookFailurePayload());
|
||||
|
||||
continue;
|
||||
}
|
||||
@@ -97,11 +101,7 @@ class Bitbucket extends Controller
|
||||
'repository' => $full_name ?? null,
|
||||
'event' => $x_bitbucket_event,
|
||||
]);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
'message' => 'Invalid signature.',
|
||||
]);
|
||||
$return_payloads->push($this->unauthenticatedManualWebhookFailurePayload());
|
||||
|
||||
continue;
|
||||
}
|
||||
@@ -114,11 +114,7 @@ class Bitbucket extends Controller
|
||||
'repository' => $full_name ?? null,
|
||||
'event' => $x_bitbucket_event,
|
||||
]);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
'message' => 'Invalid signature.',
|
||||
]);
|
||||
$return_payloads->push($this->unauthenticatedManualWebhookFailurePayload());
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Webhook\Concerns;
|
||||
|
||||
use App\Models\Application;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
trait MatchesManualWebhookApplications
|
||||
{
|
||||
protected function manualWebhookRepositoryFullName(mixed $fullName): ?string
|
||||
{
|
||||
if (! is_string($fullName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$fullName = trim($fullName, " \t\n\r\0\x0B/");
|
||||
|
||||
if ($fullName === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! preg_match('/\A[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)+\z/', $fullName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->normalizeManualWebhookRepositoryPath($fullName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Application>
|
||||
*/
|
||||
protected function manualWebhookApplications(Builder $query, string $fullName): Collection
|
||||
{
|
||||
return $query->get()
|
||||
->filter(fn (Application $application): bool => $this->manualWebhookRepositoryMatches($application->git_repository, $fullName))
|
||||
->values();
|
||||
}
|
||||
|
||||
protected function manualWebhookRepositoryMatches(?string $gitRepository, string $fullName): bool
|
||||
{
|
||||
$repositoryPath = $this->canonicalManualWebhookRepository($gitRepository);
|
||||
|
||||
return $repositoryPath !== null && hash_equals($fullName, $repositoryPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{status: string, message: string}
|
||||
*/
|
||||
protected function unauthenticatedManualWebhookFailurePayload(): array
|
||||
{
|
||||
return [
|
||||
'status' => 'failed',
|
||||
'message' => 'Invalid signature.',
|
||||
];
|
||||
}
|
||||
|
||||
protected function canonicalManualWebhookRepository(?string $gitRepository): ?string
|
||||
{
|
||||
if (! is_string($gitRepository)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$gitRepository = trim($gitRepository);
|
||||
|
||||
if ($gitRepository === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$path = null;
|
||||
$parts = parse_url($gitRepository);
|
||||
|
||||
if (is_array($parts) && isset($parts['scheme'])) {
|
||||
$path = data_get($parts, 'path');
|
||||
} elseif (Str::startsWith($gitRepository, 'git@') && str_contains($gitRepository, ':')) {
|
||||
$path = Str::after($gitRepository, ':');
|
||||
} else {
|
||||
$path = $gitRepository;
|
||||
}
|
||||
|
||||
if (! is_string($path) || $path === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->normalizeManualWebhookRepositoryPath($path);
|
||||
}
|
||||
|
||||
protected function normalizeManualWebhookRepositoryPath(string $path): string
|
||||
{
|
||||
$path = trim($path);
|
||||
$path = strtok($path, '?#') ?: $path;
|
||||
$path = trim($path, '/');
|
||||
$path = preg_replace('/\.git\z/i', '', $path) ?? $path;
|
||||
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Webhook;
|
||||
use App\Actions\Application\CleanupPreviewDeployment;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Webhook\Concerns\DetectsSkipDeployCommits;
|
||||
use App\Http\Controllers\Webhook\Concerns\MatchesManualWebhookApplications;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use Exception;
|
||||
@@ -15,6 +16,7 @@ use Visus\Cuid2\Cuid2;
|
||||
class Gitea extends Controller
|
||||
{
|
||||
use DetectsSkipDeployCommits;
|
||||
use MatchesManualWebhookApplications;
|
||||
|
||||
public function manual(Request $request)
|
||||
{
|
||||
@@ -58,15 +60,19 @@ class Gitea extends Controller
|
||||
if (! $branch) {
|
||||
return response('Nothing to do. No branch found in the request.');
|
||||
}
|
||||
$applications = Application::where('git_repository', 'like', "%$full_name%");
|
||||
$full_name = $this->manualWebhookRepositoryFullName($full_name);
|
||||
if ($full_name === null) {
|
||||
return response('Nothing to do. Invalid repository.');
|
||||
}
|
||||
$applications = Application::query();
|
||||
if ($x_gitea_event === 'push') {
|
||||
$applications = $applications->where('git_branch', $branch)->get();
|
||||
$applications = $this->manualWebhookApplications($applications->where('git_branch', $branch), $full_name);
|
||||
if ($applications->isEmpty()) {
|
||||
return response("Nothing to do. No applications found with deploy key set, branch is '$branch' and Git Repository name has $full_name.");
|
||||
}
|
||||
}
|
||||
if ($x_gitea_event === 'pull_request') {
|
||||
$applications = $applications->where('git_branch', $base_branch)->get();
|
||||
$applications = $this->manualWebhookApplications($applications->where('git_branch', $base_branch), $full_name);
|
||||
if ($applications->isEmpty()) {
|
||||
return response("Nothing to do. No applications found with branch '$base_branch'.");
|
||||
}
|
||||
@@ -80,11 +86,7 @@ class Gitea extends Controller
|
||||
'repository' => $full_name ?? null,
|
||||
'event' => $x_gitea_event,
|
||||
]);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
'message' => 'Webhook secret not configured.',
|
||||
]);
|
||||
$return_payloads->push($this->unauthenticatedManualWebhookFailurePayload());
|
||||
|
||||
continue;
|
||||
}
|
||||
@@ -96,11 +98,7 @@ class Gitea extends Controller
|
||||
'repository' => $full_name ?? null,
|
||||
'event' => $x_gitea_event,
|
||||
]);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
'message' => 'Invalid signature.',
|
||||
]);
|
||||
$return_payloads->push($this->unauthenticatedManualWebhookFailurePayload());
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Webhook;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Webhook\Concerns\DetectsSkipDeployCommits;
|
||||
use App\Http\Controllers\Webhook\Concerns\MatchesManualWebhookApplications;
|
||||
use App\Jobs\GithubAppPermissionJob;
|
||||
use App\Jobs\ProcessGithubPullRequestWebhook;
|
||||
use App\Models\Application;
|
||||
@@ -18,6 +19,7 @@ use Visus\Cuid2\Cuid2;
|
||||
class Github extends Controller
|
||||
{
|
||||
use DetectsSkipDeployCommits;
|
||||
use MatchesManualWebhookApplications;
|
||||
|
||||
public function manual(Request $request)
|
||||
{
|
||||
@@ -66,15 +68,19 @@ class Github extends Controller
|
||||
if (! $branch) {
|
||||
return response('Nothing to do. No branch found in the request.');
|
||||
}
|
||||
$applications = Application::where('git_repository', 'like', "%$full_name%");
|
||||
$full_name = $this->manualWebhookRepositoryFullName($full_name);
|
||||
if ($full_name === null) {
|
||||
return response('Nothing to do. Invalid repository.');
|
||||
}
|
||||
$applications = Application::query();
|
||||
if ($x_github_event === 'push') {
|
||||
$applications = $applications->where('git_branch', $branch)->get();
|
||||
$applications = $this->manualWebhookApplications($applications->where('git_branch', $branch), $full_name);
|
||||
if ($applications->isEmpty()) {
|
||||
return response("Nothing to do. No applications found with deploy key set, branch is '$branch' and Git Repository name has $full_name.");
|
||||
}
|
||||
}
|
||||
if ($x_github_event === 'pull_request') {
|
||||
$applications = $applications->where('git_branch', $base_branch)->get();
|
||||
$applications = $this->manualWebhookApplications($applications->where('git_branch', $base_branch), $full_name);
|
||||
if ($applications->isEmpty()) {
|
||||
return response("Nothing to do. No applications found for repo $full_name and branch '$base_branch'.");
|
||||
}
|
||||
@@ -93,11 +99,7 @@ class Github extends Controller
|
||||
'repository' => $full_name ?? null,
|
||||
'mode' => 'manual',
|
||||
]);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
'message' => 'Webhook secret not configured.',
|
||||
]);
|
||||
$return_payloads->push($this->unauthenticatedManualWebhookFailurePayload());
|
||||
|
||||
continue;
|
||||
}
|
||||
@@ -109,11 +111,7 @@ class Github extends Controller
|
||||
'repository' => $full_name ?? null,
|
||||
'mode' => 'manual',
|
||||
]);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
'message' => 'Invalid signature.',
|
||||
]);
|
||||
$return_payloads->push($this->unauthenticatedManualWebhookFailurePayload());
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Webhook;
|
||||
use App\Actions\Application\CleanupPreviewDeployment;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Webhook\Concerns\DetectsSkipDeployCommits;
|
||||
use App\Http\Controllers\Webhook\Concerns\MatchesManualWebhookApplications;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use Exception;
|
||||
@@ -15,6 +16,7 @@ use Visus\Cuid2\Cuid2;
|
||||
class Gitlab extends Controller
|
||||
{
|
||||
use DetectsSkipDeployCommits;
|
||||
use MatchesManualWebhookApplications;
|
||||
|
||||
public function manual(Request $request)
|
||||
{
|
||||
@@ -85,9 +87,18 @@ class Gitlab extends Controller
|
||||
return response($return_payloads);
|
||||
}
|
||||
}
|
||||
$applications = Application::where('git_repository', 'like', "%$full_name%");
|
||||
$full_name = $this->manualWebhookRepositoryFullName($full_name);
|
||||
if ($full_name === null) {
|
||||
$return_payloads->push([
|
||||
'status' => 'failed',
|
||||
'message' => 'Nothing to do. Invalid repository.',
|
||||
]);
|
||||
|
||||
return response($return_payloads);
|
||||
}
|
||||
$applications = Application::query();
|
||||
if ($x_gitlab_event === 'push') {
|
||||
$applications = $applications->where('git_branch', $branch)->get();
|
||||
$applications = $this->manualWebhookApplications($applications->where('git_branch', $branch), $full_name);
|
||||
if ($applications->isEmpty()) {
|
||||
$return_payloads->push([
|
||||
'status' => 'failed',
|
||||
@@ -98,7 +109,7 @@ class Gitlab extends Controller
|
||||
}
|
||||
}
|
||||
if ($x_gitlab_event === 'merge_request') {
|
||||
$applications = $applications->where('git_branch', $base_branch)->get();
|
||||
$applications = $this->manualWebhookApplications($applications->where('git_branch', $base_branch), $full_name);
|
||||
if ($applications->isEmpty()) {
|
||||
$return_payloads->push([
|
||||
'status' => 'failed',
|
||||
@@ -117,11 +128,7 @@ class Gitlab extends Controller
|
||||
'repository' => $full_name ?? null,
|
||||
'event' => $x_gitlab_event,
|
||||
]);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
'message' => 'Webhook secret not configured.',
|
||||
]);
|
||||
$return_payloads->push($this->unauthenticatedManualWebhookFailurePayload());
|
||||
|
||||
continue;
|
||||
}
|
||||
@@ -132,11 +139,7 @@ class Gitlab extends Controller
|
||||
'repository' => $full_name ?? null,
|
||||
'event' => $x_gitlab_event,
|
||||
]);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
'message' => 'Invalid signature.',
|
||||
]);
|
||||
$return_payloads->push($this->unauthenticatedManualWebhookFailurePayload());
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ describe('GitHub Manual Webhook HMAC', function () {
|
||||
], $payload);
|
||||
|
||||
$response->assertOk();
|
||||
expect($response->getContent())->toContain('Webhook secret not configured');
|
||||
expect($response->getContent())->toContain('Invalid signature');
|
||||
});
|
||||
|
||||
test('rejects push with forged hash', function () {
|
||||
@@ -118,7 +118,7 @@ describe('GitLab Manual Webhook HMAC', function () {
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
expect($response->getContent())->toContain('Webhook secret not configured');
|
||||
expect($response->getContent())->toContain('Invalid signature');
|
||||
});
|
||||
|
||||
test('rejects push with wrong token', function () {
|
||||
@@ -178,7 +178,7 @@ describe('Bitbucket Manual Webhook HMAC', function () {
|
||||
], $payload);
|
||||
|
||||
$response->assertOk();
|
||||
expect($response->getContent())->toContain('Webhook secret not configured');
|
||||
expect($response->getContent())->toContain('Invalid signature');
|
||||
});
|
||||
|
||||
test('rejects push with non-sha256 algorithm', function () {
|
||||
@@ -263,7 +263,7 @@ describe('Gitea Manual Webhook HMAC', function () {
|
||||
], $payload);
|
||||
|
||||
$response->assertOk();
|
||||
expect($response->getContent())->toContain('Webhook secret not configured');
|
||||
expect($response->getContent())->toContain('Invalid signature');
|
||||
});
|
||||
|
||||
test('rejects push with forged hash', function () {
|
||||
@@ -312,6 +312,204 @@ describe('Gitea Manual Webhook HMAC', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Manual Webhook Repository Matching', function () {
|
||||
test('github rejects empty repository without leaking applications', function () {
|
||||
$app = createApplicationWithWebhook(overrides: ['name' => 'secret-github-app']);
|
||||
|
||||
$payload = json_encode([
|
||||
'ref' => 'refs/heads/main',
|
||||
'repository' => ['full_name' => ''],
|
||||
'after' => 'abc123',
|
||||
'commits' => [],
|
||||
]);
|
||||
|
||||
$response = $this->call('POST', '/webhooks/source/github/events/manual', [], [], [], [
|
||||
'HTTP_X-GitHub-Event' => 'push',
|
||||
'HTTP_X-Hub-Signature-256' => 'sha256=forgedhashvalue',
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
], $payload);
|
||||
|
||||
$response->assertOk();
|
||||
$content = $response->getContent();
|
||||
expect($content)->toContain('Invalid repository')
|
||||
->not->toContain('secret-github-app')
|
||||
->not->toContain($app->uuid);
|
||||
});
|
||||
|
||||
test('github does not match repository substrings', function () {
|
||||
$app = createApplicationWithWebhook(overrides: ['name' => 'secret-github-app']);
|
||||
|
||||
$payload = json_encode([
|
||||
'ref' => 'refs/heads/main',
|
||||
'repository' => ['full_name' => 'test-org/test'],
|
||||
'after' => 'abc123',
|
||||
'commits' => [],
|
||||
]);
|
||||
|
||||
$response = $this->call('POST', '/webhooks/source/github/events/manual', [], [], [], [
|
||||
'HTTP_X-GitHub-Event' => 'push',
|
||||
'HTTP_X-Hub-Signature-256' => 'sha256=forgedhashvalue',
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
], $payload);
|
||||
|
||||
$response->assertOk();
|
||||
$content = $response->getContent();
|
||||
expect($content)->toContain('No applications found')
|
||||
->not->toContain('secret-github-app')
|
||||
->not->toContain($app->uuid);
|
||||
});
|
||||
|
||||
test('github invalid signature does not leak matched application identifiers', function () {
|
||||
$app = createApplicationWithWebhook(overrides: ['name' => 'secret-github-app']);
|
||||
|
||||
$payload = json_encode([
|
||||
'ref' => 'refs/heads/main',
|
||||
'repository' => ['full_name' => 'test-org/test-repo'],
|
||||
'after' => 'abc123',
|
||||
'commits' => [],
|
||||
]);
|
||||
|
||||
$response = $this->call('POST', '/webhooks/source/github/events/manual', [], [], [], [
|
||||
'HTTP_X-GitHub-Event' => 'push',
|
||||
'HTTP_X-Hub-Signature-256' => 'sha256=forgedhashvalue',
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
], $payload);
|
||||
|
||||
$response->assertOk();
|
||||
$content = $response->getContent();
|
||||
expect($content)->toContain('Invalid signature')
|
||||
->not->toContain('secret-github-app')
|
||||
->not->toContain($app->uuid)
|
||||
->not->toContain('application_uuid')
|
||||
->not->toContain('application_name');
|
||||
});
|
||||
|
||||
test('manual webhooks reject empty repositories for every provider without leaking applications', function (string $provider, string $uri, array $payload, array $headers) {
|
||||
$app = createApplicationWithWebhook(overrides: ['name' => "secret-{$provider}-app"]);
|
||||
$body = json_encode($payload);
|
||||
|
||||
$server = ['CONTENT_TYPE' => 'application/json'];
|
||||
foreach ($headers as $name => $value) {
|
||||
$server[$name] = $value;
|
||||
}
|
||||
|
||||
$response = $this->call('POST', $uri, [], [], [], $server, $body);
|
||||
|
||||
$response->assertOk();
|
||||
$content = $response->getContent();
|
||||
expect($content)->toContain('Invalid repository')
|
||||
->not->toContain("secret-{$provider}-app")
|
||||
->not->toContain($app->uuid);
|
||||
})->with([
|
||||
'gitlab' => [
|
||||
'gitlab',
|
||||
'/webhooks/source/gitlab/events/manual',
|
||||
[
|
||||
'object_kind' => 'push',
|
||||
'ref' => 'refs/heads/main',
|
||||
'project' => ['path_with_namespace' => ''],
|
||||
'after' => 'abc123',
|
||||
'commits' => [],
|
||||
],
|
||||
['HTTP_X-Gitlab-Token' => 'wrong-token'],
|
||||
],
|
||||
'bitbucket' => [
|
||||
'bitbucket',
|
||||
'/webhooks/source/bitbucket/events/manual',
|
||||
[
|
||||
'push' => ['changes' => [['new' => ['name' => 'main', 'target' => ['hash' => 'abc123']]]]],
|
||||
'repository' => ['full_name' => ''],
|
||||
],
|
||||
['HTTP_X-Event-Key' => 'repo:push', 'HTTP_X-Hub-Signature' => 'sha256=forgedhashvalue'],
|
||||
],
|
||||
'gitea' => [
|
||||
'gitea',
|
||||
'/webhooks/source/gitea/events/manual',
|
||||
[
|
||||
'ref' => 'refs/heads/main',
|
||||
'repository' => ['full_name' => ''],
|
||||
'after' => 'abc123',
|
||||
'commits' => [],
|
||||
],
|
||||
['HTTP_X-Gitea-Event' => 'push', 'HTTP_X-Hub-Signature-256' => 'sha256=forgedhashvalue'],
|
||||
],
|
||||
]);
|
||||
|
||||
test('manual webhooks do not match repository substrings for every provider', function (string $provider, string $uri, array $payload, array $headers) {
|
||||
$app = createApplicationWithWebhook(overrides: ['name' => "secret-{$provider}-app"]);
|
||||
$body = json_encode($payload);
|
||||
|
||||
$server = ['CONTENT_TYPE' => 'application/json'];
|
||||
foreach ($headers as $name => $value) {
|
||||
$server[$name] = $value;
|
||||
}
|
||||
|
||||
$response = $this->call('POST', $uri, [], [], [], $server, $body);
|
||||
|
||||
$response->assertOk();
|
||||
$content = $response->getContent();
|
||||
expect($content)->toContain('No applications found')
|
||||
->not->toContain("secret-{$provider}-app")
|
||||
->not->toContain($app->uuid);
|
||||
})->with([
|
||||
'gitlab' => [
|
||||
'gitlab',
|
||||
'/webhooks/source/gitlab/events/manual',
|
||||
[
|
||||
'object_kind' => 'push',
|
||||
'ref' => 'refs/heads/main',
|
||||
'project' => ['path_with_namespace' => 'test-org/test'],
|
||||
'after' => 'abc123',
|
||||
'commits' => [],
|
||||
],
|
||||
['HTTP_X-Gitlab-Token' => 'wrong-token'],
|
||||
],
|
||||
'bitbucket' => [
|
||||
'bitbucket',
|
||||
'/webhooks/source/bitbucket/events/manual',
|
||||
[
|
||||
'push' => ['changes' => [['new' => ['name' => 'main', 'target' => ['hash' => 'abc123']]]]],
|
||||
'repository' => ['full_name' => 'test-org/test'],
|
||||
],
|
||||
['HTTP_X-Event-Key' => 'repo:push', 'HTTP_X-Hub-Signature' => 'sha256=forgedhashvalue'],
|
||||
],
|
||||
'gitea' => [
|
||||
'gitea',
|
||||
'/webhooks/source/gitea/events/manual',
|
||||
[
|
||||
'ref' => 'refs/heads/main',
|
||||
'repository' => ['full_name' => 'test-org/test'],
|
||||
'after' => 'abc123',
|
||||
'commits' => [],
|
||||
],
|
||||
['HTTP_X-Gitea-Event' => 'push', 'HTTP_X-Hub-Signature-256' => 'sha256=forgedhashvalue'],
|
||||
],
|
||||
]);
|
||||
|
||||
test('github matches ssh git repository URL exactly', function () {
|
||||
$app = createApplicationWithWebhook(overrides: [
|
||||
'git_repository' => 'git@github.com:test-org/test-repo.git',
|
||||
]);
|
||||
$secret = $app->manual_webhook_secret_github;
|
||||
|
||||
$payload = json_encode([
|
||||
'ref' => 'refs/heads/main',
|
||||
'repository' => ['full_name' => 'test-org/test-repo'],
|
||||
'after' => 'abc123',
|
||||
'commits' => [],
|
||||
]);
|
||||
|
||||
$response = $this->call('POST', '/webhooks/source/github/events/manual', [], [], [], [
|
||||
'HTTP_X-GitHub-Event' => 'push',
|
||||
'HTTP_X-Hub-Signature-256' => 'sha256='.hash_hmac('sha256', $payload, $secret),
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
], $payload);
|
||||
|
||||
$response->assertOk();
|
||||
expect($response->getContent())->not->toContain('No applications found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Webhook Secret Auto-Generation', function () {
|
||||
test('auto-generates webhook secrets on application creation', function () {
|
||||
$app = createApplicationWithWebhook();
|
||||
|
||||
Reference in New Issue
Block a user