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,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cased
4
+ class Config
5
+ # The amount of time in seconds to allow the HTTP client to open a
6
+ # connection.
7
+ #
8
+ # @example
9
+ # CASED_HTTP_OPEN_TIMEOUT="5" rails server
10
+ #
11
+ # @example
12
+ # Cased.configure do |config|
13
+ # config.http_open_timeout = 5
14
+ # end
15
+ attr_reader :http_open_timeout
16
+
17
+ # The amount of time in seconds to allow the HTTP client to read a response
18
+ # from the server before timing out.
19
+ #
20
+ # @example
21
+ # CASED_HTTP_READ_TIMEOUT="10" rails server
22
+ #
23
+ # @example
24
+ # Cased.configure do |config|
25
+ # config.http_read_timeout = 10
26
+ # end
27
+ attr_reader :http_read_timeout
28
+
29
+ # The Cased HTTP API URL. Defaults to https://api.cased.com
30
+ #
31
+ # @example
32
+ # CASED_API_URL="https://api.cased.com" rails server
33
+ #
34
+ # @example
35
+ # Cased.configure do |config|
36
+ # config.api_url = "https://api.cased.com"
37
+ # end
38
+ attr_accessor :api_url
39
+
40
+ # The URL to publish audit events to. Defaults to https://publish.cased.com
41
+ #
42
+ # @example
43
+ # CASED_PUBLISH_URL="https://publish.cased.com" rails server
44
+ #
45
+ # @example
46
+ # Cased.configure do |config|
47
+ # config.publish_url = "https://publish.cased.com"
48
+ # end
49
+ attr_accessor :publish_url
50
+
51
+ # Publish keys are used to publish to an audit trail.
52
+ #
53
+ # A publish key is associated with a single audit trail and is required if
54
+ # you intend to publish events to Cased in your application.
55
+ #
56
+ # @example
57
+ # CASED_PUBLISH_KEY="publish_test_5dSfh6xZAuL2Esn3Z2XSM6ReMS21" rails server
58
+ #
59
+ # @example
60
+ # Cased.configure do |config|
61
+ # config.publish_key = "publish_test_5dSfh6xZAuL2Esn3Z2XSM6ReMS21"
62
+ # end
63
+ attr_accessor :publish_key
64
+
65
+ #
66
+ # @example
67
+ # CASED_ORGANIZATION_POLICY_KEY="policy_live_1dQpY2mRu3pTCcNGB7a6ewx4WFp" \
68
+ # CASED_SECURITY_POLICY_KEY="policy_live_1dQpY9Erf3cKUrooz0BQKmCD4Oc" \
69
+ # CASED_USER_POLICY_KEY="policy_live_1dSHQRbtjj17LanuAgpHd3QKtOO" \
70
+ # rails server
71
+ #
72
+ # @example
73
+ # Cased.configure do |config|
74
+ # config.policy_keys = {
75
+ # organization: "policy_live_1dQpY2mRu3pTCcNGB7a6ewx4WFp",
76
+ # security: "policy_live_1dQpY9Erf3cKUrooz0BQKmCD4Oc",
77
+ # user: "policy_live_1dSHQRbtjj17LanuAgpHd3QKtOO",
78
+ # }
79
+ # end
80
+ attr_reader :policy_keys
81
+
82
+ # Policy keys are used to query for events from audit trails.
83
+ #
84
+ # @example
85
+ # CASED_POLICY_KEY="policy_live_1dQpY1fUFHzENWhTVMvjCilAKp9" rails server
86
+ #
87
+ # @example
88
+ # Cased.configure do |config|
89
+ # config.raise_on_errors = !Rails.env.production?
90
+ # end
91
+ attr_reader :raise_on_errors
92
+
93
+ # Configure whether or not Cased will attempt to publish any events.
94
+ #
95
+ # If the CASED_SILENCE environment variable is not nil Cased will
96
+ # not publish events.
97
+ #
98
+ # @example
99
+ # CASED_SILENCE="1" rails server
100
+ #
101
+ # @example
102
+ # Cased.configure do |config|
103
+ # config.silence = Rails.env.test?
104
+ # end
105
+ #
106
+ # @example
107
+ # Cased.silence do
108
+ # User.create!
109
+ # end
110
+ attr_writer :silence
111
+
112
+ def initialize
113
+ @http_read_timeout = ENV.fetch('CASED_HTTP_READ_TIMEOUT', 10).to_i
114
+ @http_open_timeout = ENV.fetch('CASED_HTTP_OPEN_TIMEOUT', 5).to_i
115
+ @raise_on_errors = !ENV['CASED_RAISE_ON_ERRORS'].nil?
116
+ @api_url = ENV.fetch('CASED_API_URL', 'https://api.cased.com')
117
+ @publish_url = ENV.fetch('CASED_PUBLISH_URL', 'https://publish.cased.com')
118
+ @publish_key = ENV['CASED_PUBLISH_KEY']
119
+ @silence = !ENV['CASED_SILENCE'].nil?
120
+ @policy_keys = Hash.new do |hash, key|
121
+ normalized_key = key.to_sym
122
+ if normalized_key == :default
123
+ hash[normalized_key] = ENV['CASED_POLICY_KEY']
124
+ else
125
+ env_policy_name = key.to_s.tr(' ', '_').tr('-', '_').upcase
126
+ api_key = ENV["CASED_#{env_policy_name}_POLICY_KEY"]
127
+
128
+ hash[normalized_key] = api_key if api_key
129
+ end
130
+ end
131
+ end
132
+
133
+ # Policy keys are used to query for events from audit trails.
134
+ #
135
+ # @param [Symbol] policy name
136
+ #
137
+ # @example
138
+ # CASED_POLICY_KEY="policy_live_1dQpY1fUFHzENWhTVMvjCilAKp9" rails server
139
+ #
140
+ # @example
141
+ # Cased.configure do |config|
142
+ # config.policy_key = "policy_live_1dQpY1fUFHzENWhTVMvjCilAKp9"
143
+ # end
144
+ def policy_key(policy = :default)
145
+ policy_keys[policy.to_sym]
146
+ end
147
+
148
+ def policy_key=(api_key)
149
+ policy_keys[:default] = api_key
150
+ end
151
+
152
+ def raise_on_errors=(new_val)
153
+ @raise_on_errors = !!new_val
154
+ end
155
+
156
+ def http_read_timeout=(new_val)
157
+ @http_read_timeout = new_val.to_i
158
+ end
159
+
160
+ def http_open_timeout=(new_val)
161
+ @http_open_timeout = new_val.to_i
162
+ end
163
+
164
+ def raise_on_errors?
165
+ @raise_on_errors
166
+ end
167
+
168
+ def silence?
169
+ @silence || !ENV['CASED_SILENCE'].nil?
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cased/context/expander'
4
+ require 'active_support/core_ext/hash/deep_merge'
5
+
6
+ module Cased
7
+ class Context
8
+ def self.current
9
+ Thread.current[:cased_context] ||= new
10
+ end
11
+
12
+ def self.current=(context)
13
+ Thread.current[:cased_context] = new(context)
14
+ end
15
+
16
+ def self.clear!
17
+ Thread.current[:cased_context] = nil
18
+ end
19
+
20
+ attr_reader :context
21
+
22
+ def initialize(context = {})
23
+ @context = Cased::Context::Expander.expand(context || {})
24
+ end
25
+
26
+ def clear
27
+ @context = {}
28
+ end
29
+
30
+ def merge(new_context = {})
31
+ if block_given?
32
+ old_context = @context.dup
33
+ @context.deep_merge!(Cased::Context::Expander.expand(new_context))
34
+ yield
35
+ else
36
+ @context.deep_merge!(Cased::Context::Expander.expand(new_context))
37
+ end
38
+ ensure
39
+ @context = old_context if block_given?
40
+ end
41
+
42
+ def [](key)
43
+ @context[key]
44
+ end
45
+
46
+ def []=(key, value)
47
+ merge(key => value)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cased
4
+ class Context
5
+ class Expander
6
+ def self.expand(payload)
7
+ return {} if payload.nil?
8
+ return payload unless payload.respond_to?(:each)
9
+
10
+ cased_payload = payload.dup
11
+ payload.each do |key, value|
12
+ if value.respond_to?(:cased_context)
13
+ cased_payload.delete(key)
14
+ cased_payload.update(value.cased_context(category: key))
15
+ elsif value.is_a?(Hash)
16
+ cased_payload[key] = expand(value)
17
+ elsif value.is_a?(Array)
18
+ values = value.collect do |val|
19
+ if val.respond_to?(:cased_context)
20
+ val.cased_context
21
+ else
22
+ val
23
+ end
24
+ end
25
+ cased_payload.update(key => values)
26
+ end
27
+ end
28
+
29
+ cased_payload
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cased
4
+ class Error < StandardError
5
+ SystemActorMissing = Class.new(self)
6
+ MissingIdentifier = Class.new(self)
7
+ end
8
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cased/http/error'
4
+ require 'faraday'
5
+ require 'faraday_middleware'
6
+
7
+ module Cased
8
+ module HTTP
9
+ class Client
10
+ def initialize(url:, api_key:)
11
+ @client = Faraday.new(url: url) do |conn|
12
+ conn.headers[:user_agent] = "cased-ruby/v#{Cased::VERSION}"
13
+ conn.headers[:content_type] = 'application/json'
14
+ conn.headers[:accept] = 'application/json'
15
+ conn.headers[:authorization] = "Bearer #{api_key}"
16
+
17
+ conn.request :json
18
+ conn.response :json, content_type: /\bjson$/
19
+
20
+ conn.options.timeout = Cased.config.http_read_timeout
21
+ conn.options.open_timeout = Cased.config.http_open_timeout
22
+
23
+ conn.adapter :net_http_persistent
24
+ end
25
+ end
26
+
27
+ # Requests with bodies
28
+
29
+ def put(url = nil, body = nil, headers = nil, &block)
30
+ request(:put, url, body, headers, &block)
31
+ end
32
+
33
+ def post(url = nil, body = nil, headers = nil, &block)
34
+ request(:post, url, body, headers, &block)
35
+ end
36
+
37
+ def patch(url = nil, body = nil, headers = nil, &block)
38
+ request(:patch, url, body, headers, &block)
39
+ end
40
+
41
+ # Requests without bodies
42
+
43
+ def get(url = nil, params = nil, headers = nil)
44
+ request(:get, url, nil, headers) do |req|
45
+ req.params.update(params) if params
46
+ yield req if block_given?
47
+ end
48
+ end
49
+
50
+ def head(url = nil, params = nil, headers = nil)
51
+ request(:head, url, nil, headers) do |req|
52
+ req.params.update(params) if params
53
+ yield req if block_given?
54
+ end
55
+ end
56
+
57
+ def delete(url = nil, params = nil, headers = nil)
58
+ request(:delete, url, nil, headers) do |req|
59
+ req.params.update(params) if params
60
+ yield req if block_given?
61
+ end
62
+ end
63
+
64
+ def trace(url = nil, params = nil, headers = nil)
65
+ request(:trace, url, nil, headers) do |req|
66
+ req.params.update(params) if params
67
+ yield req if block_given?
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def request(method, url = nil, params_or_body = nil, headers = nil, &block)
74
+ response = @client.send(method, url, params_or_body, headers, &block)
75
+ return response if response.success?
76
+
77
+ # TODO: Look to return a Cased::Response here with error wrapped.
78
+ klass = Cased::HTTP::Error.class_from_response(response)
79
+ raise klass.from_response(response)
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cased/error'
4
+
5
+ module Cased
6
+ module HTTP
7
+ class Error < Cased::Error
8
+ attr_reader :code
9
+
10
+ def initialize(message = '', code = nil)
11
+ super(message)
12
+ @code = code
13
+ end
14
+
15
+ def self.from_response(response)
16
+ new(response.body, response.status)
17
+ end
18
+
19
+ def self.class_from_response(response)
20
+ klass = ERRORS[response.status]
21
+
22
+ if klass
23
+ klass
24
+ elsif (300...400).cover?(response.status)
25
+ RedirectionError
26
+ elsif (400...500).cover?(response.status)
27
+ ClientError
28
+ elsif (500...600).cover?(response.status)
29
+ ServerError
30
+ else
31
+ self
32
+ end
33
+ end
34
+
35
+ # 3xx
36
+ RedirectionError = Class.new(self)
37
+
38
+ # 4xx
39
+ ClientError = Class.new(self)
40
+
41
+ # 400
42
+ BadRequest = Class.new(ClientError)
43
+
44
+ # 401
45
+ Unauthorized = Class.new(ClientError)
46
+
47
+ # 403
48
+ Forbidden = Class.new(ClientError)
49
+
50
+ # 404
51
+ NotFound = Class.new(ClientError)
52
+
53
+ # 406
54
+ NotAcceptable = Class.new(ClientError)
55
+
56
+ # 408
57
+ RequestTimeout = Class.new(ClientError)
58
+
59
+ # 409
60
+ Conflict = Class.new(ClientError)
61
+
62
+ # 422
63
+ UnprocessableEntity = Class.new(ClientError)
64
+
65
+ # 429
66
+ TooManyRequests = Class.new(ClientError)
67
+
68
+ # 5xx
69
+ ServerError = Class.new(self)
70
+
71
+ # 500
72
+ InternalServerError = Class.new(ServerError)
73
+
74
+ # 502
75
+ BadGateway = Class.new(ServerError)
76
+
77
+ # 503
78
+ ServiceUnavailable = Class.new(ServerError)
79
+
80
+ # 504
81
+ GatewayTimeout = Class.new(ServerError)
82
+
83
+ ERRORS = {
84
+ 400 => BadRequest,
85
+ 401 => Unauthorized,
86
+ 403 => Forbidden,
87
+ 404 => NotFound,
88
+ 406 => NotAcceptable,
89
+ 408 => RequestTimeout,
90
+ 422 => UnprocessableEntity,
91
+ 429 => TooManyRequests,
92
+ 500 => InternalServerError,
93
+ 502 => BadGateway,
94
+ 503 => ServiceUnavailable,
95
+ 504 => GatewayTimeout,
96
+ }.freeze
97
+ end
98
+ end
99
+ end