angarium 0.1.0
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 +7 -0
- data/CHANGELOG.md +65 -0
- data/MIT-LICENSE +22 -0
- data/README.md +910 -0
- data/Rakefile +13 -0
- data/SECURITY.md +51 -0
- data/app/assets/config/angarium_manifest.js +1 -0
- data/app/assets/stylesheets/angarium/application.css +15 -0
- data/app/controllers/angarium/api/attempts_controller.rb +12 -0
- data/app/controllers/angarium/api/base_controller.rb +133 -0
- data/app/controllers/angarium/api/deliveries_controller.rb +29 -0
- data/app/controllers/angarium/api/endpoints_controller.rb +117 -0
- data/app/controllers/angarium/api/not_authorized.rb +7 -0
- data/app/controllers/angarium/api/unpermitted_parameter.rb +15 -0
- data/app/helpers/angarium/application_helper.rb +4 -0
- data/app/jobs/angarium/application_job.rb +5 -0
- data/app/jobs/angarium/deliver_job.rb +11 -0
- data/app/models/angarium/application_record.rb +17 -0
- data/app/models/angarium/delivery.rb +254 -0
- data/app/models/angarium/delivery_attempt.rb +14 -0
- data/app/models/angarium/endpoint.rb +217 -0
- data/app/models/angarium/event.rb +7 -0
- data/app/policies/angarium/api/policy.rb +78 -0
- data/app/validators/angarium/endpoint_url_validator.rb +20 -0
- data/app/views/layouts/angarium/application.html.erb +15 -0
- data/config/routes.rb +22 -0
- data/db/angarium_migrate/20260704000001_create_angarium_endpoints.rb +22 -0
- data/db/angarium_migrate/20260704000002_create_angarium_events.rb +9 -0
- data/db/angarium_migrate/20260704000003_create_angarium_deliveries.rb +13 -0
- data/db/angarium_migrate/20260704000004_create_angarium_delivery_attempts.rb +12 -0
- data/lib/angarium/address_policy.rb +82 -0
- data/lib/angarium/client.rb +53 -0
- data/lib/angarium/configuration.rb +66 -0
- data/lib/angarium/dispatch.rb +23 -0
- data/lib/angarium/engine.rb +5 -0
- data/lib/angarium/event_matcher.rb +17 -0
- data/lib/angarium/signature.rb +57 -0
- data/lib/angarium/version.rb +3 -0
- data/lib/angarium.rb +44 -0
- data/lib/generators/angarium/install/install_generator.rb +57 -0
- data/lib/generators/angarium/install/templates/initializer.rb +77 -0
- data/lib/generators/angarium/migrations/migrations_generator.rb +39 -0
- data/lib/generators/angarium/policy/policy_generator.rb +40 -0
- data/lib/generators/angarium/policy/templates/policy.rb +33 -0
- data/lib/tasks/angarium_tasks.rake +15 -0
- metadata +137 -0
data/Rakefile
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require "bundler/setup"
|
|
2
|
+
|
|
3
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
|
4
|
+
load "rails/tasks/engine.rake"
|
|
5
|
+
|
|
6
|
+
load "rails/tasks/statistics.rake"
|
|
7
|
+
|
|
8
|
+
require "bundler/gem_tasks"
|
|
9
|
+
|
|
10
|
+
# `rake standard` / `rake standard:fix` when the linter is in the bundle (dev).
|
|
11
|
+
# `bin/rails test` loads this Rakefile too, and the test-matrix bundles omit
|
|
12
|
+
# standard, so only require it when present. Release tasks live in rakelib/.
|
|
13
|
+
require "standard/rake" if Gem.loaded_specs.key?("standard")
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
Angarium is pre-release. Security fixes are provided for the `0.1.x` series
|
|
6
|
+
during this phase. Once a stable line is released, this policy will be updated to
|
|
7
|
+
list the versions that receive security updates.
|
|
8
|
+
|
|
9
|
+
| Version | Supported |
|
|
10
|
+
| ------- | --------- |
|
|
11
|
+
| 0.1.x | ✅ |
|
|
12
|
+
|
|
13
|
+
## Reporting a vulnerability
|
|
14
|
+
|
|
15
|
+
Please report security vulnerabilities **privately**. Do **not** open a public
|
|
16
|
+
issue, pull request, or discussion for a suspected vulnerability.
|
|
17
|
+
|
|
18
|
+
Use GitHub's private vulnerability reporting for the
|
|
19
|
+
[`radioactive-labs/angarium`](https://github.com/radioactive-labs/angarium)
|
|
20
|
+
repository: go to the **Security** tab and choose **Report a vulnerability**
|
|
21
|
+
(this opens a private draft advisory visible only to the maintainers).
|
|
22
|
+
|
|
23
|
+
We aim to acknowledge new reports within a few business days, and we'll keep you
|
|
24
|
+
informed as we investigate and prepare a fix. Please give us a reasonable window
|
|
25
|
+
to release a patch before any public disclosure.
|
|
26
|
+
|
|
27
|
+
## Security surfaces Angarium hardens
|
|
28
|
+
|
|
29
|
+
Angarium is designed to send user-configured webhooks safely. The intended
|
|
30
|
+
security guarantees, and the surfaces you can hold us to, are:
|
|
31
|
+
|
|
32
|
+
- **SSRF protection.** Endpoint URLs are user-supplied, so delivery is guarded by
|
|
33
|
+
an address policy that blocks private/loopback/link-local addresses by default,
|
|
34
|
+
requires an explicit per-endpoint opt-in for private networks, and **fails
|
|
35
|
+
closed** on hosts it can't resolve.
|
|
36
|
+
- **Connect-time IP pinning.** Deliveries pin the connection to the validated
|
|
37
|
+
resolved address, closing the DNS-rebinding window between resolution and
|
|
38
|
+
connect (TLS SNI and certificate verification still use the original hostname).
|
|
39
|
+
- **Encrypted secrets at rest.** Endpoint signing secrets (current and
|
|
40
|
+
in-rotation previous) and `custom_headers` (which commonly carries a receiver
|
|
41
|
+
credential such as an Authorization bearer token) are encrypted at rest with
|
|
42
|
+
Active Record Encryption.
|
|
43
|
+
- **Standard Webhooks HMAC signing.** Requests are signed per the
|
|
44
|
+
[Standard Webhooks](https://www.standardwebhooks.com) spec (HMAC-SHA256 over
|
|
45
|
+
`{id}.{timestamp}.{body}`) with a timestamp tolerance enforced on verification
|
|
46
|
+
to resist replay.
|
|
47
|
+
- **Custom-header denylist.** Per-endpoint custom headers cannot override the
|
|
48
|
+
signature headers or dangerous transport headers (request-smuggling and
|
|
49
|
+
receiver-confusion surface).
|
|
50
|
+
|
|
51
|
+
If you find a gap in any of these guarantees, we especially want to hear about it.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//= link_directory ../stylesheets/angarium .css
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
|
3
|
+
* listed below.
|
|
4
|
+
*
|
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
|
7
|
+
*
|
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
|
11
|
+
* It is generally better to create a new file per style scope.
|
|
12
|
+
*
|
|
13
|
+
*= require_tree .
|
|
14
|
+
*= require_self
|
|
15
|
+
*/
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Angarium
|
|
2
|
+
module Api
|
|
3
|
+
class AttemptsController < BaseController
|
|
4
|
+
# GET /deliveries/:delivery_id/attempts
|
|
5
|
+
def index
|
|
6
|
+
delivery = scoped_delivery(params[:delivery_id])
|
|
7
|
+
authorize!(delivery)
|
|
8
|
+
render_collection(:attempts, delivery.delivery_attempts.order(created_at: :desc)) { |a| attempt_json(a) }
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
module Angarium
|
|
2
|
+
module Api
|
|
3
|
+
# Base controller for the headless JSON API. Inherits from your app's
|
|
4
|
+
# controller (config.parent_controller, default "ApplicationController"), so
|
|
5
|
+
# your existing authentication applies here too.
|
|
6
|
+
class BaseController < Angarium.config.parent_controller.constantize
|
|
7
|
+
# These endpoints authenticate via your current-user convention (a session
|
|
8
|
+
# or token), not a form CSRF token. If we inherited a forgery-protecting
|
|
9
|
+
# base (ActionController::Base), don't reject API POSTs for a missing token.
|
|
10
|
+
protect_from_forgery with: :null_session if respond_to?(:protect_from_forgery)
|
|
11
|
+
|
|
12
|
+
before_action :authenticate_angarium_user!
|
|
13
|
+
|
|
14
|
+
rescue_from ActiveRecord::RecordNotFound, with: :angarium_render_not_found
|
|
15
|
+
rescue_from ActiveRecord::RecordInvalid, with: :angarium_render_invalid
|
|
16
|
+
rescue_from Angarium::Api::NotAuthorized, with: :angarium_render_forbidden
|
|
17
|
+
rescue_from Angarium::Api::UnpermittedParameter, with: :angarium_render_unpermitted
|
|
18
|
+
|
|
19
|
+
# Resolved current user (via config.current_user). Public so policies can
|
|
20
|
+
# read it as `controller.angarium_current_user`.
|
|
21
|
+
def angarium_current_user
|
|
22
|
+
return @angarium_current_user if defined?(@angarium_current_user)
|
|
23
|
+
|
|
24
|
+
@angarium_current_user = Angarium.config.current_user.call(self)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def authenticate_angarium_user!
|
|
30
|
+
angarium_render_unauthorized unless angarium_current_user
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The policy for this request (config.policy_class), holding scope,
|
|
34
|
+
# create-owner, and per-action permissions.
|
|
35
|
+
def angarium_policy(record = nil)
|
|
36
|
+
Angarium.config.policy_class.to_s.constantize.new(self, record)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The endpoints this user may see/act on: the policy narrows the base relation.
|
|
40
|
+
def endpoint_scope
|
|
41
|
+
angarium_policy.scope(Angarium::Endpoint.all)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# A delivery whose endpoint is within the caller's scope, or 404.
|
|
45
|
+
def scoped_delivery(id)
|
|
46
|
+
Angarium::Delivery.where(endpoint_id: endpoint_scope.select(:id)).find(id)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Guard the current action with the policy's `<action>?` predicate.
|
|
50
|
+
def authorize!(record = nil)
|
|
51
|
+
raise Angarium::Api::NotAuthorized unless angarium_policy(record).public_send("#{action_name}?")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Render a paginated list. Applies ?limit (default 50, max 200) and ?offset,
|
|
55
|
+
# and advertises the window with a `pagination` object so clients can page
|
|
56
|
+
# (there's more when offset + count < total).
|
|
57
|
+
def render_collection(key, relation, &serializer)
|
|
58
|
+
limit = params.fetch(:limit, 50).to_i.clamp(1, 200)
|
|
59
|
+
offset = [params.fetch(:offset, 0).to_i, 0].max
|
|
60
|
+
records = relation.limit(limit).offset(offset).to_a
|
|
61
|
+
render json: {
|
|
62
|
+
key => records.map(&serializer),
|
|
63
|
+
:pagination => {limit: limit, offset: offset, count: records.size, total: relation.count}
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def render_error(status, message, **extra)
|
|
68
|
+
render json: {error: message, **extra}, status: status
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def angarium_render_unauthorized = render_error(:unauthorized, "authentication required")
|
|
72
|
+
def angarium_render_forbidden = render_error(:forbidden, "not authorized")
|
|
73
|
+
def angarium_render_not_found = render_error(:not_found, "not found")
|
|
74
|
+
|
|
75
|
+
def angarium_render_invalid(error)
|
|
76
|
+
render_error(:unprocessable_entity, "validation failed", details: error.record.errors.full_messages)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def angarium_render_unpermitted(error)
|
|
80
|
+
render_error(:unprocessable_entity, "unpermitted parameter", details: [error.message])
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# --- serializers ----------------------------------------------------------
|
|
84
|
+
# signing_secret and custom_headers are never echoed in responses (they're
|
|
85
|
+
# encrypted at rest and may carry credentials); the secret is revealed only
|
|
86
|
+
# on create and rotate_secret.
|
|
87
|
+
|
|
88
|
+
def endpoint_json(endpoint, include_secret: false)
|
|
89
|
+
json = {
|
|
90
|
+
id: endpoint.id,
|
|
91
|
+
name: endpoint.name,
|
|
92
|
+
url: endpoint.url,
|
|
93
|
+
status: endpoint.status,
|
|
94
|
+
subscribed_events: endpoint.subscribed_events,
|
|
95
|
+
allow_private_network: endpoint.allow_private_network,
|
|
96
|
+
allowed_networks: endpoint.allowed_networks,
|
|
97
|
+
consecutive_failures: endpoint.consecutive_failures,
|
|
98
|
+
status_changed_at: endpoint.status_changed_at&.iso8601,
|
|
99
|
+
created_at: endpoint.created_at.iso8601,
|
|
100
|
+
updated_at: endpoint.updated_at.iso8601
|
|
101
|
+
}
|
|
102
|
+
json[:signing_secret] = endpoint.signing_secret if include_secret
|
|
103
|
+
json
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def delivery_json(delivery)
|
|
107
|
+
{
|
|
108
|
+
id: delivery.id,
|
|
109
|
+
endpoint_id: delivery.endpoint_id,
|
|
110
|
+
event: delivery.event.name,
|
|
111
|
+
state: delivery.state,
|
|
112
|
+
attempt_count: delivery.attempt_count,
|
|
113
|
+
next_attempt_at: delivery.next_attempt_at&.iso8601,
|
|
114
|
+
last_attempt_at: delivery.last_attempt_at&.iso8601,
|
|
115
|
+
created_at: delivery.created_at.iso8601,
|
|
116
|
+
updated_at: delivery.updated_at.iso8601
|
|
117
|
+
}
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def attempt_json(attempt)
|
|
121
|
+
{
|
|
122
|
+
id: attempt.id,
|
|
123
|
+
delivery_id: attempt.delivery_id,
|
|
124
|
+
response_code: attempt.response_code,
|
|
125
|
+
response_body: attempt.response_body,
|
|
126
|
+
error: attempt.error,
|
|
127
|
+
duration: attempt.duration,
|
|
128
|
+
created_at: attempt.created_at.iso8601
|
|
129
|
+
}
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Angarium
|
|
2
|
+
module Api
|
|
3
|
+
class DeliveriesController < BaseController
|
|
4
|
+
# GET /endpoints/:endpoint_id/deliveries
|
|
5
|
+
def index
|
|
6
|
+
endpoint = endpoint_scope.find(params[:endpoint_id])
|
|
7
|
+
authorize!(endpoint)
|
|
8
|
+
render_collection(:deliveries, endpoint.deliveries.includes(:event).order(created_at: :desc)) { |d| delivery_json(d) }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# GET /deliveries/:id
|
|
12
|
+
# Attempts are fetched separately (and paginated) via
|
|
13
|
+
# GET /deliveries/:id/attempts.
|
|
14
|
+
def show
|
|
15
|
+
delivery = scoped_delivery(params[:id])
|
|
16
|
+
authorize!(delivery)
|
|
17
|
+
render json: {delivery: delivery_json(delivery)}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# POST /deliveries/:id/redeliver
|
|
21
|
+
def redeliver
|
|
22
|
+
delivery = scoped_delivery(params[:id])
|
|
23
|
+
authorize!(delivery)
|
|
24
|
+
delivery.redeliver!
|
|
25
|
+
render json: {delivery: delivery_json(delivery)}, status: :accepted
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
module Angarium
|
|
2
|
+
module Api
|
|
3
|
+
class EndpointsController < BaseController
|
|
4
|
+
before_action :set_endpoint, only: %i[show update destroy rotate_secret pause enable verify ping]
|
|
5
|
+
|
|
6
|
+
def index
|
|
7
|
+
authorize!
|
|
8
|
+
render_collection(:endpoints, endpoint_scope.order(created_at: :desc)) { |e| endpoint_json(e) }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def show
|
|
12
|
+
authorize!(@endpoint)
|
|
13
|
+
render json: {endpoint: endpoint_json(@endpoint)}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create
|
|
17
|
+
# The owner comes from the policy's #owner (default: current_user), set
|
|
18
|
+
# before authorize! so policy #create? can gate the target owner.
|
|
19
|
+
endpoint = Angarium::Endpoint.new(endpoint_params)
|
|
20
|
+
endpoint.owner = angarium_policy.owner
|
|
21
|
+
endpoint.status = :unverified if angarium_policy.create_unverified?
|
|
22
|
+
authorize!(endpoint)
|
|
23
|
+
endpoint.save!
|
|
24
|
+
# The signing secret is revealed once, on creation.
|
|
25
|
+
render json: {endpoint: endpoint_json(endpoint, include_secret: true)}, status: :created
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def update
|
|
29
|
+
authorize!(@endpoint)
|
|
30
|
+
@endpoint.update!(endpoint_params)
|
|
31
|
+
render json: {endpoint: endpoint_json(@endpoint)}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def destroy
|
|
35
|
+
authorize!(@endpoint)
|
|
36
|
+
@endpoint.destroy!
|
|
37
|
+
head :no_content
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def rotate_secret
|
|
41
|
+
authorize!(@endpoint)
|
|
42
|
+
secret = @endpoint.rotate_secret!
|
|
43
|
+
render json: {endpoint: endpoint_json(@endpoint), signing_secret: secret}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def pause
|
|
47
|
+
authorize!(@endpoint)
|
|
48
|
+
@endpoint.pause!
|
|
49
|
+
render json: {endpoint: endpoint_json(@endpoint)}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def enable
|
|
53
|
+
authorize!(@endpoint)
|
|
54
|
+
@endpoint.enable!
|
|
55
|
+
render json: {endpoint: endpoint_json(@endpoint)}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def verify
|
|
59
|
+
authorize!(@endpoint)
|
|
60
|
+
@endpoint.verify!
|
|
61
|
+
render json: {endpoint: endpoint_json(@endpoint)}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def ping
|
|
65
|
+
authorize!(@endpoint)
|
|
66
|
+
delivery = @endpoint.ping!
|
|
67
|
+
render json: {delivery: delivery_json(delivery)}, status: :accepted
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def set_endpoint
|
|
73
|
+
@endpoint = endpoint_scope.find(params[:id])
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# SSRF-relevant controls, each gated by its own policy predicate. They are
|
|
77
|
+
# independent: allow_private_network relaxes the private-IP denylist
|
|
78
|
+
# (dangerous), while allowed_networks only restricts delivery to a CIDR
|
|
79
|
+
# allowlist. permit shape per attribute (scalar vs array).
|
|
80
|
+
NETWORK_CONTROLS = {
|
|
81
|
+
allow_private_network: :permit_allow_private_network?,
|
|
82
|
+
allowed_networks: :permit_allowed_networks?
|
|
83
|
+
}.freeze
|
|
84
|
+
|
|
85
|
+
def endpoint_params
|
|
86
|
+
policy = angarium_policy
|
|
87
|
+
permitted = [:name, :url, {subscribed_events: [], custom_headers: {}}]
|
|
88
|
+
|
|
89
|
+
NETWORK_CONTROLS.each do |attr, predicate|
|
|
90
|
+
if policy.public_send(predicate)
|
|
91
|
+
permitted << ((attr == :allowed_networks) ? {allowed_networks: []} : attr)
|
|
92
|
+
else
|
|
93
|
+
reject_network_control_change!(attr)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
params.require(:endpoint).permit(*permitted)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# A request may not change a network control the policy doesn't permit.
|
|
101
|
+
# Attempting to (a submitted value that differs from the record's current
|
|
102
|
+
# value) is a 422 naming the attribute, so an escalation attempt fails
|
|
103
|
+
# loudly rather than being silently ignored. Echoing the current value is a
|
|
104
|
+
# no-op and allowed, so a client can round-trip the serialized endpoint.
|
|
105
|
+
def reject_network_control_change!(attr)
|
|
106
|
+
body = params.fetch(:endpoint, ActionController::Parameters.new)
|
|
107
|
+
return unless body.key?(attr)
|
|
108
|
+
|
|
109
|
+
current = (@endpoint || Angarium::Endpoint.new).public_send(attr)
|
|
110
|
+
submitted = body[attr]
|
|
111
|
+
submitted = Array(submitted).map(&:to_s) if attr == :allowed_networks
|
|
112
|
+
|
|
113
|
+
raise Angarium::Api::UnpermittedParameter, attr unless submitted == current
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Angarium
|
|
2
|
+
module Api
|
|
3
|
+
# Raised when a request tries to change an attribute the policy doesn't permit
|
|
4
|
+
# (e.g. a privileged SSRF control), so the caller fails loudly with a 422
|
|
5
|
+
# naming the attribute instead of the change being silently dropped.
|
|
6
|
+
class UnpermittedParameter < StandardError
|
|
7
|
+
attr_reader :attribute
|
|
8
|
+
|
|
9
|
+
def initialize(attribute)
|
|
10
|
+
@attribute = attribute
|
|
11
|
+
super("#{attribute} is not permitted")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Angarium
|
|
2
|
+
class ApplicationRecord < ActiveRecord::Base
|
|
3
|
+
self.abstract_class = true
|
|
4
|
+
|
|
5
|
+
# Multi-database support: point every Angarium table at a separate database
|
|
6
|
+
# instead of the app's primary connection. The host sets either config.database
|
|
7
|
+
# (a database name; the common case) or config.connects_to (a raw hash for
|
|
8
|
+
# custom roles/shards, which wins if both are set). Read once here at class
|
|
9
|
+
# load: config/initializers run before the models are first used or
|
|
10
|
+
# eager-loaded, so the setting is in place by then.
|
|
11
|
+
if Angarium.config.connects_to
|
|
12
|
+
connects_to(**Angarium.config.connects_to)
|
|
13
|
+
elsif (db = Angarium.config.database)
|
|
14
|
+
connects_to database: {writing: db, reading: db}
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|