Skip to content

Commit

Permalink
add /webhooks
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrielburnworth committed Sep 17, 2024
1 parent cd9db34 commit 771fac5
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 0 deletions.
62 changes: 62 additions & 0 deletions app/controllers/webhooks_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
class WebhooksController < ApplicationController

skip_before_action :verify_authenticity_token

def create
request_body = request.raw_post
heroku_signature = request.headers['Heroku-Webhook-Hmac-SHA256']
secret = ENV['HEROKU_WEBHOOK_SECRET']

if secret.nil?
render json: { message: 'Secret not set' }, status: :forbidden
return
end
calculated_signature = Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', secret, request_body))

if heroku_signature && secure_compare(calculated_signature, heroku_signature)
webhook_url = ENV["RELEASE_WEBHOOK_URL"]
current = params[:data][:current]
status = params[:data][:status]
build = params[:resource] == "build" && params[:action] == "create" && status == "pending"
if webhook_url && (current || build)
environment = ENV["HEROKU_APP_NAME"]&.include?("staging") ? "staging" : "production"
notification_text = "New #{environment} Heroku event: "
if build
output = params[:data][:output_stream_url]
notification_text += "Build started"
details = "<#{output}|build log>"
else
notification_text += "#{params[:data][:description]} `#{status}`"
commit_desc = params[:data][:slug][:commit_description]
details = commit_desc
end
payload = {
"mrkdwn": true,
"text": notification_text,
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "#{notification_text}\n#{details}",
}
}
],
"channel": "#software",
}.to_json
Faraday.post(webhook_url,
payload,
"Content-Type" => "application/json")
end
render json: { message: 'Webhook received successfully' }, status: :ok
else
render json: { message: 'Invalid signature' }, status: :forbidden
end
end

private

def secure_compare(calculated_signature, heroku_signature)
ActiveSupport::SecurityUtils.secure_compare(calculated_signature, heroku_signature)
end
end
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,5 @@
get "/verify/:token" => "dashboard#confirmation_page", as: :confirmation_page
post "/csp_reports" => "dashboard#csp_reports", as: :csp_report
post "/direct_upload" => "dashboard#direct_upload", as: :direct_upload
post "/webhooks" => "webhooks#create", as: :webhooks
end
115 changes: 115 additions & 0 deletions spec/controllers/webhooks_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
require "spec_helper"

describe WebhooksController do
include Devise::Test::ControllerHelpers
before do
allow(Faraday).to receive(:post)
end

RELEASE = {
id: "id",
data: {
status: "succeeded",
current: true,
description: "description",
slug: {
commit_description: "commit_description",
},
},
resource: "release",
action: "update",
}
BUILD = {
id: "id",
data: {
status: "pending",
output_stream_url: "output_stream_url",
},
resource: "build",
action: "create",
}

it "handles a release payload" do
with_modified_env(
RELEASE_WEBHOOK_URL: "https://example.com/webhook_url",
HEROKU_WEBHOOK_SECRET: "secret",
) do
stub_request(:post, ENV["RELEASE_WEBHOOK_URL"]).
to_return(status: 200, body: "", headers: {})
request.headers["Heroku-Webhook-Hmac-SHA256"] = Base64.strict_encode64(
OpenSSL::HMAC.digest("sha256", "secret", RELEASE.to_json))
request.headers["Content-Type"] = "application/json"
post :create, body: RELEASE.to_json, params: { format: :json }
expect(response.status).to eq(200)
expect(json[:message]).to eq("Webhook received successfully")
expect(Faraday).to have_received(:post).with(
ENV["RELEASE_WEBHOOK_URL"],
{"mrkdwn":true,
"text":"New production Heroku event: description `succeeded`",
"blocks":[{
"type":"section",
"text":{
"type":"mrkdwn",
"text":"New production Heroku event: description `succeeded`\ncommit_description"}}],
"channel":"#software"}.to_json,
"Content-Type" => "application/json")
end
end

it "handles a build payload" do
with_modified_env(
RELEASE_WEBHOOK_URL: "https://example.com/webhook_url",
HEROKU_WEBHOOK_SECRET: "secret",
) do
stub_request(:post, ENV["RELEASE_WEBHOOK_URL"]).
to_return(status: 200, body: "", headers: {})
request.headers["Heroku-Webhook-Hmac-SHA256"] = Base64.strict_encode64(
OpenSSL::HMAC.digest("sha256", "secret", BUILD.to_json))
request.headers["Content-Type"] = "application/json"
post :create, body: BUILD.to_json, params: { format: :json }
expect(response.status).to eq(200)
expect(json[:message]).to eq("Webhook received successfully")
expect(Faraday).to have_received(:post).with(
ENV["RELEASE_WEBHOOK_URL"],
{"mrkdwn":true,
"text":"New production Heroku event: Build started",
"blocks":[{
"type":"section",
"text":{
"type":"mrkdwn",
"text":"New production Heroku event: Build started\n\u003coutput_stream_url|build log\u003e"}}],
"channel":"#software"}.to_json,
"Content-Type" => "application/json")
end
end

it "receives a webhook: no relay" do
with_modified_env(HEROKU_WEBHOOK_SECRET: "secret", RELEASE_WEBHOOK_URL: nil) do
request.headers["Heroku-Webhook-Hmac-SHA256"] = Base64.strict_encode64(
OpenSSL::HMAC.digest("sha256", "secret", RELEASE.to_json))
request.headers["Content-Type"] = "application/json"
post :create, body: RELEASE.to_json, params: { format: :json }
expect(response.status).to eq(200)
expect(json[:message]).to eq("Webhook received successfully")
end
end

it "rejects a webhook with an invalid signature" do
with_modified_env HEROKU_WEBHOOK_SECRET: "secret" do
request.headers["Heroku-Webhook-Hmac-SHA256"] = "invalid"
request.headers["Content-Type"] = "application/json"
post :create, body: RELEASE.to_json, params: { format: :json }
expect(response.status).to eq(403)
expect(json[:message]).to eq("Invalid signature")
end
end

it "rejects a webhook with a missing secret" do
with_modified_env HEROKU_WEBHOOK_SECRET: nil do
request.headers["Content-Type"] = "application/json"
post :create, body: RELEASE.to_json, params: { format: :json }
expect(response.status).to eq(403)
expect(json[:message]).to eq("Secret not set")
end
end
end

0 comments on commit 771fac5

Please sign in to comment.