210 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Perl
		
	
	
	
	
	
		
		
			
		
	
	
			210 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Perl
		
	
	
	
	
	
| 
								 | 
							
								use strict;
							 | 
						||
| 
								 | 
							
								use warnings;
							 | 
						||
| 
								 | 
							
								use Setup;
							 | 
						||
| 
								 | 
							
								use Test2::V0;
							 | 
						||
| 
								 | 
							
								use Test2::Tools::Subtest qw(subtest_streamed);
							 | 
						||
| 
								 | 
							
								use HTTP::Request;
							 | 
						||
| 
								 | 
							
								use HTTP::Request::Common;
							 | 
						||
| 
								 | 
							
								use JSON::MaybeXS qw(decode_json encode_json);
							 | 
						||
| 
								 | 
							
								use Digest::SHA qw(hmac_sha256_hex);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# Create webhook configuration
							 | 
						||
| 
								 | 
							
								my $github_secret = "github-test-secret-12345";
							 | 
						||
| 
								 | 
							
								my $github_secret_alt = "github-alternative-secret";
							 | 
						||
| 
								 | 
							
								my $gitea_secret = "gitea-test-secret-abcdef";
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# Create a temporary directory first to get the path
							 | 
						||
| 
								 | 
							
								use File::Temp;
							 | 
						||
| 
								 | 
							
								my $tmpdir = File::Temp->newdir(CLEANUP => 0);
							 | 
						||
| 
								 | 
							
								my $tmpdir_path = $tmpdir->dirname;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# Write webhook secrets configuration before creating test context
							 | 
						||
| 
								 | 
							
								mkdir "$tmpdir_path/hydra-data";
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# Create webhook secrets configuration file
							 | 
						||
| 
								 | 
							
								my $webhook_config = qq|
							 | 
						||
| 
								 | 
							
								<github>
							 | 
						||
| 
								 | 
							
								  secret = $github_secret
							 | 
						||
| 
								 | 
							
								  secret = $github_secret_alt
							 | 
						||
| 
								 | 
							
								</github>
							 | 
						||
| 
								 | 
							
								<gitea>
							 | 
						||
| 
								 | 
							
								  secret = $gitea_secret
							 | 
						||
| 
								 | 
							
								</gitea>
							 | 
						||
| 
								 | 
							
								|;
							 | 
						||
| 
								 | 
							
								write_file("$tmpdir_path/hydra-data/webhook-secrets.conf", $webhook_config);
							 | 
						||
| 
								 | 
							
								chmod 0600, "$tmpdir_path/hydra-data/webhook-secrets.conf";
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# Create test context with webhook configuration using include
							 | 
						||
| 
								 | 
							
								my $ctx = test_context(
							 | 
						||
| 
								 | 
							
								    tmpdir => $tmpdir,
							 | 
						||
| 
								 | 
							
								    hydra_config => qq|
							 | 
						||
| 
								 | 
							
								<webhooks>
							 | 
						||
| 
								 | 
							
								  Include $tmpdir_path/hydra-data/webhook-secrets.conf
							 | 
						||
| 
								 | 
							
								</webhooks>
							 | 
						||
| 
								 | 
							
								|
							 | 
						||
| 
								 | 
							
								);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# Import Catalyst::Test after test context is set up
							 | 
						||
| 
								 | 
							
								require Catalyst::Test;
							 | 
						||
| 
								 | 
							
								Catalyst::Test->import('Hydra');
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# Create a project and jobset for testing
							 | 
						||
| 
								 | 
							
								my $user = $ctx->db()->resultset('Users')->create({
							 | 
						||
| 
								 | 
							
								    username => "webhook-test",
							 | 
						||
| 
								 | 
							
								    emailaddress => 'webhook-test@example.org',
							 | 
						||
| 
								 | 
							
								    password => ''
							 | 
						||
| 
								 | 
							
								});
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								my $project = $ctx->db()->resultset('Projects')->create({
							 | 
						||
| 
								 | 
							
								    name => "webhook-test",
							 | 
						||
| 
								 | 
							
								    displayname => "webhook-test",
							 | 
						||
| 
								 | 
							
								    owner => $user->username
							 | 
						||
| 
								 | 
							
								});
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								my $jobset = $project->jobsets->create({
							 | 
						||
| 
								 | 
							
								    name => "test-jobset",
							 | 
						||
| 
								 | 
							
								    nixexprinput => "src",
							 | 
						||
| 
								 | 
							
								    nixexprpath => "default.nix",
							 | 
						||
| 
								 | 
							
								    emailoverride => ""
							 | 
						||
| 
								 | 
							
								});
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								my $jobsetinput = $jobset->jobsetinputs->create({name => "src", type => "git"});
							 | 
						||
