ree_lib 1.3.12 → 1.3.14

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: e10f8d9ba3558f200f081f325d2a7f10b81696459c8fbdd9054fd4e0b72c72ed
4
- data.tar.gz: fc2f2bba979d98082ed296caeeb8823cf1cef3201cbda6244adb131e7ccc5db1
3
+ metadata.gz: 2ebed252b6557db13e60268884fd6b1a0f405f256d28aaf18953764886ab6cb0
4
+ data.tar.gz: e6328fda6013213c588f834b5a0782c271c76686b8303a0f458cfac5e926c80d
5
5
  SHA512:
6
- metadata.gz: 4cbd59039f6e3b238db9c88763e75f753a567c13edd080eda2e6fafbed838e3c61a71d9ee40af35b7042f58cee315f5aa3c7cdf8be8b2e7ae339f9ed7883906d
7
- data.tar.gz: e0dddd85f84fec535514ebbec51e78425511b32a8ee877b65fdf4ac31f513717f9c12ab80d4772d7649ad1856ad625bf1d967e9c56d78823dc1435196b438c85
6
+ metadata.gz: 06ad16d1ca9361397b0e324d8ab0d9a3a064337831384f45616dd500efb88367c84be252537d203e07ec08cb109e8f976980ff39ac2de315f96938a21edc697e
7
+ data.tar.gz: fa3b6b910f9855585af6f0e985720db3773d7483779f65d3907badc16711e5bfdd159abcfcd8135466a5a2c57a75645da44c79ba532e0bcd9bf777d50aa7bdd5
data/Gemfile.lock CHANGED
@@ -9,7 +9,7 @@ PATH
9
9
  PATH
10
10
  remote: .
11
11
  specs:
12
- ree_lib (1.3.12)
12
+ ree_lib (1.3.14)
13
13
  bigdecimal
14
14
  binding_of_caller
15
15
  i18n
