ree_lib 1.3.13 → 1.3.15
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/Gemfile.lock +1 -1
- data/lib/ree_lib/packages/ree_audit/README.md +19 -11
- data/lib/ree_lib/packages/ree_audit/package/ree_audit/beans/audit.rb +14 -5
- data/lib/ree_lib/packages/ree_audit/package/ree_audit/event.rb +1 -1
- data/lib/ree_lib/packages/ree_audit/spec/ree_audit/beans/audit_spec.rb +14 -0
- data/lib/ree_lib/packages/ree_roda/spec/ree_roda/plugins/ree_routes_audit_spec.rb +17 -0
- data/lib/ree_lib/packages/ree_routes/package/ree_routes/dsl.rb +8 -0
- data/lib/ree_lib/packages/ree_routes/package/ree_routes/route.rb +11 -5
- data/lib/ree_lib/packages/ree_routes/spec/ree_routes/dsl_spec.rb +11 -1
- data/lib/ree_lib/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2ecb6d379981355aad60ca8e7f67493643cc351e3f5232c2d8baa326a7630d07
|
|
4
|
+
data.tar.gz: 2530c228029eb4df4e96d6208f7a0c13c22aeb263c573e69372683f4f195449f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b1cc7a1ffbb0a4946d8359bb24ac0340c567fe9631dd389c70dc78fba3b5812d963d7174352a0e50ea7c918e7004a2ffa9c989797673978687c7865199179eb0
|
|
7
|
+
data.tar.gz: 815c690e32475a28c02dc3bbf8b4b8808b0b23302dd0cb8e5f35322b688f26ed68f4e1d98a0c4e10a9b6d80194a5fafec15fa76d8a0e60895b394918c4446754
|
data/Gemfile.lock
CHANGED
|
@@ -54,11 +54,12 @@ through `ree_logger`.
|
|
|
54
54
|
|
|
55
55
|
## What gets audited
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
Nothing is audited until the route says so:
|
|
58
58
|
|
|
59
59
|
```ruby
|
|
60
60
|
get "api/v1/admin/users" do
|
|
61
|
-
visibility :internal
|
|
61
|
+
visibility :internal
|
|
62
|
+
audit true # recorded
|
|
62
63
|
end
|
|
63
64
|
|
|
64
65
|
get "api/v1/admin/health" do
|
|
@@ -66,19 +67,20 @@ get "api/v1/admin/health" do
|
|
|
66
67
|
audit false # deliberately silent
|
|
67
68
|
end
|
|
68
69
|
|
|
69
|
-
get "api/v1/exports" do
|
|
70
|
-
audit true # public route reaching into someone else's data
|
|
71
|
-
end
|
|
72
|
-
|
|
73
70
|
get "api/v1/organizations/:id/balance" do
|
|
74
71
|
audit :on_annotation # the owner calls it too — see below
|
|
75
72
|
end
|
|
76
73
|
```
|
|
77
74
|
|
|
78
|
-
|
|
79
|
-
`visibility`
|
|
80
|
-
|
|
81
|
-
|
|
75
|
+
The flag is declared at the route and nowhere else. Deriving it from
|
|
76
|
+
`visibility` would work just as well and read far worse: whether a call lands in
|
|
77
|
+
the access trail is exactly the kind of fact a reader must see where the route
|
|
78
|
+
is written, not infer from a flag that means something else.
|
|
79
|
+
|
|
80
|
+
Forgetting it is not possible where it matters: like `visibility`, `audit` is
|
|
81
|
+
mandatory on admin paths — a route whose path carries an `admin` segment fails
|
|
82
|
+
at boot until it declares one. So the audited set cannot drift away from the set
|
|
83
|
+
of routes that reach client data.
|
|
82
84
|
|
|
83
85
|
### `audit :on_annotation`
|
|
84
86
|
|
|
@@ -113,7 +115,7 @@ Annotating before the check, not after, keeps a refusal in the trail as well:
|
|
|
113
115
|
| `request_method`, `path`, `request_path` | `path` is the DSL template (stable), `request_path` is the actual path (shows the object) |
|
|
114
116
|
| `params` | what the action received, after filtering |
|
|
115
117
|
| `accessor` | the authenticated object — the application decides how to read it |
|
|
116
|
-
| `status` | `:ok`, `:denied`, `:error` |
|
|
118
|
+
| `status` | `:ok`, `:denied`, `:not_found`, `:error` |
|
|
117
119
|
| `error_type`, `error_message` | present when the call failed; message truncated to 512 chars |
|
|
118
120
|
| `started_at`, `duration_ms` | timing (monotonic clock) |
|
|
119
121
|
| `annotations` | whatever the application attached during the call |
|
|
@@ -122,6 +124,12 @@ Annotating before the check, not after, keeps a refusal in the trail as well:
|
|
|
122
124
|
`:permission`. A refusal to show client data is as much an audit fact as
|
|
123
125
|
showing it.
|
|
124
126
|
|
|
127
|
+
`:denied` and `:not_found` are split out of `:error` on purpose. A refusal by
|
|
128
|
+
permission and an ordinary "there was nothing to show" are both normal outcomes;
|
|
129
|
+
filing them next to a crash makes a compliance report say something broke when
|
|
130
|
+
nothing did. The split follows the domain error type, so an application gets it
|
|
131
|
+
without classifying anything itself.
|
|
132
|
+
|
|
125
133
|
There is **no HTTP status** on the event, and that is not an oversight: the
|
|
126
134
|
wrapper sits around the action call, while Roda sets the status afterwards (and,
|
|
127
135
|
on an exception, in the application's `error` block). Observing it here is
|
|
@@ -47,7 +47,7 @@ class ReeAudit::Audit
|
|
|
47
47
|
event.status = :ok
|
|
48
48
|
result
|
|
49
49
|
rescue StandardError => e
|
|
50
|
-
event.status =
|
|
50
|
+
event.status = status_for(e)
|
|
51
51
|
event.error_type = e.class.name
|
|
52
52
|
event.error_message = e.message.to_s[0, MAX_ERROR_MESSAGE_LENGTH]
|
|
53
53
|
raise
|
|
@@ -62,11 +62,20 @@ class ReeAudit::Audit
|
|
|
62
62
|
|
|
63
63
|
private
|
|
64
64
|
|
|
65
|
-
# A refusal to show client data is as much an audit fact as showing it
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
# A refusal to show client data is as much an audit fact as showing it — and
|
|
66
|
+
# it must not be filed next to a crash. Neither must "there was nothing to
|
|
67
|
+
# show": a domain 404 is an ordinary outcome, and reading it as a failure in a
|
|
68
|
+
# compliance report tells the reader something broke when nothing did.
|
|
69
|
+
def status_for(e)
|
|
70
|
+
return :error if !e.is_a?(ReeErrors::Error)
|
|
71
|
+
|
|
72
|
+
case e.type
|
|
73
|
+
when :permission then :denied
|
|
74
|
+
when :not_found then :not_found
|
|
75
|
+
else :error
|
|
76
|
+
end
|
|
68
77
|
rescue StandardError
|
|
69
|
-
|
|
78
|
+
:error
|
|
70
79
|
end
|
|
71
80
|
|
|
72
81
|
# The trail must never be the reason a request fails.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# enriched by the application through ReeAudit.annotate, and handed to a sink
|
|
5
5
|
# once the call is over.
|
|
6
6
|
class ReeAudit::Event
|
|
7
|
-
STATUSES = [:ok, :denied, :error].freeze
|
|
7
|
+
STATUSES = [:ok, :denied, :not_found, :error].freeze
|
|
8
8
|
|
|
9
9
|
attr_accessor :action_name, :package_name, :summary, :sections,
|
|
10
10
|
:request_method, :path, :request_path, :params, :accessor,
|
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
package_require('ree_audit/beans/audit')
|
|
4
4
|
package_require('ree_audit/event')
|
|
5
5
|
package_require('ree_errors/permission_error')
|
|
6
|
+
package_require('ree_errors/not_found_error')
|
|
6
7
|
|
|
7
8
|
RSpec.describe :audit do
|
|
8
9
|
link :audit, from: :ree_audit
|
|
9
10
|
link :config, from: :ree_audit
|
|
10
11
|
link :permission_error, from: :ree_errors
|
|
12
|
+
link :not_found_error, from: :ree_errors
|
|
11
13
|
link :logger, from: :ree_logger
|
|
12
14
|
|
|
13
15
|
# Captures what the application would have persisted.
|
|
@@ -89,6 +91,18 @@ RSpec.describe :audit do
|
|
|
89
91
|
expect(written.error_message).to eq("not allowed")
|
|
90
92
|
end
|
|
91
93
|
|
|
94
|
+
# 404 предметной области — обычный исход, а не поломка: строка «нечего
|
|
95
|
+
# показать» в отчёте соответствия не должна читаться как сбой.
|
|
96
|
+
it "records a domain not-found as its own outcome, not as a failure" do
|
|
97
|
+
klass = not_found_error(:missing)
|
|
98
|
+
|
|
99
|
+
expect {
|
|
100
|
+
audit.around(event) { raise klass.new("nothing here") }
|
|
101
|
+
}.to raise_error(klass)
|
|
102
|
+
|
|
103
|
+
expect(test_sink.events.first.status).to eq(:not_found)
|
|
104
|
+
end
|
|
105
|
+
|
|
92
106
|
it "records an arbitrary failure as an error and lets it through" do
|
|
93
107
|
expect {
|
|
94
108
|
audit.around(event) { raise ArgumentError.new("boom") }
|
|
@@ -99,6 +99,14 @@ RSpec.describe "ree_routes audit" do
|
|
|
99
99
|
summary "Internal route"
|
|
100
100
|
sections "Admin"
|
|
101
101
|
visibility :internal
|
|
102
|
+
audit true
|
|
103
|
+
action :ok_cmd, **opts
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
get "api/internal_undeclared" do
|
|
107
|
+
summary "Internal route that never mentions audit"
|
|
108
|
+
sections "Admin"
|
|
109
|
+
visibility :internal
|
|
102
110
|
action :ok_cmd, **opts
|
|
103
111
|
end
|
|
104
112
|
|
|
@@ -148,6 +156,7 @@ RSpec.describe "ree_routes audit" do
|
|
|
148
156
|
summary "Internal route refusing access"
|
|
149
157
|
sections "Admin"
|
|
150
158
|
visibility :internal
|
|
159
|
+
audit true
|
|
151
160
|
action :denied_cmd, **opts
|
|
152
161
|
end
|
|
153
162
|
|
|
@@ -155,6 +164,7 @@ RSpec.describe "ree_routes audit" do
|
|
|
155
164
|
summary "Internal route blowing up"
|
|
156
165
|
sections "Admin"
|
|
157
166
|
visibility :internal
|
|
167
|
+
audit true
|
|
158
168
|
action :boom_cmd, **opts
|
|
159
169
|
end
|
|
160
170
|
end
|
|
@@ -274,6 +284,13 @@ RSpec.describe "ree_routes audit" do
|
|
|
274
284
|
expect(event.accessor).to eq({user: "visitor"})
|
|
275
285
|
end
|
|
276
286
|
|
|
287
|
+
it "leaves an internal route that never declared audit out of the trail" do
|
|
288
|
+
get "api/internal_undeclared"
|
|
289
|
+
|
|
290
|
+
expect(last_response.status).to eq(200)
|
|
291
|
+
expect(test_sink.events).to be_empty
|
|
292
|
+
end
|
|
293
|
+
|
|
277
294
|
it "leaves a silenced internal route out of the trail" do
|
|
278
295
|
get "api/internal_silenced"
|
|
279
296
|
|
|
@@ -97,6 +97,14 @@ module ReeRoutes
|
|
|
97
97
|
)
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
+
if !builder.get_route.audit_declared? && admin_path?(path)
|
|
101
|
+
raise ArgumentError.new(
|
|
102
|
+
"admin route #{path} must declare audit explicitly: " \
|
|
103
|
+
"`audit true` to record the call in the access trail, " \
|
|
104
|
+
"or `audit false` if the route touches nothing worth recording"
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
100
108
|
route = builder.get_route
|
|
101
109
|
|
|
102
110
|
@dsl.link(route.action.name, from: route.action.package_name)
|
|
@@ -16,12 +16,18 @@ class ReeRoutes::Route
|
|
|
16
16
|
!public?
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
19
|
+
# Auditing is declared at the route and nowhere else. Deriving it from
|
|
20
|
+
# `visibility` would work just as well and read far worse: whether a call
|
|
21
|
+
# lands in the access trail is exactly the kind of fact that must be visible
|
|
22
|
+
# where the route is written, not inferred from a flag that means something
|
|
23
|
+
# else. `audit` is mandatory on admin paths (see DSL#define_route), so the
|
|
24
|
+
# audited set still cannot drift away from the routes that touch client data.
|
|
23
25
|
def audited?
|
|
24
|
-
@audit
|
|
26
|
+
@audit == true || @audit == :on_annotation
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def audit_declared?
|
|
30
|
+
!@audit.nil?
|
|
25
31
|
end
|
|
26
32
|
|
|
27
33
|
# `audit :on_annotation` — a public route that the client calls for its own
|
|
@@ -76,7 +76,14 @@ RSpec.describe ReeRoutes::DSL, type: [:autoclean] do
|
|
|
76
76
|
default_warden_scope :user
|
|
77
77
|
|
|
78
78
|
get "internal_default" do
|
|
79
|
-
summary "Internal route
|
|
79
|
+
summary "Internal route that says it is audited"
|
|
80
|
+
action :cmd, from: :ree_routes_test
|
|
81
|
+
visibility :internal
|
|
82
|
+
audit true
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
get "internal_undeclared" do
|
|
86
|
+
summary "Internal route that never mentions audit"
|
|
80
87
|
action :cmd, from: :ree_routes_test
|
|
81
88
|
visibility :internal
|
|
82
89
|
end
|
|
@@ -137,7 +144,10 @@ RSpec.describe ReeRoutes::DSL, type: [:autoclean] do
|
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
expect(routes["internal_default"].audited?).to eq(true)
|
|
147
|
+
expect(routes["internal_undeclared"].audited?).to eq(false)
|
|
148
|
+
expect(routes["internal_undeclared"].audit_declared?).to eq(false)
|
|
140
149
|
expect(routes["internal_silenced"].audited?).to eq(false)
|
|
150
|
+
expect(routes["internal_silenced"].audit_declared?).to eq(true)
|
|
141
151
|
expect(routes["public_default"].audited?).to eq(false)
|
|
142
152
|
expect(routes["public_audited"].audited?).to eq(true)
|
|
143
153
|
expect(routes["public_on_annotation"].audited?).to eq(true)
|
data/lib/ree_lib/version.rb
CHANGED