cased-ruby 0.3.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/rubocop.yml +21 -0
- data/.github/workflows/ruby.yml +46 -0
- data/.gitignore +10 -0
- data/.rubocop.yml +88 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +107 -0
- data/LICENSE +21 -0
- data/README.md +661 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/rubocop +29 -0
- data/cased-ruby.gemspec +48 -0
- data/lib/cased-ruby.rb +3 -0
- data/lib/cased.rb +260 -0
- data/lib/cased/clients.rb +21 -0
- data/lib/cased/collection_response.rb +117 -0
- data/lib/cased/config.rb +172 -0
- data/lib/cased/context.rb +50 -0
- data/lib/cased/context/expander.rb +33 -0
- data/lib/cased/error.rb +8 -0
- data/lib/cased/http/client.rb +83 -0
- data/lib/cased/http/error.rb +99 -0
- data/lib/cased/instrumentation/controller.rb +34 -0
- data/lib/cased/instrumentation/log_subscriber.rb +31 -0
- data/lib/cased/integrations/sidekiq.rb +17 -0
- data/lib/cased/integrations/sidekiq/client_middleware.rb +14 -0
- data/lib/cased/integrations/sidekiq/server_middleware.rb +20 -0
- data/lib/cased/model.rb +98 -0
- data/lib/cased/policy.rb +24 -0
- data/lib/cased/publishers.rb +6 -0
- data/lib/cased/publishers/active_support_publisher.rb +16 -0
- data/lib/cased/publishers/base.rb +17 -0
- data/lib/cased/publishers/error.rb +11 -0
- data/lib/cased/publishers/http_publisher.rb +15 -0
- data/lib/cased/publishers/null_publisher.rb +11 -0
- data/lib/cased/publishers/test_publisher.rb +19 -0
- data/lib/cased/query.rb +87 -0
- data/lib/cased/rack_middleware.rb +15 -0
- data/lib/cased/response.rb +37 -0
- data/lib/cased/sensitive.rb +4 -0
- data/lib/cased/sensitive/handler.rb +54 -0
- data/lib/cased/sensitive/processor.rb +78 -0
- data/lib/cased/sensitive/range.rb +54 -0
- data/lib/cased/sensitive/result.rb +8 -0
- data/lib/cased/sensitive/string.rb +43 -0
- data/lib/cased/test_helper.rb +188 -0
- data/lib/cased/version.rb +5 -0
- data/vendor/cache/activesupport-6.0.3.4.gem +0 -0
- data/vendor/cache/addressable-2.7.0.gem +0 -0
- data/vendor/cache/ast-2.4.0.gem +0 -0
- data/vendor/cache/byebug-11.0.1.gem +0 -0
- data/vendor/cache/concurrent-ruby-1.1.7.gem +0 -0
- data/vendor/cache/connection_pool-2.2.2.gem +0 -0
- data/vendor/cache/crack-0.4.3.gem +0 -0
- data/vendor/cache/docile-1.3.2.gem +0 -0
- data/vendor/cache/dotpath-0.1.0.gem +0 -0
- data/vendor/cache/faraday-1.1.0.gem +0 -0
- data/vendor/cache/faraday_middleware-1.0.0.gem +0 -0
- data/vendor/cache/hashdiff-1.0.1.gem +0 -0
- data/vendor/cache/i18n-1.8.5.gem +0 -0
- data/vendor/cache/jaro_winkler-1.5.4.gem +0 -0
- data/vendor/cache/json-2.3.1.gem +0 -0
- data/vendor/cache/minitest-5.13.0.gem +0 -0
- data/vendor/cache/mocha-1.11.2.gem +0 -0
- data/vendor/cache/multipart-post-2.1.1.gem +0 -0
- data/vendor/cache/net-http-persistent-3.1.0.gem +0 -0
- data/vendor/cache/parallel-1.19.1.gem +0 -0
- data/vendor/cache/parser-2.7.1.3.gem +0 -0
- data/vendor/cache/public_suffix-4.0.5.gem +0 -0
- data/vendor/cache/rack-2.2.2.gem +0 -0
- data/vendor/cache/rack-protection-2.0.8.1.gem +0 -0
- data/vendor/cache/rainbow-3.0.0.gem +0 -0
- data/vendor/cache/rake-10.5.0.gem +0 -0
- data/vendor/cache/redis-4.1.4.gem +0 -0
- data/vendor/cache/rubocop-0.78.0.gem +0 -0
- data/vendor/cache/rubocop-performance-1.5.2.gem +0 -0
- data/vendor/cache/ruby-progressbar-1.10.1.gem +0 -0
- data/vendor/cache/ruby2_keywords-0.0.2.gem +0 -0
- data/vendor/cache/safe_yaml-1.0.5.gem +0 -0
- data/vendor/cache/sidekiq-6.0.7.gem +0 -0
- data/vendor/cache/simplecov-0.18.5.gem +0 -0
- data/vendor/cache/simplecov-html-0.12.2.gem +0 -0
- data/vendor/cache/thread_safe-0.3.6.gem +0 -0
- data/vendor/cache/tzinfo-1.2.7.gem +0 -0
- data/vendor/cache/unicode-display_width-1.6.1.gem +0 -0
- data/vendor/cache/webmock-3.8.3.gem +0 -0
- data/vendor/cache/yard-0.9.24.gem +0 -0
- data/vendor/cache/zeitwerk-2.4.0.gem +0 -0
- metadata +375 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './log_subscriber'
|
4
|
+
|
5
|
+
module Cased
|
6
|
+
module Instrumentation
|
7
|
+
module Controller
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def log_process_action(payload)
|
12
|
+
messages = super
|
13
|
+
count = payload[:cased_events]
|
14
|
+
if count
|
15
|
+
messages << format('Cased: %<count>d %<suffix>s', count: count, suffix: 'event'.pluralize(count))
|
16
|
+
end
|
17
|
+
messages
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
def process_action(action, *args)
|
24
|
+
Cased::Instrumentation::LogSubscriber.reset_events
|
25
|
+
super
|
26
|
+
end
|
27
|
+
|
28
|
+
def append_info_to_payload(payload)
|
29
|
+
super
|
30
|
+
payload[:cased_events] = Cased::Instrumentation::LogSubscriber.events
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/log_subscriber'
|
4
|
+
|
5
|
+
module Cased
|
6
|
+
module Instrumentation
|
7
|
+
class LogSubscriber < ActiveSupport::LogSubscriber
|
8
|
+
def self.events=(value)
|
9
|
+
Thread.current['cased_events'] = value
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.events
|
13
|
+
Thread.current['cased_events'] ||= 0
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.reset_events
|
17
|
+
self.events = 0
|
18
|
+
end
|
19
|
+
|
20
|
+
def audit_event(event)
|
21
|
+
self.class.events += 1
|
22
|
+
|
23
|
+
event = JSON.generate(event.payload[:event])
|
24
|
+
name = color('Cased', CYAN, true)
|
25
|
+
debug " #{name} #{event}"
|
26
|
+
end
|
27
|
+
|
28
|
+
attach_to :cased
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sidekiq'
|
4
|
+
require 'cased/integrations/sidekiq/client_middleware'
|
5
|
+
require 'cased/integrations/sidekiq/server_middleware'
|
6
|
+
|
7
|
+
Sidekiq.configure_client do |config|
|
8
|
+
config.client_middleware do |chain|
|
9
|
+
chain.add Cased::Integrations::Sidekiq::ClientMiddleware
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
Sidekiq.configure_server do |config|
|
14
|
+
config.server_middleware do |chain|
|
15
|
+
chain.add Cased::Integrations::Sidekiq::ServerMiddleware
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cased
|
4
|
+
module Integrations
|
5
|
+
module Sidekiq
|
6
|
+
class ServerMiddleware
|
7
|
+
def call(_worker, job, _queue)
|
8
|
+
context = (job['cased_context'] || {})
|
9
|
+
context['job_class'] = job['class']
|
10
|
+
|
11
|
+
Cased::Context.current = context
|
12
|
+
|
13
|
+
yield
|
14
|
+
ensure
|
15
|
+
Cased::Context.clear!
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/cased/model.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/concern'
|
4
|
+
require 'active_support/inflector'
|
5
|
+
require 'active_support/core_ext/hash/deep_merge'
|
6
|
+
require 'cased/context'
|
7
|
+
|
8
|
+
module Cased
|
9
|
+
module Model
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
# Establishes a default `cased_category` that matches nicely to
|
14
|
+
# the class name. The default instance level `cased_category` uses this.
|
15
|
+
#
|
16
|
+
# @return [Symbol]
|
17
|
+
def cased_category
|
18
|
+
@cased_category ||= begin
|
19
|
+
category = ActiveSupport::Inflector.underscore(name)
|
20
|
+
category.to_sym
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Instruments events for the model. These events are sent directly to Cased.
|
26
|
+
#
|
27
|
+
# @param action [String, Symbol] suffix of the action.
|
28
|
+
# @param category [String, Symbol] action category name.
|
29
|
+
# @param payload [Hash] additional payload information about the event.
|
30
|
+
#
|
31
|
+
# @return [Array] of responses from Cased.publishers
|
32
|
+
def cased(action, category: cased_category, payload: {})
|
33
|
+
body = cased_payload.deep_merge(payload)
|
34
|
+
|
35
|
+
Cased.publish(body.merge(action: "#{category}.#{action}"))
|
36
|
+
end
|
37
|
+
|
38
|
+
# Defines the event key prefix for all model events.
|
39
|
+
#
|
40
|
+
# Defaults to :my_model if the class this thing is included in is MyModel.
|
41
|
+
# Feel free to override to provide a more semantic, meaningful name
|
42
|
+
# if you so desire.
|
43
|
+
#
|
44
|
+
# @return [String]
|
45
|
+
def cased_category
|
46
|
+
self.class.cased_category
|
47
|
+
end
|
48
|
+
|
49
|
+
# Defines the default payload for every event.
|
50
|
+
#
|
51
|
+
# @return [Hash]
|
52
|
+
def cased_payload
|
53
|
+
{
|
54
|
+
cased_category => self,
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
# Defines the Cased identifier for the current instance.
|
59
|
+
#
|
60
|
+
# @example
|
61
|
+
# user.cased_id # => User;1
|
62
|
+
#
|
63
|
+
# @example
|
64
|
+
# client.cased_id # => Client;1
|
65
|
+
#
|
66
|
+
# @raise [Cased::Error::MissingIdentifier] description
|
67
|
+
# @return [String]
|
68
|
+
def cased_id; end
|
69
|
+
|
70
|
+
# Internal: The String representation within the Cased object.
|
71
|
+
#
|
72
|
+
# @return [String] if cased_category key is present in cased_context.
|
73
|
+
def cased_human
|
74
|
+
cased_context[cased_category]
|
75
|
+
end
|
76
|
+
|
77
|
+
# Internal: Defines the payload to describe the current subject.
|
78
|
+
# Can be overridden in model to change fields returned.
|
79
|
+
#
|
80
|
+
# @param [String, Symbol] category prefix that can be set to override Hash prefix.
|
81
|
+
#
|
82
|
+
# @example
|
83
|
+
# model.cased_context # => { user_id: "User;1", user: "flynn" }
|
84
|
+
# model.cased_context(category: :actor) # => { actor_id: "User;1", actor: "flynn" }
|
85
|
+
#
|
86
|
+
# @return [Hash]
|
87
|
+
def cased_context(category: cased_category)
|
88
|
+
context = {}
|
89
|
+
context["#{category}_id".to_sym] = cased_id if cased_id
|
90
|
+
|
91
|
+
if method(:to_s).owner == self.class
|
92
|
+
context[category] = Cased::Sensitive::String.new(to_s, label: self.class.name)
|
93
|
+
end
|
94
|
+
|
95
|
+
context
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
data/lib/cased/policy.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cased/query'
|
4
|
+
|
5
|
+
module Cased
|
6
|
+
class Policy
|
7
|
+
attr_reader :api_key
|
8
|
+
attr_reader :client
|
9
|
+
|
10
|
+
def initialize(api_key:)
|
11
|
+
@api_key = api_key
|
12
|
+
@client = Cased::Clients.create(api_key: @api_key)
|
13
|
+
end
|
14
|
+
|
15
|
+
def events(phrase: nil, variables: {})
|
16
|
+
Query.new(@client, phrase: phrase, variables: variables)
|
17
|
+
end
|
18
|
+
|
19
|
+
def event(id)
|
20
|
+
response = @client.get("events/#{id}")
|
21
|
+
response.body
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# ActiveSupport::Notifications will fail if concurrent isn't loaded
|
4
|
+
require 'concurrent'
|
5
|
+
require 'active_support/notifications'
|
6
|
+
require 'cased/publishers/base'
|
7
|
+
|
8
|
+
module Cased
|
9
|
+
module Publishers
|
10
|
+
class ActiveSupportPublisher < Base
|
11
|
+
def publish(event)
|
12
|
+
::ActiveSupport::Notifications.instrument('event.cased', event: event)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cased/publishers/error'
|
4
|
+
|
5
|
+
module Cased
|
6
|
+
module Publishers
|
7
|
+
class Base
|
8
|
+
def publish(_audit_event)
|
9
|
+
raise "#{self.class} must implement the #{self.class}#publish method"
|
10
|
+
end
|
11
|
+
|
12
|
+
def ==(other)
|
13
|
+
self.class == other.class
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cased/publishers/base'
|
4
|
+
|
5
|
+
module Cased
|
6
|
+
module Publishers
|
7
|
+
class HTTPPublisher < Base
|
8
|
+
def publish(audit_event)
|
9
|
+
Cased.clients.publish.post do |req|
|
10
|
+
req.body = JSON.generate(audit_event)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cased/publishers/base'
|
4
|
+
|
5
|
+
module Cased
|
6
|
+
module Publishers
|
7
|
+
class TestPublisher < Base
|
8
|
+
attr_reader :events
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@events = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def publish(event)
|
15
|
+
@events << event
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/cased/query.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cased/collection_response'
|
4
|
+
|
5
|
+
module Cased
|
6
|
+
class Query
|
7
|
+
# @param client [Cased::HTTP::Client] the HTTP client authorized to query an
|
8
|
+
# audit trail policy
|
9
|
+
# @param phrase [String, nil] the phrase to search for audit trail events
|
10
|
+
# @param variables [Hash] the query variables
|
11
|
+
def initialize(client, phrase: nil, variables: {})
|
12
|
+
raise ArgumentError, 'variables must be a Hash' unless variables.is_a?(Hash)
|
13
|
+
|
14
|
+
@client = client
|
15
|
+
@phrase = phrase
|
16
|
+
@page = 1
|
17
|
+
@limit = 25
|
18
|
+
@variables = variables
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param new_phrase [String] The audit trail policy search phrase.
|
22
|
+
# @return [Cased::Query]
|
23
|
+
def phrase(new_phrase)
|
24
|
+
@phrase = new_phrase
|
25
|
+
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
# @param new_limit [Integer] The number of audit trail events to return.
|
30
|
+
# @return [Cased::Query]
|
31
|
+
def limit(new_limit)
|
32
|
+
@limit = [[new_limit, 100].min, 1].max
|
33
|
+
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
# @param new_page [Integer] The page of audit trail events to request.
|
38
|
+
# @return [Cased::Query]
|
39
|
+
def page(new_page)
|
40
|
+
@page = [1, new_page.to_i].max
|
41
|
+
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
# If any of these methods are called we need the request to fulfil the response.
|
46
|
+
delegate :results, \
|
47
|
+
:total_count, \
|
48
|
+
:total_pages, \
|
49
|
+
:next_page_url?, \
|
50
|
+
:next_page_url, \
|
51
|
+
:next_page, \
|
52
|
+
:next_page?, \
|
53
|
+
:previous_page_url?, \
|
54
|
+
:previous_page_url, \
|
55
|
+
:previous_page, \
|
56
|
+
:previous_page?, \
|
57
|
+
:first_page_url?, \
|
58
|
+
:first_page_url, \
|
59
|
+
:first_page, \
|
60
|
+
:first_page?, \
|
61
|
+
:last_page_url?, \
|
62
|
+
:last_page_url, \
|
63
|
+
:last_page, \
|
64
|
+
:last_page?, \
|
65
|
+
:error, \
|
66
|
+
:error?, \
|
67
|
+
:success?, \
|
68
|
+
to: :response
|
69
|
+
|
70
|
+
def response
|
71
|
+
return @response if defined?(@response)
|
72
|
+
|
73
|
+
@response = begin
|
74
|
+
resp = @client.get('events') do |req|
|
75
|
+
req.params['phrase'] = @phrase unless @phrase.nil?
|
76
|
+
req.params['per_page'] = @limit unless @limit.nil?
|
77
|
+
req.params['page'] = @page unless @page.nil?
|
78
|
+
req.params['variables'] = @variables unless @variables.nil?
|
79
|
+
end
|
80
|
+
|
81
|
+
CollectionResponse.new(response: resp)
|
82
|
+
rescue Cased::HTTP::Error, Faraday::Error => e
|
83
|
+
CollectionResponse.new(exception: e)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|