@@ -0,0 +1,204 @@
1
+ # ree_audit
2
+
3
+ An opt-in trail of audited action calls.
4
+
5
+ The package answers one question: **who called what, when, and how did it end.**
6
+ It says nothing about what happened inside the action, and nothing about files
7
+ downloaded straight from object storage — see *Boundaries* below.
8
+
9
+ ## Why it exists
10
+
11
+ An application that serves an internal (admin) API eventually has to answer a
12
+ customer, an auditor or a regulator: *did your staff look at my data, and when?*
13
+ Answering that from request logs does not work — they rotate, they contain
14
+ secrets, and they cannot be handed to a customer. This package produces a
15
+ structured record per audited call that an application can persist wherever it
16
+ wants and export as-is.
17
+
18
+ ## Off by default
19
+
20
+ `AUDIT_ENABLED` defaults to `false`, and an application that never passes
21
+ `audit:` to the `ree_routes` plugin behaves exactly as it did before the gem was
22
+ bumped. Both properties are covered by specs: a shared framework must not start
23
+ recording anything because someone upgraded a dependency.
24
+
25
+ When audit is disabled, `Audit#around` costs a single `if`.
26
+
27
+ ## Wiring it up
28
+
29
+ ```ruby
30
+ class MyApp < ReeRoda::App
31
+ link :audit, from: :ree_audit, import: -> { Audit }
32
+ link :my_sink, from: :my_package, import: -> { MySink }
33
+
34
+ AUDIT = Audit.new # singleton bean
35
+ AUDIT.sink = MySink.new # the one and only mutation, at boot
36
+
37
+ plugin :ree_routes, audit: AUDIT
38
+ end
39
+ ```
40
+
41
+ A sink implements one method:
42
+
43
+ ```ruby
44
+ class MySink < ReeAudit::Sink
45
+ def write(event)
46
+ # persist it
47
+ end
48
+ end
49
+ ```
50
+
51
+ The package ships `NullSink` (default) and `LoggerSink` (dev). A sink that
52
+ raises never fails the request: `Audit#write_safely` catches it and reports
53
+ through `ree_logger`.
54
+
55
+ ## What gets audited
56
+
57
+ Nothing is audited until the route says so:
58
+
59
+ ```ruby
60
+ get "api/v1/admin/users" do
61
+ visibility :internal
62
+ audit true # recorded
63
+ end
64
+
65
+ get "api/v1/admin/health" do
66
+ visibility :internal
67
+ audit false # deliberately silent
68
+ end
69
+
70
+ get "api/v1/organizations/:id/balance" do
71
+ audit :on_annotation # the owner calls it too — see below
72
+ end
73
+ ```
74
+
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.
84
+
85
+ ### `audit :on_annotation`
86
+
87
+ Some routes are called by two different people: the owner of the data, reading
88
+ its own, and a staff member reading someone else's. Which one it was is known
89
+ only after the permission check has run, inside the action — so the event is
90
+ always assembled, and it reaches the sink only if the code annotated it:
91
+
92
+ ```ruby
93
+ def call(organization_id, user_id)
94
+ admin = platform_admins.active.by_user(user_id).first
95
+
96
+ if admin
97
+ # this call was made by staff, and that is what makes it worth recording
98
+ annotate_audit(organization_id: organization_id, staff_id: admin.id)
99
+ end
100
+ ...
101
+ end
102
+ ```
103
+
104
+ `audit true` on such a route would bury the handful of staff calls under every
105
+ ordinary request the owners make; leaving it unaudited would lose them.
106
+ Annotating before the check, not after, keeps a refusal in the trail as well:
107
+ `Event#recordable?` is decided in `ensure`, when the annotation is already there.
108
+
109
+ ## The event
110
+
111
+ | field | meaning |
112
+ |---|---|
113
+ | `action_name`, `package_name` | which action ran |
114
+ | `summary`, `sections` | route metadata, useful for grouping and for classifying sensitivity |
115
+ | `request_method`, `path`, `request_path` | `path` is the DSL template (stable), `request_path` is the actual path (shows the object) |
116
+ | `params` | what the action received, after filtering |
117
+ | `accessor` | the authenticated object — the application decides how to read it |
118
+ | `status` | `:ok`, `:denied`, `:error` |
119
+ | `error_type`, `error_message` | present when the call failed; message truncated to 512 chars |
120
+ | `started_at`, `duration_ms` | timing (monotonic clock) |
121
+ | `annotations` | whatever the application attached during the call |
122
+
123
+ `:denied` means the action raised a `ReeErrors::Error` whose type is
124
+ `:permission`. A refusal to show client data is as much an audit fact as
125
+ showing it.
126
+
127
+ There is **no HTTP status** on the event, and that is not an oversight: the
128
+ wrapper sits around the action call, while Roda sets the status afterwards (and,
129
+ on an exception, in the application's `error` block). Observing it here is
130
+ impossible, and deriving it from the error type would put a guess into an audit
131
+ trail. `status` carries the fact instead.
132
+
133
+ ## Annotations
134
+
135
+ Product code attaches what only it knows:
136
+
137
+ ```ruby
138
+ class MyPackage::AdminUploadQuery
139
+ action :admin_upload_query do
140
+ link :annotate_audit, from: :ree_audit
141
+ end
142
+
143
+ def call(access, attrs)
144
+ upload = find_upload(attrs)
145
+ annotate_audit(organization_id: upload.organization_id,
146
+ subject: {type: :upload, id: upload.uuid})
147
+ ...
148
+ end
149
+ end
150
+ ```
151
+
152
+ `annotate_audit` returns `nil` when audit is off or the code was reached from
153
+ outside an audited route, so it is always safe to call.
154
+
155
+ Use the `annotate_audit` fn rather than `ReeAudit.annotate` directly. A bare
156
+ constant works, but Ree only verifies a declared package dependency for
157
+ `link`/`import` — so removing `depends_on :ree_audit` as "unused" would compile
158
+ fine and blow up with `NameError` in production. With `link`, the same removal
159
+ fails loudly at load time.
160
+
161
+ ## Params filtering
162
+
163
+ Two limits, applied before the event reaches the sink:
164
+
165
+ * keys containing any `filter_words` are replaced with `'FILTERED'` **at any
166
+ depth** (the logger only inspects the top level);
167
+ * anything nested deeper than `AUDIT_MAX_PARAMS_DEPTH` becomes
168
+ `'[TRUNCATED_DEPTH]'`, and a payload whose JSON exceeds
169
+ `AUDIT_MAX_PARAMS_BYTES` is replaced with `{truncated: true, size: N}`.
170
+
171
+ This is a blacklist, and it is the framework's job: it protects any host
172
+ application from leaking a credential. An application that exports the trail to
173
+ its customers should apply its own **whitelist** on top — a blacklist silently
174
+ passes through a key someone adds six months from now.
175
+
176
+ ## Nested calls
177
+
178
+ Events live on a per-fiber stack (`ReeAudit::Context`), so an audited action may
179
+ call another one without the inner call stealing the outer one's annotations.
180
+ The stack is unwound in `ensure`, including when the action raises — a leftover
181
+ event would attach one client's context to the next request served by the same
182
+ fiber.
183
+
184
+ ## Configuration
185
+
186
+ | variable | default | meaning |
187
+ |---|---|---|
188
+ | `AUDIT_ENABLED` | `false` | master switch |
189
+ | `AUDIT_MAX_PARAMS_BYTES` | `4096` | size limit for the serialized params |
190
+ | `AUDIT_MAX_PARAMS_DEPTH` | `4` | nesting limit |
191
+ | `AUDIT_FILTER_WORDS` | see `Config::DEFAULT_FILTER_WORDS` | comma-separated |
192
+
193
+ ## Boundaries
194
+
195
+ The package answers *who called what*. It does not see anything that bypasses
196
+ the application: direct database access, an object-storage console, or a file
197
+ fetched with a pre-signed URL that was handed out earlier. An application that
198
+ needs those answers has to record them where they happen.
199
+
200
+ ## Specs
201
+
202
+ ```
203
+ cd ree_lib/lib/ree_lib && bundle exec ree spec ree_audit
204
+ ```
@@ -56,7 +56,7 @@ class ReeAudit::Audit
56
56
  (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_monotonic) * 1000
57
57
  ).round
