diff --git a/app/controllers/webhooks_controller.rb b/app/controllers/webhooks_controller.rb new file mode 100644 index 000000000..65299121b --- /dev/null +++ b/app/controllers/webhooks_controller.rb @@ -0,0 +1,51 @@ +class WebhooksController < ApplicationController + include FilterScoped + + before_action :set_webhook, except: %i[ index new create ] + + def index + set_page_and_extract_portion_from Webhook.all.ordered + end + + def show + end + + def new + @webhook = Webhook.new + end + + def create + @webhook = Webhook.new(webhook_params) + + if @webhook.save + redirect_to @webhook, status: :see_other + else + render :new, status: :unprocessable_entity + end + end + + def edit + end + + def update + if @webhook.update(webhook_params.except(:url)) + redirect_to @webhook, status: :see_other + else + render :edit, status: :unprocessable_entity + end + end + + def destroy + @webhook.destroy! + redirect_to webhooks_path, status: :see_other + end + + private + def set_webhook + @webhook = Webhook.find(params[:id]) + end + + def webhook_params + params.require(:webhook).permit(:name, :url, subscribed_actions: []) + end +end diff --git a/app/models/webhook.rb b/app/models/webhook.rb index 6878b62b3..50bee846f 100644 --- a/app/models/webhook.rb +++ b/app/models/webhook.rb @@ -1,17 +1,46 @@ class Webhook < ApplicationRecord PERMITTED_SCHEMES = %w[ http https ].freeze + PERMITTED_ACTIONS = %w[ + card_assigned + card_boosted + card_closed + card_collection_changed + card_created + card_popped + card_published + card_reopened + card_staged + card_title_changed + card_unassigned + card_unstaged + comment_created + ].freeze encrypts :signing_secret has_secure_token :signing_secret has_many :deliveries, dependent: :delete_all + serialize :subscribed_actions, type: Array, coder: JSON + + scope :ordered, -> { order(name: :asc, id: :desc) } + scope :triggered_by_action, ->(action) { where("subscribed_actions LIKE ?", "%\"#{action}\"%") } + + # normalizes :subscribed_actions, with: ->(value) { Array(value).map(&:to_s).map(&:strip).reject(&:blank?).uniq & PERMITTED_ACTIONS } + + validates :name, presence: true + validates :subscribed_actions, presence: true + validate :validate_url def deactivate update_columns active: false end + def trigger(event) + deliveries.create!(event: event).deliver_later + end + private def validate_url uri = URI.parse(url.presence) diff --git a/app/models/webhook/delivery.rb b/app/models/webhook/delivery.rb index 5e3252dea..b5baa51fd 100644 --- a/app/models/webhook/delivery.rb +++ b/app/models/webhook/delivery.rb @@ -3,29 +3,22 @@ class Webhook::Delivery < ApplicationRecord ENDPOINT_TIMEOUT = 7.seconds DNS_RESOLUTION_TIMEOUT = 2 PRIVATE_IP_RANGES = [ - # Loopback - IPAddr.new("127.0.0.0/8"), - IPAddr.new("::1/128"), # IPv4 mapped to IPv6 IPAddr.new("::ffff:0:0/96"), - # RFC1918 - local network IP addresses - IPAddr.new("10.0.0.0/8"), - IPAddr.new("172.16.0.0/12"), - IPAddr.new("192.168.0.0/16"), # Link-local (DHCP and router stuff) IPAddr.new("169.254.0.0/16"), - IPAddr.new("fe80::/10"), - # ULA - IPAddr.new("fc00::/7") + IPAddr.new("fe80::/10") ].freeze belongs_to :webhook belongs_to :event + encrypts :request, :response + store :request, coder: JSON store :response, coder: JSON - encrypts :request, :response + scope :chronologically, -> { order created_at: :asc, id: :desc } enum :state, %w[ pending in_progress completed errored ].index_by(&:itself), default: :pending @@ -80,10 +73,8 @@ class Webhook::Delivery < ApplicationRecord end end - ip_addresses.any? do |ip_address| - PRIVATE_IP_RANGES.any? do |private_ip_range| - private_ip_range.include?(ip_address) - end + ip_addresses.any? do |ip| + ip.private? || ip.loopback? || PRIVATE_IP_RANGES.any? { |range| range.include?(ip) } end end diff --git a/app/views/account/settings/show.html.erb b/app/views/account/settings/show.html.erb index 6d41d439d..6ab7e3bb7 100644 --- a/app/views/account/settings/show.html.erb +++ b/app/views/account/settings/show.html.erb @@ -4,6 +4,10 @@ <%= render "filters/menu", user_filtering: @user_filtering %>
+ <%= @webhook.signing_secret %> +
++ We'll sent a `X-Webhook-Signature` header with each request. + You can generate a HMAC using SHA256 of the request body with this secret + to verify that the request came from us. +
+This Webhook hasn't been triggered yet
+ <% else %> +