| 
								 | 
							
								$jobsetinput->jobsetinputalts->create({altnr => 0, value => "https://github.com/owner/repo.git"});
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# Create another jobset for Gitea
							 | 
						||
| 
								 | 
							
								my $jobset_gitea = $project->jobsets->create({
							 | 
						||
| 
								 | 
							
								    name => "test-jobset-gitea",
							 | 
						||
| 
								 | 
							
								    nixexprinput => "src",
							 | 
						||
| 
								 | 
							
								    nixexprpath => "default.nix",
							 | 
						||
| 
								 | 
							
								    emailoverride => ""
							 | 
						||
| 
								 | 
							
								});
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								my $jobsetinput_gitea = $jobset_gitea->jobsetinputs->create({name => "src", type => "git"});
							 | 
						||
| 
								 | 
							
								$jobsetinput_gitea->jobsetinputalts->create({altnr => 0, value => "https://gitea.example.com/owner/repo.git"});
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								subtest "GitHub webhook authentication" => sub {
							 | 
						||
| 
								 | 
							
								    my $payload = encode_json({
							 | 
						||
| 
								 | 
							
								        repository => {
							 | 
						||
| 
								 | 
							
								            owner => { name => "owner" },
							 | 
						||
| 
								 | 
							
								            name => "repo"
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    subtest "without authentication - properly rejects" => sub {
							 | 
						||
| 
								 | 
							
								        my $req = POST '/api/push-github',
							 | 
						||
| 
								 | 
							
								            "Content-Type" => "application/json",
							 | 
						||
| 
								 | 
							
								            "Content" => $payload;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        my $response = request($req);
							 | 
						||
| 
								 | 
							
								        is($response->code, 401, "Unauthenticated request is rejected");
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        my $data = decode_json($response->content);
							 | 
						||
| 
								 | 
							
								        is($data->{error}, "Missing webhook signature", "Proper error message for missing signature");
							 | 
						||
| 
								 | 
							
								    };
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    subtest "with valid signature" => sub {
							 | 
						||
| 
								 | 
							
								        my $signature = "sha256=" . hmac_sha256_hex($payload, $github_secret);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        my $req = POST '/api/push-github',
							 | 
						||
| 
								 | 
							
								            "Content-Type" => "application/json",
							 | 
						||
| 
								 | 
							
								            "X-Hub-Signature-256" => $signature,
							 | 
						||
| 
								 | 
							
								            "Content" => $payload;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        my $response = request($req);
							 | 
						||
| 
								 | 
							
								        is($response->code, 200, "Valid signature is accepted");
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if ($response->code != 200) {
							 | 
						||
| 
								 | 
							
								            diag("Error response: " . $response->content);
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        my $data = decode_json($response->content);
							 | 
						||
| 
								 | 
							
								        is($data->{jobsetsTriggered}, ["webhook-test:test-jobset"], "Jobset was triggered with valid authentication");
							 | 
						||
| 
								 | 
							
								    };
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    subtest "with invalid signature" => sub {
							 | 
						||
| 
								 | 
							
								        my $signature = "sha256=" . hmac_sha256_hex($payload, "wrong-secret");
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        my $req = POST '/api/push-github',
							 | 
						||
| 
								 | 
							
								            "Content-Type" => "application/json",
							 | 
						||
| 
								 | 
							
								            "X-Hub-Signature-256" => $signature,
							 | 
						||
| 
								 | 
							
								            "Content" => $payload;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        my $response = request($req);
							 | 
						||
| 
								 | 
							
								        is($response->code, 401, "Invalid signature is rejected");
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        my $data = decode_json($response->content);
							 | 
						||
| 
								 | 
							
								        is($data->{error}, "Invalid webhook signature", "Proper error message for invalid signature");
							 | 
						||
| 
								 | 
							
								    };
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    subtest "with second valid secret (multiple secrets configured)" => sub {
							 | 
						||
| 
								 | 
							
								        my $signature = "sha256=" . hmac_sha256_hex($payload, $github_secret_alt);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        my $req = POST '/api/push-github',
							 | 
						||
| 
								 | 
							
								            "Content-Type" => "application/json",
							 | 
						||
| 
								 | 
							
								            "X-Hub-Signature-256" => $signature,
							 | 
						||
| 
								 | 
							
								            "Content" => $payload;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        my $response = request($req);
							 | 
						||
| 
								 | 
							
								        is($response->code, 200, "Second valid secret is accepted");
							 | 
						||
| 
								 | 
							
								    };
							 | 
						||
| 
								 | 
							
								};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								subtest "Gitea webhook authentication" => sub {
							 | 
						||
| 
								 | 
							
								    my $payload = encode_json({
							 | 
						||
| 
								 | 
							
								        repository => {
							 | 
						||
| 
								 | 
							
								            owner => { username => "owner" },
							 | 
						||
| 
								 | 
							
								            name => "repo",
							 | 
						||
| 
								 | 
							
								            clone_url => "https://gitea.example.com/owner/repo.git"
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    subtest "without authentication - properly rejects" => sub {
							 | 
						||
| 
								 | 
							
								        my $req = POST '/api/push-gitea',
							 | 
						||
| 
								 | 
							
								            "Content-Type" => "application/json",
							 | 
						||
| 
								 | 
							
								            "Content" => $payload;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        my $response = request($req);
							 | 
						||
| 
								 | 
							
								        is($response->code, 401, "Unauthenticated request is rejected");
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        my $data = decode_json($response->content);
							 | 
						||
| 
								 | 
							
								        is($data->{error}, "Missing webhook signature", "Proper error message for missing signature");
							 | 
						||
| 
								 | 
							
								    };
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    subtest "with valid signature" => sub {
							 | 
						||
| 
								 | 
							
								        # Note: Gitea doesn't use sha256= prefix
							 | 
						||
| 
								 | 
							
								        my $signature = hmac_sha256_hex($payload, $gitea_secret);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        my $req = POST '/api/push-gitea',
							 | 
						||
| 
								 | 
							
								            "Content-Type" => "application/json",
							 | 
						||
| 
								 | 
							
								            "X-Gitea-Signature" => $signature,
							 | 
						||
| 
								 | 
							
								            "Content" => $payload;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        my $response = request($req);
							 | 
						||
| 
								 | 
							
								        is($response->code, 200, "Valid signature is accepted");
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if ($response->code != 200) {
							 | 
						||
| 
								 | 
							
								            diag("Error response: " . $response->content);
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        my $data = decode_json($response->content);
							 | 
						||
| 
								 | 
							
								        is($data->{jobsetsTriggered}, ["webhook-test:test-jobset-gitea"], "Jobset was triggered with valid authentication");
							 | 
						||
| 
								 | 
							
								    };
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    subtest "with invalid signature" => sub {
							 | 
						||
| 
								 | 
							
								        my $signature = hmac_sha256_hex($payload, "wrong-secret");
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        my $req = POST '/api/push-gitea',
							 | 
						||
| 
								 | 
							
								            "Content-Type" => "application/json",
							 | 
						||
| 
								 | 
							
								            "X-Gitea-Signature" => $signature,
							 | 
						||
| 
								 | 
							
								            "Content" => $payload;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        my $response = request($req);
							 | 
						||
| 
								 | 
							
								        is($response->code, 401, "Invalid signature is rejected");
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        my $data = decode_json($response->content);
							 | 
						||
| 
								 | 
							
								        is($data->{error}, "Invalid webhook signature", "Proper error message for invalid signature");
							 | 
						||
| 
								 | 
							
								    };
							 | 
						||
| 
								 | 
							
								};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								done_testing;
							 |