Skip to content

Commit c72aaa8

Browse files
authored
fix(github_hooks): improve GitHub App webhook handling (#82)
1 parent c0cf817 commit c72aaa8

File tree

9 files changed

+97
-32
lines changed

9 files changed

+97
-32
lines changed

github_hooks/app/controllers/projects_controller.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ def repo_host_post_commit_hook
2020
head :ok and return
2121
end
2222

23+
if webhook_filter.github_app_webhook? && App.check_github_app_webhook?
24+
signature = repo_host_request.headers["X-Hub-Signature-256"]
25+
secret = Semaphore::GithubApp::Credentials.github_app_webhook_secret
26+
27+
if Semaphore::GithubApp::Hook.webhook_signature_valid?(secret, signature, repo_host_request.body.string) != :ok
28+
logger.error("Webhook validation failed")
29+
head :not_found and return
30+
end
31+
end
32+
2333
if webhook_filter.github_app_installation_webhook?
2434
Watchman.increment("repo_host_post_commit_hooks.controller.github_app_webhook")
2535
logger.info("Github App Webhook")

github_hooks/config/app/development.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
config.github_app_id = SemaphoreConfig.github_app_id
1313
config.github_secret_id = SemaphoreConfig.github_secret_id
14+
config.github_app_webhook_secret = SemaphoreConfig.github_app_webhook_secret
15+
config.check_github_app_webhook = SemaphoreConfig.check_github_app_webhook
1416

1517
config.bitbucket_app_id = SemaphoreConfig.bitbucket_app_id
1618
config.bitbucket_secret_id = SemaphoreConfig.bitbucket_secret_id

github_hooks/config/app/production.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
config.github_app_id = SemaphoreConfig.github_app_id
1515
config.github_secret_id = SemaphoreConfig.github_secret_id
16+
config.github_app_webhook_secret = SemaphoreConfig.github_app_webhook_secret
17+
config.check_github_app_webhook = SemaphoreConfig.check_github_app_webhook.to_s == "true"
1618

1719
config.bitbucket_app_id = SemaphoreConfig.bitbucket_app_id
1820
config.bitbucket_secret_id = SemaphoreConfig.bitbucket_secret_id

github_hooks/config/app/test.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
config.github_app_id = "bd59c3a0c448179b5f3f"
1111
config.github_secret_id = "c40e646d16dca15d4a5155397e4e66b928678f15"
12+
config.github_app_webhook_secret = "lkasjdlkjKSJHKsa123lskdfn"
13+
config.check_github_app_webhook = true
1214

1315
config.bitbucket_app_id = "G3cXBDsDEwVp25rCXL"
1416
config.bitbucket_secret_id = "LNNfhaLKsfuzjYEeJLkN5Y93cNDb2ej4"

github_hooks/config/config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ secret_token: "jfieitmv83yfv73osjkdjfvy8213kskjdfo834jv"
99

1010
github_app_id: "bd59c3a0c448179b5f3f"
1111
github_secret_id: "c40e646d16dca15d4a5155397e4e66b928678f15"
12+
github_app_webhook_secret: "lkasjdlkjKSJHKsa123lskdfn"
1213

1314
bitbucket_app_id: "G3cXBDsDEwVp25rCXL"
1415
bitbucket_secret_id: "LNNfhaLKsfuzjYEeJLkN5Y93cNDb2ej4"

github_hooks/lib/semaphore/github_app/credentials.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module Semaphore::GithubApp::Credentials
44
@github_application_id = nil
55
@github_client_id = nil
66
@github_client_secret = nil
7+
@github_app_webhook_secret = nil
78

89
def self.private_key
910
@private_key ||= Local.pem || InstanceConfigClient.pem
@@ -25,6 +26,10 @@ def self.github_client_secret
2526
@github_client_secret ||= InstanceConfigClient.github_client_secret || Local.github_client_secret
2627
end
2728

29+
def self.github_app_webhook_secret
30+
@github_app_webhook_secret ||= InstanceConfigClient.github_app_webhook_secret || Local.github_app_webhook_secret
31+
end
32+
2833
class Local
2934
def self.pem
3035
if File.exist?(App.github_application_key_path) # function
@@ -47,6 +52,10 @@ def self.github_client_id
4752
def self.github_client_secret
4853
App.github_secret_id.presence
4954
end
55+
56+
def self.github_app_webhook_secret
57+
App.github_app_webhook_secret.presence
58+
end
5059
end
5160

5261
class InstanceConfigClient
@@ -92,6 +101,10 @@ def self.github_client_id
92101
get_field("client_id")
93102
end
94103

104+
def self.github_app_webhook_secret
105+
get_field("webhook_secret")
106+
end
107+
95108
def self.github_client_secret
96109
get_field("client_secret")
97110
end

github_hooks/lib/semaphore/github_app/hook.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,5 +124,16 @@ def self.remove_repositories(installation_id, repositories)
124124
def self.get_installation(installation_id)
125125
GithubAppInstallation.find_by!(:installation_id => installation_id)
126126
end
127+
128+
def self.webhook_signature_valid?(secret, signature, payload)
129+
signing_secret = secret
130+
computed_signature = "sha256=#{OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), signing_secret, payload)}"
131+
132+
if Rack::Utils.secure_compare(computed_signature, signature)
133+
:ok
134+
else
135+
:not_verified
136+
end
137+
end
127138
end
128139
end

github_hooks/spec/controllers/projects_controller_spec.rb

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ def post_app_payload(payload)
2626
context "when GitHub hook" do
2727
let(:workflow) { double(Workflow, :id => 111, :update_attribute => nil) }
2828
let(:payload) { RepoHost::Github::Responses::Payload.post_receive_hook_pull_request }
29+
let(:signature) do
30+
"sha256=#{OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), "secret", payload)}"
31+
end
2932
let(:payload_request) do
3033
body = StringIO.new
3134
body.puts payload
@@ -34,14 +37,15 @@ def post_app_payload(payload)
3437
"User-Agent" => "GitHub-Hookshot/xxx",
3538
"X-Github-Event" => event,
3639
"X-GitHub-Hook-Installation-Target-Type" => installation_target_type,
37-
"X-Hub-Signature-256" => "sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17"
40+
"X-Hub-Signature-256" => signature
3841
}, :body => body, :raw_post => "Hello, World!")
3942
end
4043