58
58
  ReeAudit::Context.pop
59
- write_safely(event)
59
+ write_safely(event) if event.recordable?
60
60
  end
61
61
  end
62
62
 
@@ -9,7 +9,7 @@ class ReeAudit::Event
9
9
  attr_accessor :action_name, :package_name, :summary, :sections,
10
10
  :request_method, :path, :request_path, :params, :accessor,
11
11
  :status, :error_type, :error_message, :started_at,
12
- :duration_ms, :annotations
12
+ :duration_ms, :annotations, :on_annotation
13
13
 
14
14
  class << self
15
15
  # Keyword-only on purpose: callers live in another package and must not
@@ -23,7 +23,8 @@ class ReeAudit::Event
23
23
  path: nil,
24
24
  request_path: nil,
25
25
  params: {},
26
- accessor: nil
26
+ accessor: nil,
27
+ on_annotation: false
27
28
  )
28
29
  event = new
29
30
 
@@ -37,6 +38,7 @@ class ReeAudit::Event
37
38
  event.params = params || {}
38
39
  event.accessor = accessor
39
40
  event.annotations = {}
41
+ event.on_annotation = on_annotation
40
42
 
41
43
  event
42
44
  end
@@ -47,6 +49,7 @@ class ReeAudit::Event
47
49
  @params = {}
48
50
  @annotations = {}
49
51
  @status = :ok
52
+ @on_annotation = false
50
53
  end
51
54
 
52
55
  def ok?
@@ -57,6 +60,14 @@ class ReeAudit::Event
57
60
  status == :denied
58
61
  end
59
62
 
63
+ # An `on_annotation` event is the same call seen from two sides: the client
64
+ # asking for its own data, and a staff member asking for the client's. Only
65
+ # the code that resolved the caller can tell them apart, and it says so by
66
+ # annotating — so an untouched event of that kind is not worth recording.
67
+ def recordable?
68
+ !on_annotation || !annotations.empty?
69
+ end
70
+
60
71
  def to_h
61
72
  {
62
73
  action_name: action_name,
@@ -158,4 +158,41 @@ RSpec.describe :audit do
158
158
  it "ignores an annotation made outside an audited call" do
159
159
  expect(ReeAudit.annotate(permission: :access_uploads)).to be_nil
160
160
  end
161
+
162
+ context "an :on_annotation event" do
163
+ let(:event) {
164
+ ReeAudit::Event.build(action_name: :balance_query, on_annotation: true)
165
+ }
166
+
167
+ it "drops the call nobody annotated" do
168
+ result = audit.around(event) { :action_result }
169
+
170
+ expect(result).to eq(:action_result)
171
+ expect(test_sink.events).to be_empty
172
+ end
173
+
174
+ it "writes the call the action annotated" do
175
+ audit.around(event) do
176
+ ReeAudit.annotate(platform_admin_id: 7)
177
+ :action_result
178
+ end
179
+
180
+ expect(test_sink.events.size).to eq(1)
181
+ expect(test_sink.events.first.annotations).to eq({platform_admin_id: 7})
182
+ end
183
+
184
+ it "writes a refusal that was annotated before the raise" do
185
+ klass = permission_error(:no_access)
186
+
187
+ expect {
188
+ audit.around(event) do
189
+ ReeAudit.annotate(platform_admin_id: 7)
190
+ raise klass.new("not allowed")
191
+ end
192
+ }.to raise_error(klass)
193
+
194
+ expect(test_sink.events.size).to eq(1)
195
+ expect(test_sink.events.first.status).to eq(:denied)
196
+ end
197
+ end
161
198
  end
@@ -146,7 +146,8 @@ class Roda
146
146
  path: route.path,
147
147
  request_path: r.path,
148
148
  params: filtered_params,
149
- accessor: accessor
149
+ accessor: accessor,
150
+ on_annotation: route.audit_on_annotation?
150
151
  )
