bullet_train-outgoing_webhooks 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/MIT-LICENSE +1 -1
- data/app/controllers/account/webhooks/outgoing/endpoints_controller.rb +4 -3
- data/app/controllers/api/v1/webhooks/outgoing/endpoints_endpoint.rb +7 -7
- data/app/jobs/webhooks/outgoing/generate_job.rb +7 -0
- data/app/models/concerns/webhooks/outgoing/delivery_attempt_support.rb +64 -0
- data/app/models/concerns/webhooks/outgoing/delivery_support.rb +66 -0
- data/app/models/concerns/webhooks/outgoing/endpoint_support.rb +37 -0
- data/app/models/concerns/webhooks/outgoing/event_support.rb +45 -0
- data/app/models/concerns/webhooks/outgoing/issuing_model.rb +35 -17
- data/app/models/concerns/webhooks/outgoing/team_support.rb +16 -0
- data/app/models/concerns/webhooks/outgoing/uri_filtering.rb +149 -0
- data/app/models/webhooks/outgoing/delivery.rb +2 -61
- data/app/models/webhooks/outgoing/delivery_attempt.rb +2 -47
- data/app/models/webhooks/outgoing/endpoint.rb +2 -14
- data/app/models/webhooks/outgoing/event.rb +2 -40
- data/app/views/account/webhooks/outgoing/deliveries/_menu_item.html.erb +2 -2
- data/app/views/account/webhooks/outgoing/delivery_attempts/_menu_item.html.erb +2 -2
- data/app/views/account/webhooks/outgoing/endpoints/_breadcrumbs.html.erb +1 -1
- data/app/views/account/webhooks/outgoing/endpoints/_form.html.erb +2 -2
- data/app/views/account/webhooks/outgoing/endpoints/_index.html.erb +3 -3
- data/app/views/account/webhooks/outgoing/endpoints/_menu_item.html.erb +2 -2
- data/app/views/account/webhooks/outgoing/endpoints/show.html.erb +1 -1
- data/config/routes.rb +1 -1
- data/lib/bullet_train/outgoing_webhooks/engine.rb +26 -1
- data/lib/bullet_train/outgoing_webhooks/version.rb +1 -1
- data/lib/bullet_train/outgoing_webhooks.rb +26 -1
- metadata +39 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a4d3884adf8d199b92ccf09ca749e8237e86a421224de5b8b81cf5ab0d9f4329
|
4
|
+
data.tar.gz: cec7b468644c484f8bcb0f4064b74764ab3e7b8873cb72ba5c8944009b95a576
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 98d7265e65165a0c2b55a0b2880063f00c72292e0a461e4bde667d54f077860f1b4861873f5131bd203e45b85ab03868822970071e626c34e2ed640059e5cc11
|
7
|
+
data.tar.gz: f36d57ccd33ba9268a106ef22c1457560f9b171ff0533b4f8102301e5962691f827ffa437c7f13d3fd7ab847b1b626a9c6374cca45efbbcab228caf7c265d2a9
|
data/MIT-LICENSE
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
class Account::Webhooks::Outgoing::EndpointsController < Account::ApplicationController
|
2
|
-
account_load_and_authorize_resource :endpoint, through:
|
2
|
+
account_load_and_authorize_resource :endpoint, through: BulletTrain::OutgoingWebhooks.parent_association, through_association: :webhooks_outgoing_endpoints
|
3
|
+
before_action { @parent = instance_variable_get("@#{BulletTrain::OutgoingWebhooks.parent_association}") }
|
3
4
|
|
4
5
|
# GET /account/teams/:team_id/webhooks/outgoing/endpoints
|
5
6
|
# GET /account/teams/:team_id/webhooks/outgoing/endpoints.json
|
@@ -26,7 +27,7 @@ class Account::Webhooks::Outgoing::EndpointsController < Account::ApplicationCon
|
|
26
27
|
def create
|
27
28
|
respond_to do |format|
|
28
29
|
if @endpoint.save
|
29
|
-
format.html { redirect_to [:account, @
|
30
|
+
format.html { redirect_to [:account, @parent, :webhooks_outgoing_endpoints], notice: I18n.t("webhooks/outgoing/endpoints.notifications.created") }
|
30
31
|
format.json { render :show, status: :created, location: [:account, @endpoint] }
|
31
32
|
else
|
32
33
|
format.html { render :new, status: :unprocessable_entity }
|
@@ -54,7 +55,7 @@ class Account::Webhooks::Outgoing::EndpointsController < Account::ApplicationCon
|
|
54
55
|
def destroy
|
55
56
|
@endpoint.destroy
|
56
57
|
respond_to do |format|
|
57
|
-
format.html { redirect_to [:account, @
|
58
|
+
format.html { redirect_to [:account, @parent, :webhooks_outgoing_endpoints], notice: I18n.t("webhooks/outgoing/endpoints.notifications.destroyed") }
|
58
59
|
format.json { head :no_content }
|
59
60
|
end
|
60
61
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
class Api::V1::Webhooks::Outgoing::EndpointsEndpoint < Api::V1::Root
|
2
2
|
helpers do
|
3
|
-
params
|
4
|
-
requires
|
3
|
+
params BulletTrain::OutgoingWebhooks.parent_association_id do
|
4
|
+
requires BulletTrain::OutgoingWebhooks.parent_association_id, type: Integer, allow_blank: false, desc: "#{BulletTrain::OutgoingWebhooks.parent_class} ID"
|
5
5
|
end
|
6
6
|
|
7
7
|
params :id do
|
@@ -19,7 +19,7 @@ class Api::V1::Webhooks::Outgoing::EndpointsEndpoint < Api::V1::Root
|
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
|
-
resource
|
22
|
+
resource BulletTrain::OutgoingWebhooks.parent_resource, desc: Api.title(:collection_actions) do
|
23
23
|
after_validation do
|
24
24
|
load_and_authorize_api_resource Webhooks::Outgoing::Endpoint
|
25
25
|
end
|
@@ -30,11 +30,11 @@ class Api::V1::Webhooks::Outgoing::EndpointsEndpoint < Api::V1::Root
|
|
30
30
|
|
31
31
|
desc Api.title(:index), &Api.index_desc
|
32
32
|
params do
|
33
|
-
use
|
33
|
+
use BulletTrain::OutgoingWebhooks.parent_association_id
|
34
34
|
end
|
35
35
|
oauth2
|
36
36
|
paginate per_page: 100
|
37
|
-
get "
|
37
|
+
get "/:#{BulletTrain::OutgoingWebhooks.parent_association_id}/webhooks/outgoing/endpoints" do
|
38
38
|
@paginated_endpoints = paginate @endpoints
|
39
39
|
render @paginated_endpoints, serializer: Api.serializer
|
40
40
|
end
|
@@ -45,12 +45,12 @@ class Api::V1::Webhooks::Outgoing::EndpointsEndpoint < Api::V1::Root
|
|
45
45
|
|
46
46
|
desc Api.title(:create), &Api.create_desc
|
47
47
|
params do
|
48
|
-
use
|
48
|
+
use BulletTrain::OutgoingWebhooks.parent_association_id
|
49
49
|
use :endpoint
|
50
50
|
end
|
51
51
|
route_setting :api_resource_options, permission: :create
|
52
52
|
oauth2 "write"
|
53
|
-
post "
|
53
|
+
post "/:#{BulletTrain::OutgoingWebhooks.parent_association_id}/webhooks/outgoing/endpoints" do
|
54
54
|
if @endpoint.save
|
55
55
|
render @endpoint, serializer: Api.serializer
|
56
56
|
else
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Webhooks::Outgoing::DeliveryAttemptSupport
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
include Webhooks::Outgoing::UriFiltering
|
4
|
+
|
5
|
+
SUCCESS_RESPONSE_CODES = [200, 201, 202, 203, 204, 205, 206, 207, 226].freeze
|
6
|
+
|
7
|
+
included do
|
8
|
+
belongs_to :delivery
|
9
|
+
has_one :team, through: :delivery unless BulletTrain::OutgoingWebhooks.parent_class_specified?
|
10
|
+
scope :successful, -> { where(response_code: SUCCESS_RESPONSE_CODES) }
|
11
|
+
|
12
|
+
before_create do
|
13
|
+
self.attempt_number = delivery.attempt_count + 1
|
14
|
+
end
|
15
|
+
|
16
|
+
validates :response_code, presence: true
|
17
|
+
end
|
18
|
+
|
19
|
+
def still_attempting?
|
20
|
+
error_message.nil? && response_code.nil?
|
21
|
+
end
|
22
|
+
|
23
|
+
def successful?
|
24
|
+
SUCCESS_RESPONSE_CODES.include?(response_code)
|
25
|
+
end
|
26
|
+
|
27
|
+
def failed?
|
28
|
+
!(successful? || still_attempting?)
|
29
|
+
end
|
30
|
+
|
31
|
+
def attempt
|
32
|
+
uri = URI.parse(delivery.endpoint_url)
|
33
|
+
|
34
|
+
unless allowed_uri?(uri)
|
35
|
+
self.response_code = 0
|
36
|
+
self.error_message = "URI is not allowed: " + uri
|
37
|
+
return false
|
38
|
+
end
|
39
|
+
|
40
|
+
http = Net::HTTP.new(resolve_ip_from_authoritative(uri.hostname.downcase), uri.port)
|
41
|
+
http.use_ssl = true if uri.scheme == "https"
|
42
|
+
request = Net::HTTP::Post.new(uri.path)
|
43
|
+
request.add_field("Host", uri.host)
|
44
|
+
request.add_field("Content-Type", "application/json")
|
45
|
+
request.body = delivery.event.payload.to_json
|
46
|
+
|
47
|
+
begin
|
48
|
+
response = http.request(request)
|
49
|
+
self.response_message = response.message
|
50
|
+
self.response_code = response.code
|
51
|
+
self.response_body = response.body
|
52
|
+
rescue => exception
|
53
|
+
self.response_code = 0
|
54
|
+
self.error_message = exception.message
|
55
|
+
end
|
56
|
+
|
57
|
+
save
|
58
|
+
successful?
|
59
|
+
end
|
60
|
+
|
61
|
+
def label_string
|
62
|
+
"#{attempt_number.ordinalize} Attempt"
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Webhooks::Outgoing::DeliverySupport
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
belongs_to :endpoint, class_name: "Webhooks::Outgoing::Endpoint"
|
6
|
+
belongs_to :event, class_name: "Webhooks::Outgoing::Event"
|
7
|
+
|
8
|
+
has_one :team, through: :endpoint unless BulletTrain::OutgoingWebhooks.parent_class_specified?
|
9
|
+
has_many :delivery_attempts, class_name: "Webhooks::Outgoing::DeliveryAttempt", dependent: :destroy, foreign_key: :delivery_id
|
10
|
+
end
|
11
|
+
|
12
|
+
ATTEMPT_SCHEDULE = {
|
13
|
+
1 => 15.seconds,
|
14
|
+
2 => 1.minute,
|
15
|
+
3 => 5.minutes,
|
16
|
+
4 => 15.minutes,
|
17
|
+
5 => 1.hour,
|
18
|
+
}
|
19
|
+
|
20
|
+
def label_string
|
21
|
+
event.short_uuid
|
22
|
+
end
|
23
|
+
|
24
|
+
def next_reattempt_delay
|
25
|
+
ATTEMPT_SCHEDULE[attempt_count]
|
26
|
+
end
|
27
|
+
|
28
|
+
def deliver_async
|
29
|
+
if still_attempting?
|
30
|
+
Webhooks::Outgoing::DeliveryJob.set(wait: next_reattempt_delay).perform_later(self)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def deliver
|
35
|
+
if delivery_attempts.create.attempt
|
36
|
+
touch(:delivered_at)
|
37
|
+
else
|
38
|
+
deliver_async
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def attempt_count
|
43
|
+
delivery_attempts.count
|
44
|
+
end
|
45
|
+
|
46
|
+
def delivered?
|
47
|
+
delivered_at.present?
|
48
|
+
end
|
49
|
+
|
50
|
+
def still_attempting?
|
51
|
+
return false if delivered?
|
52
|
+
attempt_count < max_attempts
|
53
|
+
end
|
54
|
+
|
55
|
+
def failed?
|
56
|
+
!(delivered? || still_attempting?)
|
57
|
+
end
|
58
|
+
|
59
|
+
def name
|
60
|
+
event.short_uuid
|
61
|
+
end
|
62
|
+
|
63
|
+
def max_attempts
|
64
|
+
ATTEMPT_SCHEDULE.keys.max
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Webhooks::Outgoing::EndpointSupport
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
include Webhooks::Outgoing::UriFiltering
|
4
|
+
|
5
|
+
included do
|
6
|
+
belongs_to BulletTrain::OutgoingWebhooks.parent_association
|
7
|
+
|
8
|
+
has_many :deliveries, class_name: "Webhooks::Outgoing::Delivery", dependent: :destroy, foreign_key: :endpoint_id
|
9
|
+
has_many :events, -> { distinct }, through: :deliveries
|
10
|
+
|
11
|
+
scope :listening_for_event_type_id, ->(event_type_id) { where("event_type_ids @> ? OR event_type_ids = '[]'::jsonb", "\"#{event_type_id}\"") }
|
12
|
+
|
13
|
+
validates :name, presence: true
|
14
|
+
|
15
|
+
before_validation { url&.strip! }
|
16
|
+
|
17
|
+
validates :url, presence: true, allowed_uri: true
|
18
|
+
|
19
|
+
after_initialize do
|
20
|
+
self.event_type_ids ||= []
|
21
|
+
end
|
22
|
+
|
23
|
+
after_save :touch_parent
|
24
|
+
end
|
25
|
+
|
26
|
+
def valid_event_types
|
27
|
+
Webhooks::Outgoing::EventType.all
|
28
|
+
end
|
29
|
+
|
30
|
+
def event_types
|
31
|
+
event_type_ids.map { |id| Webhooks::Outgoing::EventType.find(id) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def touch_parent
|
35
|
+
send(BulletTrain::OutgoingWebhooks.parent_association).touch
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Webhooks::Outgoing::EventSupport
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
include HasUuid
|
4
|
+
|
5
|
+
included do
|
6
|
+
belongs_to BulletTrain::OutgoingWebhooks.parent_association
|
7
|
+
belongs_to :event_type, class_name: "Webhooks::Outgoing::EventType"
|
8
|
+
belongs_to :subject, polymorphic: true
|
9
|
+
has_many :deliveries, dependent: :destroy
|
10
|
+
|
11
|
+
before_create do
|
12
|
+
self.payload = generate_payload
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def generate_payload
|
17
|
+
{
|
18
|
+
event_id: uuid,
|
19
|
+
event_type: event_type_id,
|
20
|
+
subject_id: subject_id,
|
21
|
+
subject_type: subject_type,
|
22
|
+
data: data
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def event_type_name
|
27
|
+
payload.dig("event_type")
|
28
|
+
end
|
29
|
+
|
30
|
+
def endpoints
|
31
|
+
send(BulletTrain::OutgoingWebhooks.parent_association).webhooks_outgoing_endpoints.listening_for_event_type_id(event_type_id)
|
32
|
+
end
|
33
|
+
|
34
|
+
def deliver
|
35
|
+
endpoints.each do |endpoint|
|
36
|
+
unless endpoint.deliveries.where(event: self).any?
|
37
|
+
endpoint.deliveries.create(event: self, endpoint_url: endpoint.url).deliver_async
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def label_string
|
43
|
+
short_uuid
|
44
|
+
end
|
45
|
+
end
|
@@ -9,41 +9,59 @@ module Webhooks::Outgoing::IssuingModel
|
|
9
9
|
has_many :webhooks_outgoing_events, as: :subject, class_name: "Webhooks::Outgoing::Event", dependent: :nullify
|
10
10
|
end
|
11
11
|
|
12
|
-
|
13
|
-
|
12
|
+
def skip_generate_webhook?(action)
|
13
|
+
false
|
14
14
|
end
|
15
15
|
|
16
|
-
def
|
17
|
-
|
18
|
-
|
16
|
+
def parent
|
17
|
+
return unless respond_to? BulletTrain::OutgoingWebhooks.parent_association
|
18
|
+
send(BulletTrain::OutgoingWebhooks.parent_association)
|
19
|
+
end
|
20
|
+
|
21
|
+
def generate_webhook(action, async: true)
|
22
|
+
# allow individual models to opt out of generating webhooks
|
23
|
+
return if skip_generate_webhook?(action)
|
24
|
+
|
25
|
+
# we can only generate webhooks for objects that return their their team / parent.
|
26
|
+
return unless parent.present?
|
19
27
|
|
20
28
|
# Try to find an event type definition for this action.
|
21
29
|
event_type = Webhooks::Outgoing::EventType.find_by(id: "#{self.class.name.underscore}.#{action}")
|
22
30
|
|
23
31
|
# If the event type is defined as one that people can be subscribed to,
|
24
|
-
# and this object has a
|
25
|
-
if event_type
|
26
|
-
|
32
|
+
# and this object has a parent where an associated outgoing webhooks endpoint could be registered.
|
33
|
+
if event_type
|
27
34
|
# Only generate an event record if an endpoint is actually listening for this event type.
|
28
|
-
if
|
29
|
-
|
30
|
-
|
31
|
-
|
35
|
+
if parent.endpoints_listening_for_event_type?(event_type)
|
36
|
+
if async
|
37
|
+
# serialization can be heavy so run it as a job
|
38
|
+
Webhooks::Outgoing::GenerateJob.perform_later(self, action)
|
39
|
+
else
|
40
|
+
generate_webhook_perform(action)
|
41
|
+
end
|
32
42
|
end
|
33
43
|
end
|
34
44
|
end
|
35
45
|
|
46
|
+
def generate_webhook_perform(action)
|
47
|
+
event_type = Webhooks::Outgoing::EventType.find_by(id: "#{self.class.name.underscore}.#{action}")
|
48
|
+
data = "Api::V1::#{self.class.name}Serializer".constantize.new(self).serializable_hash[:data]
|
49
|
+
webhook = send(BulletTrain::OutgoingWebhooks.parent_association).webhooks_outgoing_events.create(event_type_id: event_type.id, subject: self, data: data)
|
50
|
+
webhook.deliver
|
51
|
+
end
|
52
|
+
|
36
53
|
def generate_created_webhook
|
37
|
-
generate_webhook(
|
54
|
+
generate_webhook(:created)
|
38
55
|
end
|
39
56
|
|
40
57
|
def generate_updated_webhook
|
41
|
-
generate_webhook(
|
58
|
+
generate_webhook(:updated)
|
42
59
|
end
|
43
60
|
|
44
61
|
def generate_deleted_webhook
|
45
|
-
return false unless
|
46
|
-
return false if
|
47
|
-
|
62
|
+
return false unless parent.present?
|
63
|
+
return false if parent.being_destroyed?
|
64
|
+
|
65
|
+
generate_webhook(:deleted, async: false)
|
48
66
|
end
|
49
67
|
end
|
@@ -8,6 +8,22 @@ module Webhooks::Outgoing::TeamSupport
|
|
8
8
|
before_destroy :mark_for_destruction, prepend: true
|
9
9
|
end
|
10
10
|
|
11
|
+
def should_cache_endpoints_listening_for_event_type?
|
12
|
+
true
|
13
|
+
end
|
14
|
+
|
15
|
+
def endpoints_listening_for_event_type?(event_type)
|
16
|
+
if should_cache_endpoints_listening_for_event_type?
|
17
|
+
key = "#{cache_key_with_version}/endpoints_for_event_type/#{event_type.cache_key}"
|
18
|
+
|
19
|
+
Rails.cache.fetch(key, expires_in: 24.hours, race_condition_ttl: 5.seconds) do
|
20
|
+
webhooks_outgoing_endpoints.listening_for_event_type_id(event_type.id).any?
|
21
|
+
end
|
22
|
+
else
|
23
|
+
webhooks_outgoing_endpoints.listening_for_event_type_id(event_type.id).any?
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
11
27
|
def mark_for_destruction
|
12
28
|
# This allows downstream logic to check whether a team is being destroyed in order to bypass webhook issuance.
|
13
29
|
update_column(:being_destroyed, true)
|
@@ -0,0 +1,149 @@
|
|
1
|
+
require "resolv"
|
2
|
+
require "public_suffix"
|
3
|
+
|
4
|
+
module Webhooks::Outgoing::UriFiltering
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
# WEBHOOK SECURITY PRIMER
|
8
|
+
# =============================================================================
|
9
|
+
# Outgoing webhooks can be dangerous. By allowing your users to set
|
10
|
+
# up outgoing webhooks, you"re giving them permission to call arbitrary
|
11
|
+
# URLs from your server, including URLs that could represent resources
|
12
|
+
# internal to your company. Malicious actors can use this permission to
|
13
|
+
# examine your infrastructure, call internal APIs, and generally cause
|
14
|
+
# havok.
|
15
|
+
|
16
|
+
# This module attempts to block malicious actors with the following algorithm
|
17
|
+
# 1. Block anything but http and https requests
|
18
|
+
# 2. Block or allow defined hostnames, both regex and strings
|
19
|
+
# 3. Block if `custom_block_callback` returns true (args: self, uri)
|
20
|
+
# 4. Allow if `custom_allow_callback` returns true (args: self, uri)
|
21
|
+
# 5. Resolve the IP associated with the webhook"s host directly from
|
22
|
+
# the authoritative name server for the host"s domain. This IP
|
23
|
+
# is cached for the returned DNS TTL
|
24
|
+
# 6. Match the given IP against lists of allowed and blocked cidr ranges.
|
25
|
+
# The blocked list by default includes all of the defined private address
|
26
|
+
# ranges, localhost, the private IPv6 prefix, and the AWS metadata
|
27
|
+
# API endpoint.
|
28
|
+
|
29
|
+
# If at any point a URI is determined to be blocked we call `audit_callback`
|
30
|
+
# (args: self, uri) so it can be logged for auditing.
|
31
|
+
|
32
|
+
# We resolve the IP from the authoritative name server directly so we can avoid
|
33
|
+
# certain classes of DNS poisoning attacks.
|
34
|
+
|
35
|
+
# Users of this gem are _strongly_ enouraged to add additional cidr ranges
|
36
|
+
# and hostnames to the appropriate lists and/or implement `custom_block_callback`.
|
37
|
+
# At the very least you should add the public hostname that your
|
38
|
+
# application uses to the blocked_hostnames list.
|
39
|
+
|
40
|
+
class AllowedUriValidator < ActiveModel::EachValidator
|
41
|
+
def validate_each(record, attribute, value)
|
42
|
+
uri = URI.parse(value)
|
43
|
+
unless record.allowed_uri?(uri)
|
44
|
+
record.errors.add attribute, "is not an allowed uri"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def resolve_ip_from_authoritative(hostname)
|
50
|
+
begin
|
51
|
+
ip = IPAddr.new(hostname)
|
52
|
+
return ip.to_s
|
53
|
+
rescue IPAddr::InvalidAddressError
|
54
|
+
# this is fine, proceed with resolver path
|
55
|
+
end
|
56
|
+
|
57
|
+
cache_key = "#{cache_key_with_version}/uri_ip/#{Digest::SHA2.hexdigest(hostname)}"
|
58
|
+
|
59
|
+
cached = Rails.cache.read(cache_key)
|
60
|
+
if cached
|
61
|
+
return cached == "invalid" ? nil : cached
|
62
|
+
end
|
63
|
+
|
64
|
+
begin
|
65
|
+
# This is sort of a half-recursive DNS resolver.
|
66
|
+
# We can't implement a full recursive resolver using just Resolv::DNS so instead
|
67
|
+
# this asks a public cache for the NS record for the given domain. Then it asks
|
68
|
+
# the authoritative nameserver directly for the address and caches it according
|
69
|
+
# to the returned TTL.
|
70
|
+
|
71
|
+
config = Rails.configuration.outgoing_webhooks
|
72
|
+
ns_resolver = Resolv::DNS.new(nameserver: config[:public_resolvers])
|
73
|
+
ns_resolver.timeouts = 1
|
74
|
+
|
75
|
+
domain = PublicSuffix.domain(hostname)
|
76
|
+
authoritative = ns_resolver.getresource(domain, Resolv::DNS::Resource::IN::NS)
|
77
|
+
|
78
|
+
authoritative_resolver = Resolv::DNS.new(nameserver: [authoritative.name.to_s])
|
79
|
+
authoritative_resolver.timeouts = 1
|
80
|
+
|
81
|
+
resource = authoritative_resolver.getresource(hostname, Resolv::DNS::Resource::IN::A)
|
82
|
+
Rails.cache.write(cache_key, resource.address.to_s, expires_in: resource.ttl, race_condition_ttl: 5)
|
83
|
+
resource.address.to_s
|
84
|
+
rescue IPAddr::InvalidAddressError, ArgumentError # standard:disable Lint/ShadowedException
|
85
|
+
Rails.cache.write(cache_key, "invalid", expires_in: 10.minutes, race_condition_ttl: 5)
|
86
|
+
nil
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def allowed_uri?(uri)
|
91
|
+
unless _allowed_uri?(uri)
|
92
|
+
config = Rails.configuration.outgoing_webhooks
|
93
|
+
if config[:audit_callback].present?
|
94
|
+
config[:audit_callback].call(self, uri)
|
95
|
+
end
|
96
|
+
return false
|
97
|
+
end
|
98
|
+
|
99
|
+
true
|
100
|
+
end
|
101
|
+
|
102
|
+
def _allowed_uri?(uri)
|
103
|
+
config = Rails.configuration.outgoing_webhooks
|
104
|
+
hostname = uri.hostname.downcase
|
105
|
+
|
106
|
+
return false unless config[:allowed_schemes].include?(uri.scheme)
|
107
|
+
|
108
|
+
config[:blocked_hostnames].each do |blocked|
|
109
|
+
if blocked.is_a?(Regexp)
|
110
|
+
return false if blocked.match?(hostname)
|
111
|
+
end
|
112
|
+
|
113
|
+
return false if blocked == hostname
|
114
|
+
end
|
115
|
+
|
116
|
+
config[:allowed_hostnames].each do |allowed|
|
117
|
+
if allowed.is_a?(Regexp)
|
118
|
+
return true if allowed.match?(hostname)
|
119
|
+
end
|
120
|
+
|
121
|
+
return true if allowed == hostname
|
122
|
+
end
|
123
|
+
|
124
|
+
if config[:custom_allow_callback].present?
|
125
|
+
return true if config[:custom_allow_callback].call(self, uri)
|
126
|
+
end
|
127
|
+
|
128
|
+
if config[:custom_block_callback].present?
|
129
|
+
return false if config[:custom_block_callback].call(self, uri)
|
130
|
+
end
|
131
|
+
|
132
|
+
resolved_ip = resolve_ip_from_authoritative(hostname)
|
133
|
+
return false if resolved_ip.nil?
|
134
|
+
|
135
|
+
begin
|
136
|
+
config[:allowed_cidrs].each do |cidr|
|
137
|
+
return true if IPAddr.new(cidr).include?(resolved_ip)
|
138
|
+
end
|
139
|
+
|
140
|
+
config[:blocked_cidrs].each do |cidr|
|
141
|
+
return false if IPAddr.new(cidr).include?(resolved_ip)
|
142
|
+
end
|
143
|
+
rescue IPAddr::InvalidAddressError
|
144
|
+
return false
|
145
|
+
end
|
146
|
+
|
147
|
+
true
|
148
|
+
end
|
149
|
+
end
|
@@ -1,21 +1,9 @@
|
|
1
|
-
class Webhooks::Outgoing::Delivery <
|
1
|
+
class Webhooks::Outgoing::Delivery < BulletTrain::OutgoingWebhooks.base_class.constantize
|
2
|
+
include Webhooks::Outgoing::DeliverySupport
|
2
3
|
# 🚅 add concerns above.
|
3
4
|
|
4
|
-
belongs_to :endpoint, class_name: "Webhooks::Outgoing::Endpoint"
|
5
|
-
belongs_to :event, class_name: "Webhooks::Outgoing::Event"
|
6
|
-
has_one :team, through: :endpoint
|
7
|
-
|
8
|
-
ATTEMPT_SCHEDULE = {
|
9
|
-
1 => 15.seconds,
|
10
|
-
2 => 1.minute,
|
11
|
-
3 => 5.minutes,
|
12
|
-
4 => 15.minutes,
|
13
|
-
5 => 1.hour,
|
14
|
-
}
|
15
|
-
|
16
5
|
# 🚅 add belongs_to associations above.
|
17
6
|
|
18
|
-
has_many :delivery_attempts, class_name: "Webhooks::Outgoing::DeliveryAttempt", dependent: :destroy, foreign_key: :delivery_id
|
19
7
|
# 🚅 add has_many associations above.
|
20
8
|
|
21
9
|
# 🚅 add has_one associations above.
|
@@ -28,52 +16,5 @@ class Webhooks::Outgoing::Delivery < ApplicationRecord
|
|
28
16
|
|
29
17
|
# 🚅 add delegations above.
|
30
18
|
|
31
|
-
def label_string
|
32
|
-
event.short_uuid
|
33
|
-
end
|
34
|
-
|
35
|
-
def next_reattempt_delay
|
36
|
-
ATTEMPT_SCHEDULE[attempt_count]
|
37
|
-
end
|
38
|
-
|
39
|
-
def deliver_async
|
40
|
-
if still_attempting?
|
41
|
-
Webhooks::Outgoing::DeliveryJob.set(wait: next_reattempt_delay).perform_later(self)
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
def deliver
|
46
|
-
if delivery_attempts.create.attempt
|
47
|
-
touch(:delivered_at)
|
48
|
-
else
|
49
|
-
deliver_async
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
def attempt_count
|
54
|
-
delivery_attempts.count
|
55
|
-
end
|
56
|
-
|
57
|
-
def delivered?
|
58
|
-
delivered_at.present?
|
59
|
-
end
|
60
|
-
|
61
|
-
def still_attempting?
|
62
|
-
return false if delivered?
|
63
|
-
attempt_count < max_attempts
|
64
|
-
end
|
65
|
-
|
66
|
-
def failed?
|
67
|
-
!(delivered? || still_attempting?)
|
68
|
-
end
|
69
|
-
|
70
|
-
def name
|
71
|
-
event.short_uuid
|
72
|
-
end
|
73
|
-
|
74
|
-
def max_attempts
|
75
|
-
ATTEMPT_SCHEDULE.keys.max
|
76
|
-
end
|
77
|
-
|
78
19
|
# 🚅 add methods above.
|
79
20
|
end
|
@@ -1,51 +1,7 @@
|
|
1
|
-
class Webhooks::Outgoing::DeliveryAttempt <
|
1
|
+
class Webhooks::Outgoing::DeliveryAttempt < BulletTrain::OutgoingWebhooks.base_class.constantize
|
2
|
+
include Webhooks::Outgoing::DeliveryAttemptSupport
|
2
3
|
# 🚅 add concerns above.
|
3
4
|
|
4
|
-
belongs_to :delivery
|
5
|
-
has_one :team, through: :delivery
|
6
|
-
scope :successful, -> { where(response_code: 200) }
|
7
|
-
|
8
|
-
before_create do
|
9
|
-
self.attempt_number = delivery.attempt_count + 1
|
10
|
-
end
|
11
|
-
|
12
|
-
def still_attempting?
|
13
|
-
error_message.nil? && response_code.nil?
|
14
|
-
end
|
15
|
-
|
16
|
-
def successful?
|
17
|
-
[200, 201, 202, 203, 204, 205, 206, 207, 226].include?(response_code)
|
18
|
-
end
|
19
|
-
|
20
|
-
def failed?
|
21
|
-
!(successful? || still_attempting?)
|
22
|
-
end
|
23
|
-
|
24
|
-
def attempt
|
25
|
-
uri = URI.parse(delivery.endpoint_url)
|
26
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
27
|
-
http.use_ssl = true if uri.scheme == "https"
|
28
|
-
request = Net::HTTP::Post.new(uri.path)
|
29
|
-
request.add_field("Content-Type", "application/json")
|
30
|
-
request.body = delivery.event.payload.to_json
|
31
|
-
|
32
|
-
begin
|
33
|
-
response = http.request(request)
|
34
|
-
self.response_message = response.message
|
35
|
-
self.response_code = response.code
|
36
|
-
self.response_body = response.body
|
37
|
-
rescue Exception => exception
|
38
|
-
self.response_code = 0
|
39
|
-
self.error_message = exception.message
|
40
|
-
end
|
41
|
-
|
42
|
-
save
|
43
|
-
successful?
|
44
|
-
end
|
45
|
-
|
46
|
-
def label_string
|
47
|
-
"#{attempt_number.ordinalize} Attempt"
|
48
|
-
end
|
49
5
|
# 🚅 add belongs_to associations above.
|
50
6
|
|
51
7
|
# 🚅 add has_many associations above.
|
@@ -54,7 +10,6 @@ class Webhooks::Outgoing::DeliveryAttempt < ApplicationRecord
|
|
54
10
|
|
55
11
|
# 🚅 add scopes above.
|
56
12
|
|
57
|
-
validates :response_code, presence: true
|
58
13
|
# 🚅 add validations above.
|
59
14
|
|
60
15
|
# 🚅 add callbacks above.
|
@@ -1,32 +1,20 @@
|
|
1
|
-
class Webhooks::Outgoing::Endpoint <
|
1
|
+
class Webhooks::Outgoing::Endpoint < BulletTrain::OutgoingWebhooks.base_class.constantize
|
2
|
+
include Webhooks::Outgoing::EndpointSupport
|
2
3
|
# 🚅 add concerns above.
|
3
4
|
|
4
|
-
belongs_to :team
|
5
5
|
# 🚅 add belongs_to associations above.
|
6
6
|
|
7
|
-
has_many :deliveries, class_name: "Webhooks::Outgoing::Delivery", dependent: :destroy, foreign_key: :endpoint_id
|
8
|
-
has_many :events, -> { distinct }, through: :deliveries
|
9
7
|
# 🚅 add has_many associations above.
|
10
8
|
|
11
9
|
# 🚅 add has_one associations above.
|
12
10
|
|
13
|
-
scope :listening_for_event_type_id, ->(event_type_id) { where("event_type_ids @> ? OR event_type_ids = '[]'::jsonb", "\"#{event_type_id}\"") }
|
14
11
|
# 🚅 add scopes above.
|
15
12
|
|
16
|
-
validates :name, presence: true
|
17
13
|
# 🚅 add validations above.
|
18
14
|
|
19
15
|
# 🚅 add callbacks above.
|
20
16
|
|
21
17
|
# 🚅 add delegations above.
|
22
18
|
|
23
|
-
def valid_event_types
|
24
|
-
Webhooks::Outgoing::EventType.all
|
25
|
-
end
|
26
|
-
|
27
|
-
def event_types
|
28
|
-
event_type_ids.map { |id| Webhooks::Outgoing::EventType.find(id) }
|
29
|
-
end
|
30
|
-
|
31
19
|
# 🚅 add methods above.
|
32
20
|
end
|
@@ -1,41 +1,3 @@
|
|
1
|
-
class Webhooks::Outgoing::Event <
|
2
|
-
include
|
3
|
-
belongs_to :team
|
4
|
-
belongs_to :event_type, class_name: "Webhooks::Outgoing::EventType"
|
5
|
-
belongs_to :subject, polymorphic: true
|
6
|
-
has_many :deliveries, dependent: :destroy
|
7
|
-
|
8
|
-
before_create do
|
9
|
-
self.payload = generate_payload
|
10
|
-
end
|
11
|
-
|
12
|
-
def generate_payload
|
13
|
-
{
|
14
|
-
event_id: uuid,
|
15
|
-
event_type: event_type_id,
|
16
|
-
subject_id: subject_id,
|
17
|
-
subject_type: subject_type,
|
18
|
-
data: data
|
19
|
-
}
|
20
|
-
end
|
21
|
-
|
22
|
-
def event_type_name
|
23
|
-
payload.dig("event_type")
|
24
|
-
end
|
25
|
-
|
26
|
-
def endpoints
|
27
|
-
team.webhooks_outgoing_endpoints.listening_for_event_type_id(event_type_id)
|
28
|
-
end
|
29
|
-
|
30
|
-
def deliver
|
31
|
-
endpoints.each do |endpoint|
|
32
|
-
unless endpoint.deliveries.where(event: self).any?
|
33
|
-
endpoint.deliveries.create(event: self, endpoint_url: endpoint.url).deliver_async
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
def label_string
|
39
|
-
short_uuid
|
40
|
-
end
|
1
|
+
class Webhooks::Outgoing::Event < BulletTrain::OutgoingWebhooks.base_class.constantize
|
2
|
+
include Webhooks::Outgoing::EventSupport
|
41
3
|
end
|
@@ -1,6 +1,6 @@
|
|
1
|
-
<% if can? :read, Webhooks::Outgoing::Delivery.new(
|
1
|
+
<% if can? :read, Webhooks::Outgoing::Delivery.new(BulletTrain::OutgoingWebhooks.parent_association => send(BulletTrain::OutgoingWebhooks.current_parent_method)) %>
|
2
2
|
<%= render 'account/shared/menu/item', {
|
3
|
-
url: main_app.polymorphic_path([:account,
|
3
|
+
url: main_app.polymorphic_path([:account, send(BulletTrain::OutgoingWebhooks.current_parent_method), :webhooks_outgoing_deliveries]),
|
4
4
|
label: t('webhooks/outgoing/deliveries.navigation.label'),
|
5
5
|
} do |p| %>
|
6
6
|
<% p.content_for :icon do %>
|
@@ -1,6 +1,6 @@
|
|
1
|
-
<% if can? :read, Webhooks::Outgoing::DeliveryAttempt.new(
|
1
|
+
<% if can? :read, Webhooks::Outgoing::DeliveryAttempt.new(BulletTrain::OutgoingWebhooks.parent_association => send(BulletTrain::OutgoingWebhooks.current_parent_method)) %>
|
2
2
|
<%= render 'account/shared/menu/item', {
|
3
|
-
url: main_app.polymorphic_path([:account,
|
3
|
+
url: main_app.polymorphic_path([:account, send(BulletTrain::OutgoingWebhooks.current_parent_method), :webhooks_outgoing_delivery_attempts]),
|
4
4
|
label: t('webhooks/outgoing/delivery_attempts.navigation.label'),
|
5
5
|
} do |p| %>
|
6
6
|
<% p.content_for :icon do %>
|
@@ -1,5 +1,5 @@
|
|
1
1
|
<% endpoint ||= @endpoint %>
|
2
|
-
<% team ||= @
|
2
|
+
<% team ||= @parent || endpoint&.send(BulletTrain::OutgoingWebhooks.parent_association) %>
|
3
3
|
<%= render 'account/teams/breadcrumbs', team: team %>
|
4
4
|
<%= render 'account/shared/breadcrumb', label: t('.label'), url: [:account, team, :webhooks_outgoing_endpoints] %>
|
5
5
|
<% if endpoint&.persisted? %>
|
@@ -1,4 +1,4 @@
|
|
1
|
-
<%= form_with model: endpoint, url: (endpoint.persisted? ? [:account, endpoint] : [:account, @
|
1
|
+
<%= form_with model: endpoint, url: (endpoint.persisted? ? [:account, endpoint] : [:account, @parent, :webhooks_outgoing_endpoints]), local: true, class: 'form' do |form| %>
|
2
2
|
<%= render 'account/shared/forms/errors', form: form %>
|
3
3
|
|
4
4
|
<% with_field_settings form: form do %>
|
@@ -14,7 +14,7 @@
|
|
14
14
|
<% if form.object.persisted? %>
|
15
15
|
<%= link_to t('global.buttons.cancel'), [:account, endpoint], class: "button-secondary" %>
|
16
16
|
<% else %>
|
17
|
-
<%= link_to t('global.buttons.cancel'), [:account, @
|
17
|
+
<%= link_to t('global.buttons.cancel'), [:account, @parent, :webhooks_outgoing_endpoints], class: "button-secondary" %>
|
18
18
|
<% end %>
|
19
19
|
</div>
|
20
20
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
<% team = @
|
1
|
+
<% team = @parent %>
|
2
2
|
<% context ||= team %>
|
3
3
|
<% collection ||= :webhooks_outgoing_endpoints %>
|
4
4
|
<% hide_actions ||= false %>
|
@@ -49,8 +49,8 @@
|
|
49
49
|
<% p.content_for :actions do %>
|
50
50
|
<% unless hide_actions %>
|
51
51
|
<% if context == team %>
|
52
|
-
<% if can? :create, Webhooks::Outgoing::Endpoint.new(
|
53
|
-
<%= link_to t('.buttons.new'), [:new, :account,
|
52
|
+
<% if can? :create, Webhooks::Outgoing::Endpoint.new(BulletTrain::OutgoingWebhooks.parent_association => @parent) %>
|
53
|
+
<%= link_to t('.buttons.new'), [:new, :account, @parent, :webhooks_outgoing_endpoint], class: "#{first_button_primary(:webhooks_outgoing_endpoint)} new" %>
|
54
54
|
<% end %>
|
55
55
|
<% end %>
|
56
56
|
|
@@ -1,6 +1,6 @@
|
|
1
|
-
<% if can? :read, Webhooks::Outgoing::Endpoint.new(
|
1
|
+
<% if can? :read, Webhooks::Outgoing::Endpoint.new(BulletTrain::OutgoingWebhooks.parent_association => send(BulletTrain::OutgoingWebhooks.current_parent_method)) %>
|
2
2
|
<%= render 'account/shared/menu/item', {
|
3
|
-
url: main_app.polymorphic_path([:account,
|
3
|
+
url: main_app.polymorphic_path([:account, send(BulletTrain::OutgoingWebhooks.current_parent_method), :webhooks_outgoing_endpoints]),
|
4
4
|
label: t('webhooks/outgoing/endpoints.navigation.label'),
|
5
5
|
} do |p| %>
|
6
6
|
<% p.content_for :icon do %>
|
@@ -32,7 +32,7 @@
|
|
32
32
|
<% p.content_for :actions do %>
|
33
33
|
<%= link_to t('.buttons.edit'), [:edit, :account, @endpoint], class: first_button_primary if can? :edit, @endpoint %>
|
34
34
|
<%= button_to t('.buttons.destroy'), [:account, @endpoint], method: :delete, class: first_button_primary, data: { confirm: t('.buttons.confirmations.destroy', model_locales(@endpoint)) } if can? :destroy, @endpoint %>
|
35
|
-
<%= link_to t('global.buttons.back'), [:account, @
|
35
|
+
<%= link_to t('global.buttons.back'), [:account, @parent, :webhooks_outgoing_endpoints], class: first_button_primary %>
|
36
36
|
<% end %>
|
37
37
|
<% end %>
|
38
38
|
|
data/config/routes.rb
CHANGED
@@ -1,8 +1,33 @@
|
|
1
1
|
module BulletTrain
|
2
2
|
module OutgoingWebhooks
|
3
3
|
class Engine < ::Rails::Engine
|
4
|
+
config.before_configuration do
|
5
|
+
default_blocked_cidrs = %w[
|
6
|
+
10.0.0.0/8
|
7
|
+
172.16.0.0/12
|
8
|
+
192.168.0.0/16
|
9
|
+
100.64.0.0/10
|
10
|
+
127.0.0.0/8
|
11
|
+
169.254.169.254/32
|
12
|
+
fc00::/7
|
13
|
+
::1
|
14
|
+
]
|
15
|
+
|
16
|
+
config.outgoing_webhooks = {
|
17
|
+
blocked_cidrs: default_blocked_cidrs,
|
18
|
+
allowed_cidrs: [],
|
19
|
+
blocked_hostnames: %w[localhost],
|
20
|
+
allowed_hostnames: [],
|
21
|
+
public_resolvers: %w[8.8.8.8 1.1.1.1],
|
22
|
+
allowed_schemes: %w[http https],
|
23
|
+
custom_block_callback: nil,
|
24
|
+
custom_allow_callback: nil,
|
25
|
+
audit_callback: ->(obj, uri) { Rails.logger.error("BlockedURI obj=#{obj.persisted? ? obj.to_global_id : "New #{obj.class}"} uri=#{uri}") }
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
4
29
|
initializer "bullet_train.outgoing_webhooks.register_api_endpoints" do |app|
|
5
|
-
if BulletTrain::Api
|
30
|
+
if Object.const_defined?("BulletTrain::Api")
|
6
31
|
BulletTrain::Api.endpoints << "Api::V1::Webhooks::Outgoing::EndpointsEndpoint"
|
7
32
|
BulletTrain::Api.endpoints << "Api::V1::Webhooks::Outgoing::DeliveriesEndpoint"
|
8
33
|
BulletTrain::Api.endpoints << "Api::V1::Webhooks::Outgoing::DeliveryAttemptsEndpoint"
|
@@ -3,6 +3,31 @@ require "bullet_train/outgoing_webhooks/engine"
|
|
3
3
|
|
4
4
|
module BulletTrain
|
5
5
|
module OutgoingWebhooks
|
6
|
-
|
6
|
+
def self.default_for(klass, method, default_value)
|
7
|
+
klass.respond_to?(method) ? klass.send(method) || default_value : default_value
|
8
|
+
end
|
9
|
+
|
10
|
+
mattr_accessor :parent_class, default: default_for(BulletTrain, :parent_class, "Team")
|
11
|
+
mattr_accessor :base_class, default: default_for(BulletTrain, :base_class, "ApplicationRecord")
|
12
|
+
|
13
|
+
def self.parent_association
|
14
|
+
parent_class.underscore.to_sym
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.parent_resource
|
18
|
+
parent_class.underscore.pluralize.to_sym
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.parent_class_specified?
|
22
|
+
parent_class != "Team"
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.current_parent_method
|
26
|
+
"current_#{parent_association}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.parent_association_id
|
30
|
+
"#{parent_association}_id".to_sym
|
31
|
+
end
|
7
32
|
end
|
8
33
|
end
|
metadata
CHANGED
@@ -1,29 +1,57 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bullet_train-outgoing_webhooks
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Culver
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-08-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: standard
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: rails
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
16
30
|
requirements:
|
17
31
|
- - ">="
|
18
32
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
33
|
+
version: 6.0.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 6.0.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: public_suffix
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
20
48
|
type: :runtime
|
21
49
|
prerelease: false
|
22
50
|
version_requirements: !ruby/object:Gem::Requirement
|
23
51
|
requirements:
|
24
52
|
- - ">="
|
25
53
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
54
|
+
version: '0'
|
27
55
|
description: Allow users of your Rails application to subscribe and receive webhooks
|
28
56
|
when activity takes place in your application.
|
29
57
|
email:
|
@@ -43,8 +71,14 @@ files:
|
|
43
71
|
- app/controllers/api/v1/webhooks/outgoing/delivery_attempts_endpoint.rb
|
44
72
|
- app/controllers/api/v1/webhooks/outgoing/endpoints_endpoint.rb
|
45
73
|
- app/jobs/webhooks/outgoing/delivery_job.rb
|
74
|
+
- app/jobs/webhooks/outgoing/generate_job.rb
|
75
|
+
- app/models/concerns/webhooks/outgoing/delivery_attempt_support.rb
|
76
|
+
- app/models/concerns/webhooks/outgoing/delivery_support.rb
|
77
|
+
- app/models/concerns/webhooks/outgoing/endpoint_support.rb
|
78
|
+
- app/models/concerns/webhooks/outgoing/event_support.rb
|
46
79
|
- app/models/concerns/webhooks/outgoing/issuing_model.rb
|
47
80
|
- app/models/concerns/webhooks/outgoing/team_support.rb
|
81
|
+
- app/models/concerns/webhooks/outgoing/uri_filtering.rb
|
48
82
|
- app/models/webhooks.rb
|
49
83
|
- app/models/webhooks/outgoing.rb
|
50
84
|
- app/models/webhooks/outgoing/delivery.rb
|
@@ -141,7 +175,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
141
175
|
- !ruby/object:Gem::Version
|
142
176
|
version: '0'
|
143
177
|
requirements: []
|
144
|
-
rubygems_version: 3.
|
178
|
+
rubygems_version: 3.3.7
|
145
179
|
signing_key:
|
146
180
|
specification_version: 4
|
147
181
|
summary: Allow users of your Rails application to subscribe and receive webhooks when
|