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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5a4843ec9ab12ce886333af41c258c9994ced5666fc6720a443fd42e7b8e1300
4
- data.tar.gz: de155878c3031c3bcbfc24a3ca1b2fb1cf6822420cbd3959f3221da9172f1769
3
+ metadata.gz: 2ecb6d379981355aad60ca8e7f67493643cc351e3f5232c2d8baa326a7630d07
4
+ data.tar.gz: 2530c228029eb4df4e96d6208f7a0c13c22aeb263c573e69372683f4f195449f
5
5
  SHA512:
6
- metadata.gz: 399d35c7b455d605572dcf8c5ebd426de556401e9eb6fd75b0c47745abf691dc8956f743e94e3ab8674c991d6bea040d55901e0b9443d7ec55a7c7bbc609f65a
7
- data.tar.gz: ecad1589e6231b1fee0baa64d7bfcc5fea36dcb23deb5b02b6bbe9f0bcc307934f50ec086b7ec795c55a8a22c899516c8680b1bb0943c50136dcb9020963914d
6
+ metadata.gz: b1cc7a1ffbb0a4946d8359bb24ac0340c567fe9631dd389c70dc78fba3b5812d963d7174352a0e50ea7c918e7004a2ffa9c989797673978687c7865199179eb0
7
+ data.tar.gz: 815c690e32475a28c02dc3bbf8b4b8808b0b23302dd0cb8e5f35322b688f26ed68f4e1d98a0c4e10a9b6d80194a5fafec15fa76d8a0e60895b394918c4446754
data/Gemfile.lock CHANGED
@@ -9,7 +9,7 @@ PATH
9
9
  PATH
10
10
  remote: .
11
11
  specs:
12
- ree_lib (1.3.13)
12
+ ree_lib (1.3.15)
13
13
  bigdecimal
14
14
  binding_of_caller
15
15
  i18n
@@ -54,11 +54,12 @@ through `ree_logger`.
54
54
 
55
55
  ## What gets audited
56
56
 
57
- `Route#audited?` returns `internal?` unless the route says otherwise:
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 # audited by default
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
- Tying the default to `visibility` rather than to a hand-kept list is deliberate:
79
- `visibility` is mandatory on admin paths (the routes DSL fails at boot without
80
- it), so the audited set cannot drift away from the set of routes that actually
81
- touch client data.
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 = denied?(e) ? :denied : :error
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
- def denied?(e)
67
- e.is_a?(ReeErrors::Error) && e.type == :permission
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
- false
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
- # Internal routes are audited unless told otherwise. `visibility` is
20
- # mandatory for admin paths (see DSL#define_route), so this list can never
21
- # drift away from the set of routes that actually touch client data which a
22
- # hand-maintained list of audited routes inevitably would.
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.nil? ? internal? : @audit != false
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, audited by default"
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ReeLib
4
- VERSION = "1.3.13"
4
+ VERSION = "1.3.15"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ree_lib
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.13
4
+ version: 1.3.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ruslan Gatiyatov