cased-ruby 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
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