151
152
 
152
153
  audit.around(audit_event) do
@@ -49,6 +49,34 @@ RSpec.describe "ree_routes audit" do
49
49
  end
50
50
  end
51
51
 
52
+ # Клиентская ручка глазами сотрудника: код опознаёт служебное обращение и
53
+ # помечает его аннотацией.
54
+ class ReeRodaAuditTest::StaffCmd
55
+ include ReeActions::DSL
56
+
57
+ action :staff_cmd
58
+
59
+ def call(access, attrs)
60
+ ReeAudit.annotate(staff_id: 42)
61
+ {result: "ok"}
62
+ end
63
+ end
64
+
65
+ class ReeRodaAuditTest::DeniedStaffCmd
66
+ include ReeActions::DSL
67
+
68
+ action :denied_staff_cmd do
69
+ link :permission_error, from: :ree_errors
70
+ end
71
+
72
+ PermissionErr = permission_error(:no_access, msg: "not allowed")
73
+
74
+ def call(access, attrs)
75
+ ReeAudit.annotate(staff_id: 42)
76
+ raise PermissionErr
77
+ end
78
+ end
79
+
52
80
  class ReeRodaAuditTest::BoomCmd
53
81
  include ReeActions::DSL
54
82
 
@@ -71,6 +99,14 @@ RSpec.describe "ree_routes audit" do
71
99
  summary "Internal route"
72
100
  sections "Admin"
73
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
74
110
  action :ok_cmd, **opts
75
111
  end
76
112
 
@@ -95,10 +131,32 @@ RSpec.describe "ree_routes audit" do
95
131
  action :ok_cmd, **opts
96
132
  end
97
133
 
134
+ get "api/on_annotation_client" do
135
+ summary "Client reading its own data"
136
+ sections "Public"
137
+ audit :on_annotation
138
+ action :ok_cmd, **opts
139
+ end
140
+
141
+ get "api/on_annotation_staff" do
142
+ summary "Staff reading someone else's data"
143
+ sections "Public"
144
+ audit :on_annotation
145
+ action :staff_cmd, **opts
146
+ end
147
+
148
+ get "api/on_annotation_staff_denied" do
149
+ summary "Staff refused someone else's data"
150
+ sections "Public"
151
+ audit :on_annotation
152
+ action :denied_staff_cmd, **opts
153
+ end
154
+
98
155
  get "api/internal_denied" do
99
156
  summary "Internal route refusing access"
100
157
  sections "Admin"
101
158
  visibility :internal
159
+ audit true
102
160
  action :denied_cmd, **opts
103
161
  end
104
162
 
@@ -106,6 +164,7 @@ RSpec.describe "ree_routes audit" do
106
164
  summary "Internal route blowing up"
107
165
  sections "Admin"
108
166
  visibility :internal
167
+ audit true
109
168
  action :boom_cmd, **opts
110
169
  end
111
170
  end
@@ -225,6 +284,13 @@ RSpec.describe "ree_routes audit" do
225
284
  expect(event.accessor).to eq({user: "visitor"})
226
285
  end
227
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
+
228
294
  it "leaves a silenced internal route out of the trail" do
229
295
  get "api/internal_silenced"
230
296
 
@@ -247,6 +313,31 @@ RSpec.describe "ree_routes audit" do
247
313
  expect(test_sink.events.first.status).to eq(:ok)
248
314
  end
249
315
 
316
+ it "leaves an unannotated :on_annotation route out of the trail" do
317
+ get "api/on_annotation_client"
318
+
319
+ expect(last_response.status).to eq(200)
320
+ expect(test_sink.events).to be_empty
321
+ end
322
+
323
+ it "writes an :on_annotation route once the action annotated it" do
324
+ get "api/on_annotation_staff"
325
+
326
+ expect(last_response.status).to eq(200)
327
+ expect(test_sink.events.size).to eq(1)
328
+ expect(test_sink.events.first.annotations).to eq({staff_id: 42})
329
+ end
330
+
331
+ # Отказ сотруднику — такой же факт журнала, как и показ данных: аннотация
332
+ # успевает лечь до того, как действие бросит исключение.
333
+ it "writes an annotated :on_annotation route that ended in a refusal" do
334
+ get "api/on_annotation_staff_denied"
335
+
336
+ expect(last_response.status).to eq(403)
337
+ expect(test_sink.events.size).to eq(1)
338
+ expect(test_sink.events.first.status).to eq(:denied)
339
+ end
340
+
250
341
  it "records a refusal and still answers 403" do
