cased-ruby 0.3.3

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.
Files changed (93) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/rubocop.yml +21 -0
  3. data/.github/workflows/ruby.yml +46 -0
  4. data/.gitignore +10 -0
  5. data/.rubocop.yml +88 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +7 -0
  8. data/CODE_OF_CONDUCT.md +74 -0
  9. data/Gemfile +8 -0
  10. data/Gemfile.lock +107 -0
  11. data/LICENSE +21 -0
  12. data/README.md +661 -0
  13. data/Rakefile +12 -0
  14. data/bin/console +15 -0
  15. data/bin/rubocop +29 -0
  16. data/cased-ruby.gemspec +48 -0
  17. data/lib/cased-ruby.rb +3 -0
  18. data/lib/cased.rb +260 -0
  19. data/lib/cased/clients.rb +21 -0
  20. data/lib/cased/collection_response.rb +117 -0
  21. data/lib/cased/config.rb +172 -0
  22. data/lib/cased/context.rb +50 -0
  23. data/lib/cased/context/expander.rb +33 -0
  24. data/lib/cased/error.rb +8 -0
  25. data/lib/cased/http/client.rb +83 -0
  26. data/lib/cased/http/error.rb +99 -0
  27. data/lib/cased/instrumentation/controller.rb +34 -0
  28. data/lib/cased/instrumentation/log_subscriber.rb +31 -0
  29. data/lib/cased/integrations/sidekiq.rb +17 -0
  30. data/lib/cased/integrations/sidekiq/client_middleware.rb +14 -0
  31. data/lib/cased/integrations/sidekiq/server_middleware.rb +20 -0
  32. data/lib/cased/model.rb +98 -0
  33. data/lib/cased/policy.rb +24 -0
  34. data/lib/cased/publishers.rb +6 -0
  35. data/lib/cased/publishers/active_support_publisher.rb +16 -0
  36. data/lib/cased/publishers/base.rb +17 -0
  37. data/lib/cased/publishers/error.rb +11 -0
  38. data/lib/cased/publishers/http_publisher.rb +15 -0
  39. data/lib/cased/publishers/null_publisher.rb +11 -0
  40. data/lib/cased/publishers/test_publisher.rb +19 -0
  41. data/lib/cased/query.rb +87 -0
  42. data/lib/cased/rack_middleware.rb +15 -0
  43. data/lib/cased/response.rb +37 -0
  44. data/lib/cased/sensitive.rb +4 -0
  45. data/lib/cased/sensitive/handler.rb +54 -0
  46. data/lib/cased/sensitive/processor.rb +78 -0
  47. data/lib/cased/sensitive/range.rb +54 -0
  48. data/lib/cased/sensitive/result.rb +8 -0
  49. data/lib/cased/sensitive/string.rb +43 -0
  50. data/lib/cased/test_helper.rb +188 -0
  51. data/lib/cased/version.rb +5 -0
  52. data/vendor/cache/activesupport-6.0.3.4.gem +0 -0
  53. data/vendor/cache/addressable-2.7.0.gem +0 -0
  54. data/vendor/cache/ast-2.4.0.gem +0 -0
  55. data/vendor/cache/byebug-11.0.1.gem +0 -0
  56. data/vendor/cache/concurrent-ruby-1.1.7.gem +0 -0
  57. data/vendor/cache/connection_pool-2.2.2.gem +0 -0
  58. data/vendor/cache/crack-0.4.3.gem +0 -0
  59. data/vendor/cache/docile-1.3.2.gem +0 -0
  60. data/vendor/cache/dotpath-0.1.0.gem +0 -0
  61. data/vendor/cache/faraday-1.1.0.gem +0 -0
  62. data/vendor/cache/faraday_middleware-1.0.0.gem +0 -0
  63. data/vendor/cache/hashdiff-1.0.1.gem +0 -0
  64. data/vendor/cache/i18n-1.8.5.gem +0 -0
  65. data/vendor/cache/jaro_winkler-1.5.4.gem +0 -0
  66. data/vendor/cache/json-2.3.1.gem +0 -0
  67. data/vendor/cache/minitest-5.13.0.gem +0 -0
  68. data/vendor/cache/mocha-1.11.2.gem +0 -0
  69. data/vendor/cache/multipart-post-2.1.1.gem +0 -0
  70. data/vendor/cache/net-http-persistent-3.1.0.gem +0 -0
  71. data/vendor/cache/parallel-1.19.1.gem +0 -0
  72. data/vendor/cache/parser-2.7.1.3.gem +0 -0
  73. data/vendor/cache/public_suffix-4.0.5.gem +0 -0
  74. data/vendor/cache/rack-2.2.2.gem +0 -0
  75. data/vendor/cache/rack-protection-2.0.8.1.gem +0 -0
  76. data/vendor/cache/rainbow-3.0.0.gem +0 -0
  77. data/vendor/cache/rake-10.5.0.gem +0 -0
  78. data/vendor/cache/redis-4.1.4.gem +0 -0
  79. data/vendor/cache/rubocop-0.78.0.gem +0 -0
  80. data/vendor/cache/rubocop-performance-1.5.2.gem +0 -0
  81. data/vendor/cache/ruby-progressbar-1.10.1.gem +0 -0
  82. data/vendor/cache/ruby2_keywords-0.0.2.gem +0 -0
  83. data/vendor/cache/safe_yaml-1.0.5.gem +0 -0
  84. data/vendor/cache/sidekiq-6.0.7.gem +0 -0
  85. data/vendor/cache/simplecov-0.18.5.gem +0 -0
  86. data/vendor/cache/simplecov-html-0.12.2.gem +0 -0
  87. data/vendor/cache/thread_safe-0.3.6.gem +0 -0
  88. data/vendor/cache/tzinfo-1.2.7.gem +0 -0
  89. data/vendor/cache/unicode-display_width-1.6.1.gem +0 -0
  90. data/vendor/cache/webmock-3.8.3.gem +0 -0
  91. data/vendor/cache/yard-0.9.24.gem +0 -0
  92. data/vendor/cache/zeitwerk-2.4.0.gem +0 -0
  93. metadata +375 -0
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << 'test'
8
+ t.libs << 'lib'
9
+ t.test_files = FileList['test/**/*_test.rb']
10
+ end
11
+
12
+ task default: :test
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'cased-ruby'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rubocop' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require 'pathname'
12
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path('bundle', __dir__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if /This file was generated by Bundler/.match?(File.read(bundle_binstub, 300))
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require 'rubygems'
27
+ require 'bundler/setup'
28
+
29
+ load Gem.bin_path('rubocop', 'rubocop')
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'cased/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'cased-ruby'
9
+ spec.version = Cased::VERSION
10
+ spec.authors = ['Garrett Bjerkhoel']
11
+ spec.email = ['garrett@cased.com']
12
+
13
+ spec.summary = 'Ruby library for Cased'
14
+ spec.description = 'Ruby library for Cased'
15
+ spec.homepage = 'https://github.com/cased/cased-ruby'
16
+ spec.license = 'MIT'
17
+
18
+ spec.metadata['homepage_uri'] = spec.homepage
19
+ spec.metadata['source_code_uri'] = 'https://github.com/cased/cased-ruby'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/cased/cased-ruby/releases'
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
+ end
27
+ spec.bindir = 'exe'
28
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ['lib']
30
+
31
+ spec.add_dependency 'activesupport', '~> 6'
32
+ spec.add_dependency 'dotpath', '0.1.0'
33
+ spec.add_dependency 'faraday', '~> 1.0'
34
+ spec.add_dependency 'faraday_middleware', '~> 1.0'
35
+ spec.add_dependency 'json', '~> 2'
36
+ spec.add_dependency 'net-http-persistent', '~> 3.0'
37
+ spec.add_development_dependency 'bundler', '2.1.4'
38
+ spec.add_development_dependency 'byebug', '11.0.1'
39
+ spec.add_development_dependency 'minitest', '5.13.0'
40
+ spec.add_development_dependency 'mocha', '1.11.2'
41
+ spec.add_development_dependency 'rack', '2.2.2'
42
+ spec.add_development_dependency 'rake', '10.5.0'
43
+ spec.add_development_dependency 'rubocop', '0.78.0'
44
+ spec.add_development_dependency 'rubocop-performance', '1.5.2'
45
+ spec.add_development_dependency 'sidekiq', '6.0.7'
46
+ spec.add_development_dependency 'webmock', '3.8.3'
47
+ spec.add_development_dependency 'yard', '0.9.24'
48
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cased'
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+ require 'json'
5
+ require 'dotpath'
6
+ require 'cased/version'
7
+ require 'cased/config'
8
+ require 'cased/context'
9
+ require 'cased/model'
10
+ require 'cased/error'
11
+ require 'cased/clients'
12
+ require 'cased/policy'
13
+ require 'cased/rack_middleware'
14
+ require 'cased/sensitive'
15
+ require 'cased/publishers'
16
+ require 'cased/test_helper'
17
+ require 'cased/instrumentation/log_subscriber'
18
+
19
+ # Integrations
20
+ begin
21
+ require 'cased/integrations/sidekiq'
22
+ rescue LoadError # rubocop:disable Lint/SuppressedException
23
+ # Sidekiq is not installed in host application
24
+ end
25
+
26
+ module Cased
27
+ def self.sensitive(label, handler)
28
+ Cased::Sensitive::Handler.register(label, handler)
29
+ end
30
+
31
+ #
32
+ # @example
33
+ # Cased.policies[:organization]
34
+ #
35
+ # @return [Hash{Symbol => Cased::Policy, nil}]
36
+ def self.policies
37
+ @policies ||= Hash.new do |hash, name|
38
+ key = name.to_sym
39
+ api_key = Cased.config.policy_key(key)
40
+ hash[key] = Policy.new(api_key: api_key)
41
+ end
42
+ end
43
+
44
+ # Helper method for accessing the applications default policy.
45
+ #
46
+ # @example
47
+ # Cased.configure do |config|
48
+ # config.policy_key = 'policy_test_1dQpY5JliYgHSkEntAbMVzuOROh'
49
+ # end
50
+ #
51
+ # policy = Cased.policy
52
+ # policy.events.each do |event|
53
+ # puts event['action'] # => user.login
54
+ # end
55
+ #
56
+ # @return [Cased::Policy, nil]
57
+ def self.policy
58
+ policies[:default]
59
+ end
60
+
61
+ # @return [Cased::Config]
62
+ def self.config
63
+ @config ||= Cased::Config.new
64
+ end
65
+
66
+ # @example
67
+ # Cased.configure do |config|
68
+ # config.policy_key = 'policy_test_1dQpY5JliYgHSkEntAbMVzuOROh'
69
+ # end
70
+ #
71
+ # @return [void]
72
+ def self.configure(&block)
73
+ block.call(config)
74
+ end
75
+
76
+ class << self
77
+ attr_writer :publishers
78
+ end
79
+
80
+ # The list of publishers that will receive the processed audit event when calling Cased.publish.
81
+ #
82
+ # The desired behavior for Cased.publish should not change based on the order
83
+ # of the publishers.
84
+ #
85
+ # @example Adding a publisher to the stack
86
+ # Cased.publishers << Cased::Publishers::KafkaPublisher.new
87
+ #
88
+ # @example Setting the list of publishers
89
+ # Cased.publishers = [
90
+ # Cased::Publishers::KafkaPublisher.new,
91
+ # ]
92
+ #
93
+ # @return [Array<Cased::Publishers::Base>]
94
+ def self.publishers
95
+ @publishers ||= [
96
+ Cased::Publishers::HTTPPublisher.new,
97
+ Cased::Publishers::ActiveSupportPublisher.new,
98
+ ]
99
+ end
100
+
101
+ def self.clients
102
+ @clients ||= Cased::Clients.new
103
+ end
104
+
105
+ # @param audit_event [Hash] the audit event.
106
+ #
107
+ # @example
108
+ # Cased.publish(
109
+ # action: "user.login",
110
+ # actor: "garrett@cased.com",
111
+ # actor_id: "user_1dQpY5JliYgHSkEntAbMVzuOROh",
112
+ # http_user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36",
113
+ # http_url: "https://app.cased.com/",
114
+ # http_method: "GET",
115
+ # language: "en-US"
116
+ # )
117
+ #
118
+ # @example With User object that includes Cased::Model
119
+ # Cased.publish(
120
+ # action: "user.login",
121
+ # actor: User.find(1),
122
+ # )
123
+ #
124
+ # @return [Array] of responses from Cased.publishers
125
+ # @return [false] if Cased has been silenced.
126
+ # @raise [ArgumentError] if a publisher does not implement the #publish method
127
+ def self.publish(audit_event)
128
+ return false if config.silence?
129
+
130
+ processed_audit_event = process(audit_event)
131
+
132
+ publishers.each do |publisher|
133
+ unless publisher.respond_to?(:publish)
134
+ raise ArgumentError, "#{publisher.class} must implement #{publisher.class}#publish"
135
+ end
136
+
137
+ publisher.publish(processed_audit_event.dup)
138
+ rescue StandardError => e
139
+ handle_exception(e)
140
+ end
141
+ end
142
+
143
+ # @return [Cased::Context]
144
+ def self.context
145
+ Context.current
146
+ end
147
+
148
+ # The main entry point to handling any exceptions encountered in the event creation lifecycle.
149
+ #
150
+ # @param exception [Exception] the exception to be raised
151
+ #
152
+ # @return [void]
153
+ # @raise [Exception] if Cased is configured to raise on errors
154
+ def self.handle_exception(exception)
155
+ raise exception if config.raise_on_errors?
156
+
157
+ if exception_handler.nil?
158
+ warn exception.message
159
+ return
160
+ end
161
+
162
+ exception_handler.call(exception)
163
+ end
164
+
165
+ # Applications can determine where they want exceptions generated by Cased to be sent.
166
+ #
167
+ # @return [Proc, nil]
168
+ def self.exception_handler
169
+ @exception_handler
170
+ end
171
+
172
+ # Sets the system user to be used for Cased events that do not contain an actor.
173
+ #
174
+ # @param handler [Proc] - The Proc or lambda that takes a single exception argument.
175
+ #
176
+ # @example
177
+ # Cased.exception_handler = Proc.new do |exception|
178
+ # Raven.capture_exception(exception)
179
+ # end
180
+ #
181
+ # @return [void]
182
+ # @raise [ArgumentError] if the provided handler does not respond to call or
183
+ # accept an argument.
184
+ def self.exception_handler=(handler)
185
+ if handler.nil?
186
+ @exception_handler = nil
187
+ return
188
+ elsif !handler.respond_to?(:call)
189
+ @exception_handler = nil
190
+ raise ArgumentError, "#{handler.class} does not respond to #call"
191
+ elsif handler.arity != 1
192
+ raise ArgumentError, 'handler does not accept any arguments'
193
+ end
194
+
195
+ @exception_handler = handler
196
+ end
197
+
198
+ # Configures a default context for console sessions.
199
+ #
200
+ # When a console session is started you don't have the context generated from
201
+ # a typical web request lifecycle where authentication will happen and an IP
202
+ # address is present. This uses the server's hostname as a standard location
203
+ # for migrations or data transitions.
204
+ #
205
+ # @param options [Hash]
206
+ #
207
+ # @return [void]
208
+ def self.console(options = {})
209
+ context.merge({
210
+ location: Socket.gethostname,
211
+ }.merge(options))
212
+ end
213
+
214
+ # Generates Cased compatible resource identifier.
215
+ #
216
+ # @param model [Object] an object that responds to #cased_id
217
+ #
218
+ # @return [String] the Cased::Model#cased_id
219
+ # @raise [Cased::Error::MissingIdentifier] when the provided model does not
220
+ # respond to #cased_id
221
+ def self.id(model)
222
+ raise Cased::Error::MissingIdentifier unless model.respond_to?(:cased_id)
223
+
224
+ model.cased_id
225
+ end
226
+
227
+ # Don't send any events to Cased that are created within the lifecycle of the block.
228
+ #
229
+ # @example
230
+ # Cased.silence do
231
+ # user.save
232
+ # end
233
+ def self.silence
234
+ original_silence = config.silence?
235
+ config.silence = true
236
+ yield
237
+ ensure
238
+ config.silence = original_silence
239
+ end
240
+
241
+ # Publish the Cased event so all subscribers can handle storing the Cased event.
242
+ #
243
+ # @param payload [Hash] payload to publish.
244
+ #
245
+ # @return [Hash] the processed audit event payload.
246
+ private_class_method def self.process(payload)
247
+ expanded_audit_event = Cased::Context::Expander.expand(payload)
248
+ event = expanded_audit_event.merge(
249
+ cased_id: SecureRandom.hex,
250
+ timestamp: Time.now.utc.iso8601(6),
251
+ )
252
+
253
+ safe_context = context.context.dup
254
+ audit_event = safe_context.merge(event)
255
+
256
+ Cased::Sensitive::Processor.process!(audit_event)
257
+
258
+ audit_event
259
+ end
260
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cased/http/client'
4
+
5
+ module Cased
6
+ class Clients
7
+ def self.create(api_key:, url: nil)
8
+ url ||= Cased.config.api_url
9
+
10
+ Cased::HTTP::Client.new(url: url, api_key: api_key)
11
+ end
12
+
13
+ def organization
14
+ @organization ||= self.class.create(api_key: ENV.fetch('CASED_ORGANIZATION_KEY'))
15
+ end
16
+
17
+ def publish
18
+ @publish ||= self.class.create(url: Cased.config.publish_url, api_key: Cased.config.publish_key)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cased/response'
4
+
5
+ module Cased
6
+ class CollectionResponse < Response
7
+ def results
8
+ return [] unless body
9
+
10
+ body['results']
11
+ end
12
+
13
+ def total_count
14
+ return unless body
15
+
16
+ body['total_count']
17
+ end
18
+
19
+ def total_pages
20
+ return unless body
21
+
22
+ body['total_pages']
23
+ end
24
+
25
+ def next_page_url?
26
+ next_page_url.present?
27
+ end
28
+
29
+ def next_page_url
30
+ links[:next]
31
+ end
32
+
33
+ def next_page
34
+ page_from(:next)
35
+ end
36
+
37
+ def next_page?
38
+ next_page.present?
39
+ end
40
+
41
+ def previous_page_url?
42
+ previous_page_url.present?
43
+ end
44
+
45
+ def previous_page_url
46
+ links[:prev]
47
+ end
48
+
49
+ def previous_page
50
+ page_from(:prev)
51
+ end
52
+
53
+ def previous_page?
54
+ previous_page.present?
55
+ end
56
+
57
+ def first_page_url?
58
+ first_page_url.present?
59
+ end
60
+
61
+ def first_page_url
62
+ links[:first]
63
+ end
64
+
65
+ def first_page
66
+ page_from(:first)
67
+ end
68
+
69
+ def first_page?
70
+ first_page.present?
71
+ end
72
+
73
+ def last_page_url?
74
+ last_page_url.present?
75
+ end
76
+
77
+ def last_page_url
78
+ links[:last]
79
+ end
80
+
81
+ def last_page
82
+ page_from(:last)
83
+ end
84
+
85
+ def last_page?
86
+ last_page.present?
87
+ end
88
+
89
+ private
90
+
91
+ def page_from(rel)
92
+ rel = links[rel.to_sym]
93
+ return unless rel
94
+
95
+ uri = Addressable::URI.parse(rel)
96
+ return unless uri
97
+
98
+ page = uri.query_values['page']
99
+ return unless page
100
+
101
+ page.to_i
102
+ end
103
+
104
+ def links
105
+ link_header = @response.headers['Link']
106
+ return {} unless link_header
107
+
108
+ links = link_header.split(', ').map do |link|
109
+ href, name = link.match(/<(.*?)>; rel="(\w+)"/).captures
110
+
111
+ [name.to_sym, href]
112
+ end
113
+
114
+ Hash[*links.flatten]
115
+ end
116
+ end
117
+ end