4144
before do
4245
allow(controller).to receive(:repo_host_request) { payload_request }
4346
allow(Semaphore::RepoHost::Hooks::Recorder).to receive(:record_hook).and_return(workflow)
4447
allow(Semaphore::RepoHost::Hooks::Handler).to receive(:enqueue_job)
48+
allow(Semaphore::GithubApp::Credentials).to receive(:github_app_webhook_secret).and_return("secret")
4549

4650
@organization = FactoryBot.create(:organization)
4751
@project = FactoryBot.create(:project,
@@ -110,7 +114,7 @@ def post_app_payload(payload)
110114

111115
it "calls execute on post commit service" do
112116
expect(Semaphore::RepoHost::Hooks::Recorder).to receive(:record_hook).and_return(workflow)
113-
expect(Semaphore::RepoHost::Hooks::Handler::Worker).to receive(:perform_async).with(111, "Hello, World!", "sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17", 0)
117+
expect(Semaphore::RepoHost::Hooks::Handler::Worker).to receive(:perform_async).with(111, "Hello, World!", signature, 0)
114118

115119
post_payload(payload)
116120
end
@@ -148,61 +152,62 @@ def post_app_payload(payload)
148152

149153
context "when issue comment event occurs" do
150154
let(:event) { "issue_comment" }
155+
let(:payload) { RepoHost::Github::Responses::Payload.post_receive_hook_issue_comment }
151156

152157
it "doesn't required authenticated_user" do
153158
expect(controller).not_to receive(:authenticate_user!)
154159

155-
post_payload(RepoHost::Github::Responses::Payload.post_receive_hook_issue_comment)
160+
post_payload(payload)
156161
end
157162

158163
it "doesn't publish event" do
159164
expect(Tackle).not_to receive(:publish)
160165
expect(Semaphore::GithubApp::Collaborators::Worker).not_to receive(:perform_async)
161166

162-
post_payload(RepoHost::Github::Responses::Payload.post_receive_hook_issue_comment)
167+
post_payload(payload)
163168
end
164169

165170
it "saves the hook payload" do
166171
expect(Semaphore::RepoHost::Hooks::Recorder).to receive(:record_hook)
167172

168-
post_payload(RepoHost::Github::Responses::Payload.post_receive_hook_issue_comment)
173+
post_payload(payload)
169174
end
170175

171176
it "calls execute on post commit service" do
172177
expect(Semaphore::RepoHost::Hooks::Recorder).to receive(:record_hook).and_return(workflow)
173-
expect(Semaphore::RepoHost::Hooks::Handler::Worker).to receive(:perform_async).with(111, "Hello, World!", "sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17", 0)
178+
expect(Semaphore::RepoHost::Hooks::Handler::Worker).to receive(:perform_async).with(111, "Hello, World!", signature, 0)
174179

175-
post_payload(RepoHost::Github::Responses::Payload.post_receive_hook_issue_comment)
180+
post_payload(payload)
176181
end
177182

178183
it "saves workflow's result as OK" do
179184
expect(workflow).to receive(:update_attribute).with(:result, Workflow::RESULT_OK)
180185

181-
post_payload(RepoHost::Github::Responses::Payload.post_receive_hook_issue_comment)
186+
post_payload(payload)
182187
end
183188

184189
it "responds with OK" do
185-
post_payload(RepoHost::Github::Responses::Payload.post_receive_hook_issue_comment)
190+
post_payload(payload)
186191

187192
expect(response).to be_ok
188193
end
189194

190195
it "measures the execution duration" do
191196
expect(Watchman).to receive(:benchmark).with("repo_host_post_commit_hooks.controller.duration").and_call_original
192197

193-
post_payload(RepoHost::Github::Responses::Payload.post_receive_hook_issue_comment)
198+
post_payload(payload)
194199
end
195200

196201
it "increments the handled hooks count" do
197202
expect(Watchman).to receive(:increment).with("IncommingHooks.processed", { external: true }).and_call_original
198203

199-
post_payload(RepoHost::Github::Responses::Payload.post_receive_hook_issue_comment)
204+
post_payload(payload)
200205
end
201206

202207
it "increments the external metric for hooks count" do
203208
expect(Watchman).to receive(:increment).with("IncommingHooks.received", { external: true }).and_call_original
204209

205-
post_payload(RepoHost::Github::Responses::Payload.post_receive_hook_issue_comment)
210+
post_payload(payload)
206211
end
207212
end
208213

@@ -271,17 +276,18 @@ def post_app_payload(payload)
271276
context "when repository event occurs" do
272277
let(:event) { "repository" }
273278
let(:installation_target_type) { "integration" }
279+
let(:payload) { RepoHost::Github::Responses::Payload.repository_renamed_app_hook }
274280

275281
it "response with head ok" do
276-
post_payload(RepoHost::Github::Responses::Payload.repository_renamed_app_hook)
282+
post_payload(payload)
277283

278284
expect(response).to be_ok
279285
end
280286

281287
it "measures the execution duration" do
282288
expect(Watchman).to receive(:benchmark).with("repo_host_post_commit_hooks.controller.duration").and_call_original
283289

284-
post_payload(RepoHost::Github::Responses::Payload.repository_renamed_app_hook)
290+
post_payload(payload)
285291
end
286292

287293
it "increments the repository_webhook count" do
@@ -290,35 +296,36 @@ def post_app_payload(payload)
290296
expect(Watchman).to receive(:increment).with("repo_host_post_commit_hooks.controller.github_app_webhook").and_call_original
291297
expect(Watchman).to receive(:increment).with("repo_host_post_commit_hooks.controller.repository_webhook").and_call_original
292298

293-
post_payload(RepoHost::Github::Responses::Payload.repository_renamed_app_hook)
299+
post_payload(payload)
294300
end
295301

296302
it "perform repository sync" do
297303
expect(Semaphore::GithubApp::Repositories::Worker).to receive(:perform_async).and_call_original
298304

299-
post_payload(RepoHost::Github::Responses::Payload.repository_renamed_app_hook)
305+
post_payload(payload)
300306
end
301307
end
302308

303309
context "when repository renamed event occurs" do
304310
let(:event) { "repository" }
311+
let(:payload) { RepoHost::Github::Responses::Payload.repository_renamed_hook }
305312

306313
it "response with head ok" do
307-
post_payload(RepoHost::Github::Responses::Payload.repository_renamed_hook)
314+
post_payload(payload)
308315

309316
expect(response).to be_ok
310317
end
311318

312319
it "measures the execution duration" do
313320
expect(Watchman).to receive(:benchmark).with("repo_host_post_commit_hooks.controller.duration").and_call_original
314321

315-
post_payload(RepoHost::Github::Responses::Payload.repository_renamed_hook)
322+
post_payload(payload)
316323
end
317324

318325
it "increments the repository_renamed_webhook count" do
319326
expect(Watchman).to receive(:increment).with("repo_host_post_commit_hooks.controller.repository_webhook").and_call_original
320327

321-
post_payload(RepoHost::Github::Responses::Payload.repository_renamed_hook)
328+
post_payload(payload)
322329
end
323330

324331
it "publish event" do
@@ -328,29 +335,30 @@ def post_app_payload(payload)
328335

329336
expect(Semaphore::Events::RemoteRepositoryChanged).to receive(:emit).and_call_original
330337

331-
post_payload(RepoHost::Github::Responses::Payload.repository_renamed_hook)
338+
post_payload(payload)
332339
end
333340
end
334341

335342
context "when default branch changed event occurs" do
336343
let(:event) { "repository" }
344+
let(:payload) { RepoHost::Github::Responses::Payload.default_branch_changed }
337345

338346
it "response with head ok" do
339-
post_payload(RepoHost::Github::Responses::Payload.default_branch_changed)
347+
post_payload(payload)
340348

341349
expect(response).to be_ok
342350
end
343351

344352
it "measures the execution duration" do
345353
expect(Watchman).to receive(:benchmark).with("repo_host_post_commit_hooks.controller.duration").and_call_original
346354

347-
post_payload(RepoHost::Github::Responses::Payload.default_branch_changed)
355+
post_payload(payload)
348356
end
349357

350358
it "increments the default_branch_changed count" do
351359
expect(Watchman).to receive(:increment).with("repo_host_post_commit_hooks.controller.repository_webhook").and_call_original
352360

353-
post_payload(RepoHost::Github::Responses::Payload.default_branch_changed)
361+
post_payload(payload)
354362
end
355363

356364
it "publish event" do
@@ -360,63 +368,65 @@ def post_app_payload(payload)
360368

361369
expect(Semaphore::Events::RemoteRepositoryChanged).to receive(:emit).and_call_original
362370

363-
post_payload(RepoHost::Github::Responses::Payload.default_branch_changed)
371+
post_payload(payload)
364372
end
365373
end
366374

367375
context "when github_app installation event occurs" do
368376
let(:event) { "installation" }
377+
let(:payload) { RepoHost::Github::Responses::Payload.installation_created }
369378

370379
it "response with head ok" do
371-
post_app_payload(RepoHost::Github::Responses::Payload.installation_created)
380+
post_app_payload(payload)
372381

373382
expect(response).to be_ok
374383
end
375384

376385
it "measures the execution duration" do
377386
expect(Watchman).to receive(:benchmark).with("repo_host_post_commit_hooks.controller.duration").and_call_original
378387

379-
post_app_payload(RepoHost::Github::Responses::Payload.installation_created)
388+
post_app_payload(payload)
380389
end
381390

382391
it "increments the github_app_webhook count" do
383392
expect(Watchman).to receive(:increment).with("repo_host_post_commit_hooks.controller.github_app_webhook").and_call_original
384393

385-
post_app_payload(RepoHost::Github::Responses::Payload.installation_created)
394+
post_app_payload(payload)
386395
end
387396

388397
it "calls GithubApp hook processor" do
389398
expect(Semaphore::GithubApp::Hook).to receive(:process).and_call_original
390399

391-
post_app_payload(RepoHost::Github::Responses::Payload.installation_created)
400+
post_app_payload(payload)
392401
end
393402
end
394403

395404
context "when github_app installation_repositories event occurs" do
396405
let(:event) { "installation_repositories" }
406+
let(:payload) { RepoHost::Github::Responses::Payload.installation_repositories_added }
397407

398408
it "response with head ok" do
399-
post_app_payload(RepoHost::Github::Responses::Payload.installation_repositories_added)
409+
post_app_payload(payload)
400410

401411
expect(response).to be_ok
402412
end
403413

404414
it "measures the execution duration" do
405415
expect(Watchman).to receive(:benchmark).with("repo_host_post_commit_hooks.controller.duration").and_call_original
406416

407-
post_app_payload(RepoHost::Github::Responses::Payload.installation_repositories_added)
417+
post_app_payload(payload)
408418
end
409419

410420
it "increments the github_app_webhook count" do
411421
expect(Watchman).to receive(:increment).with("repo_host_post_commit_hooks.controller.github_app_webhook").and_call_original
412422

413-
post_app_payload(RepoHost::Github::Responses::Payload.installation_repositories_added)
423+
post_app_payload(payload)
414424
end
415425

416426
it "calls GithubApp hook processor" do
417427
expect(Semaphore::GithubApp::Hook).to receive(:process).and_call_original
418428

419-
post_app_payload(RepoHost::Github::Responses::Payload.installation_repositories_added)
429+
post_app_payload(payload)
420430
end
421431
end
422432

0 commit comments

Comments
 (0)