251
342
  get "api/internal_denied"
252
343
 
@@ -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,27 @@ 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
26
+ @audit == true || @audit == :on_annotation
27
+ end
28
+
29
+ def audit_declared?
30
+ !@audit.nil?
31
+ end
32
+
33
+ # `audit :on_annotation` — a public route that the client calls for its own
34
+ # data and a staff member calls for someone else's. The event is always
35
+ # assembled, but it only reaches the sink if the code annotated the call;
36
+ # auditing such a route unconditionally would bury the staff access under
37
+ # every ordinary client request.
38
+ def audit_on_annotation?
39
+ @audit == :on_annotation
25
40
  end
26
41
 
27
42
  def valid?
@@ -6,6 +6,7 @@ class ReeRoutes::RouteBuilder
6
6
  Redirect = Struct.new(:path, :code)
7
7
 
8
8
  VISIBILITIES = [:public, :internal].freeze
9
+ AUDIT_MODES = [true, false, :on_annotation].freeze
9
10
 
10
11
  def initialize
11
12
  @route = ReeRoutes::Route.new
@@ -44,10 +45,18 @@ class ReeRoutes::RouteBuilder
44
45
  @route.summary = str
45
46
  end
46
47
 
47
- # Needed in exactly two cases: silencing a noisy internal route, and turning
48
- # the trail on for a public route that reaches into someone else's data.
49
- contract Bool => Bool
48
+ # Needed in exactly three cases: silencing a noisy internal route, turning
49
+ # the trail on for a public route that reaches into someone else's data, and
50
+ # `:on_annotation` for a route both the owner of the data and a staff member
51
+ # can call — see Route#audit_on_annotation?.
52
+ contract Or[Bool, Symbol] => Or[Bool, Symbol]
50
53
  def audit(value)
54
+ if !AUDIT_MODES.include?(value)
55
+ raise ArgumentError.new(
56
+ "audit should be one of #{AUDIT_MODES.inspect}, got #{value.inspect}"
57
+ )
58
+ end
59
+
51
60
  @route.audit = value
52
61
  end
53
62
 
@@ -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
@@ -98,6 +105,12 @@ RSpec.describe ReeRoutes::DSL, type: [:autoclean] do
98
105
  action :cmd, from: :ree_routes_test
99
106
  audit true
100
107
  end
108
+
109
+ get "public_on_annotation" do
110
+ summary "Public route the owner and a staff member both call"
111
+ action :cmd, from: :ree_routes_test
112
+ audit :on_annotation
113
+ end
101
114
  end
102
115
  end
103
116
  end
@@ -131,8 +144,22 @@ RSpec.describe ReeRoutes::DSL, type: [:autoclean] do
131
144
  }
132
145
 
133
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)
134
149
  expect(routes["internal_silenced"].audited?).to eq(false)
150
+ expect(routes["internal_silenced"].audit_declared?).to eq(true)
135
151
  expect(routes["public_default"].audited?).to eq(false)
136
152
  expect(routes["public_audited"].audited?).to eq(true)
153
+ expect(routes["public_on_annotation"].audited?).to eq(true)
154
+
155
+ expect(routes["internal_default"].audit_on_annotation?).to eq(false)
156
+ expect(routes["public_audited"].audit_on_annotation?).to eq(false)
157
+ expect(routes["public_on_annotation"].audit_on_annotation?).to eq(true)
137
158
  }
159
+
160
+ it "refuses an audit mode it does not know" do
161
+ expect {
162
+ ReeRoutes::RouteBuilder.new.audit(:on_annotations)
163
+ }.to raise_error(ArgumentError, /audit should be one of/)
164
+ end
138
165
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ReeLib
4
- VERSION = "1.3.12"
4
+ VERSION = "1.3.14"
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.12
4
+ version: 1.3.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ruslan Gatiyatov
@@ -277,6 +277,7 @@ files:
277
277
  - lib/ree_lib/packages/ree_array/spec/ree_array/functions/wrap_spec.rb
278
278
  - lib/ree_lib/packages/ree_array/spec/spec_helper.rb
279
279
  - lib/ree_lib/packages/ree_audit/.rspec
280
+ - lib/ree_lib/packages/ree_audit/README.md
280
281
  - lib/ree_lib/packages/ree_audit/package/ree_audit.rb
281
282
  - lib/ree_lib/packages/ree_audit/package/ree_audit/beans/audit.rb
282
283
  - lib/ree_lib/packages/ree_audit/package/ree_audit/config.rb