logux_rails 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.pryrc +8 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +33 -0
  6. data/.rubocop_todo.yml +17 -0
  7. data/.travis.yml +11 -0
  8. data/Appraisals +13 -0
  9. data/Gemfile +8 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +68 -0
  12. data/Rakefile +10 -0
  13. data/app/controllers/logux_controller.rb +41 -0
  14. data/app/helpers/logux_helper.rb +4 -0
  15. data/app/logux/actions.rb +3 -0
  16. data/app/logux/policies.rb +3 -0
  17. data/bin/console +15 -0
  18. data/bin/setup +8 -0
  19. data/config/routes.rb +5 -0
  20. data/docker-compose.yml +29 -0
  21. data/lib/generators/logux/model/USAGE +11 -0
  22. data/lib/generators/logux/model/model_generator.rb +28 -0
  23. data/lib/generators/logux/model/templates/migration.rb.erb +14 -0
  24. data/lib/logux.rb +107 -0
  25. data/lib/logux/action_caller.rb +42 -0
  26. data/lib/logux/action_controller.rb +6 -0
  27. data/lib/logux/actions.rb +29 -0
  28. data/lib/logux/add.rb +37 -0
  29. data/lib/logux/auth.rb +6 -0
  30. data/lib/logux/base_controller.rb +37 -0
  31. data/lib/logux/channel_controller.rb +24 -0
  32. data/lib/logux/class_finder.rb +61 -0
  33. data/lib/logux/client.rb +21 -0
  34. data/lib/logux/engine.rb +6 -0
  35. data/lib/logux/error_renderer.rb +40 -0
  36. data/lib/logux/meta.rb +36 -0
  37. data/lib/logux/model.rb +39 -0
  38. data/lib/logux/model/dsl.rb +15 -0
  39. data/lib/logux/model/proxy.rb +24 -0
  40. data/lib/logux/model/updater.rb +39 -0
  41. data/lib/logux/model/updates_deprecator.rb +54 -0
  42. data/lib/logux/node.rb +37 -0
  43. data/lib/logux/policy.rb +14 -0
  44. data/lib/logux/policy_caller.rb +34 -0
  45. data/lib/logux/process.rb +9 -0
  46. data/lib/logux/process/action.rb +60 -0
  47. data/lib/logux/process/auth.rb +27 -0
  48. data/lib/logux/process/batch.rb +59 -0
  49. data/lib/logux/response.rb +18 -0
  50. data/lib/logux/stream.rb +25 -0
  51. data/lib/logux/test.rb +35 -0
  52. data/lib/logux/test/helpers.rb +75 -0
  53. data/lib/logux/test/matchers.rb +10 -0
  54. data/lib/logux/test/matchers/base.rb +25 -0
  55. data/lib/logux/test/matchers/response_chunks.rb +48 -0
  56. data/lib/logux/test/matchers/send_to_logux.rb +51 -0
  57. data/lib/logux/test/store.rb +21 -0
  58. data/lib/logux/version.rb +5 -0
  59. data/lib/logux_rails.rb +3 -0
  60. data/lib/tasks/logux_tasks.rake +46 -0
  61. data/logux_rails.gemspec +46 -0
  62. metadata +398 -0
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ module Model
5
+ class UpdatesDeprecator
6
+ EVENT = 'logux.insecure_update'
7
+
8
+ class << self
9
+ def watch(args = {}, &block)
10
+ new(args).watch(&block)
11
+ end
12
+ end
13
+
14
+ def initialize(level: :warn)
15
+ @level = level
16
+ end
17
+
18
+ def watch(&block)
19
+ callback = lambda(&method(:handle_insecure_update))
20
+ ActiveSupport::Notifications.subscribed(callback, EVENT, &block)
21
+ end
22
+
23
+ private
24
+
25
+ # rubocop:disable Naming/UncommunicativeMethodParamName
26
+ def handle_insecure_update(_, _, _, _, args)
27
+ model = args[:model]
28
+
29
+ attributes = model.changed.map(&:to_sym) - [:logux_fields_updated_at]
30
+ insecure_attributes =
31
+ attributes & model.class.logux_crdt_mapped_attributes
32
+ return if insecure_attributes.empty?
33
+
34
+ notify_about_insecure_update(insecure_attributes)
35
+ end
36
+ # rubocop:enable Naming/UncommunicativeMethodParamName
37
+
38
+ def notify_about_insecure_update(insecure_attributes)
39
+ pluralized_attributes = 'attribute'.pluralize(insecure_attributes.count)
40
+
41
+ message = <<~TEXT
42
+ Logux tracked #{pluralized_attributes} (#{insecure_attributes.join(', ')}) should be updated using model.logux.update(...)
43
+ TEXT
44
+
45
+ case @level
46
+ when :warn
47
+ ActiveSupport::Deprecation.warn(message)
48
+ when :error
49
+ raise InsecureUpdateError, message
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ class Node
5
+ include Singleton
6
+
7
+ attr_accessor :last_time, :sequence
8
+ attr_writer :node_id
9
+
10
+ def generate_action_id
11
+ mutex.synchronize do
12
+ if last_time && now_time <= last_time
13
+ @sequence += 1
14
+ else
15
+ @sequence = 0
16
+ @last_time = now_time
17
+ end
18
+
19
+ "#{last_time} #{node_id} #{sequence}"
20
+ end
21
+ end
22
+
23
+ def node_id
24
+ @node_id ||= "server:#{Nanoid.generate(size: 8)}"
25
+ end
26
+
27
+ private
28
+
29
+ def now_time
30
+ Time.now.to_datetime.strftime('%Q')
31
+ end
32
+
33
+ def mutex
34
+ @mutex ||= Mutex.new
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ class Policy
5
+ class UnauthorizedError < StandardError; end
6
+
7
+ attr_reader :action, :meta
8
+
9
+ def initialize(action:, meta:)
10
+ @action = action
11
+ @meta = meta
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ class PolicyCaller
5
+ attr_reader :action, :meta
6
+
7
+ delegate :logger, :configuration, to: :Logux
8
+
9
+ def initialize(action:, meta:)
10
+ @action = action
11
+ @meta = meta
12
+ end
13
+
14
+ def call!
15
+ logger.debug('Searching policy for Logux action:' \
16
+ " #{action}, meta: #{meta}")
17
+ policy.public_send("#{action.action_type}?")
18
+ rescue Logux::UnknownActionError, Logux::UnknownChannelError => e
19
+ raise e if configuration.verify_authorized
20
+
21
+ logger.warn(e)
22
+ end
23
+
24
+ private
25
+
26
+ def class_finder
27
+ @class_finder ||= Logux::ClassFinder.new(action: action, meta: meta)
28
+ end
29
+
30
+ def policy
31
+ class_finder.find_policy_class.new(action: action, meta: meta)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ module Process
5
+ autoload :Batch, 'logux/process/batch'
6
+ autoload :Auth, 'logux/process/auth'
7
+ autoload :Action, 'logux/process/action'
8
+ end
9
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ module Process
5
+ class Action
6
+ attr_reader :stream, :chunk
7
+ attr_accessor :stop_process
8
+
9
+ def initialize(stream:, chunk:)
10
+ @stream = stream
11
+ @chunk = chunk
12
+ end
13
+
14
+ def call
15
+ process_authorization!
16
+ process_action!
17
+ end
18
+
19
+ def action_from_chunk
20
+ @action_from_chunk ||= chunk[:action]
21
+ end
22
+
23
+ def meta_from_chunk
24
+ @meta_from_chunk ||= chunk[:meta]
25
+ end
26
+
27
+ def stop_process?
28
+ @stop_process ||= false
29
+ end
30
+
31
+ def stop_process!
32
+ @stop_process = true
33
+ end
34
+
35
+ private
36
+
37
+ def process_action!
38
+ return if stop_process?
39
+
40
+ action_caller = Logux::ActionCaller.new(
41
+ action: action_from_chunk,
42
+ meta: meta_from_chunk
43
+ )
44
+
45
+ stream.write(action_caller.call!.format)
46
+ end
47
+
48
+ def process_authorization!
49
+ policy_caller = Logux::PolicyCaller.new(action: action_from_chunk,
50
+ meta: meta_from_chunk)
51
+ policy_check = policy_caller.call!
52
+ status = policy_check ? :approved : :forbidden
53
+ stream.write([status, meta_from_chunk.id])
54
+ return stream.write(',') if policy_check
55
+
56
+ stop_process!
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ module Process
5
+ class Auth
6
+ attr_reader :stream, :chunk
7
+
8
+ def initialize(stream:, chunk:)
9
+ @stream = stream
10
+ @chunk = chunk
11
+ end
12
+
13
+ def call
14
+ authed = Logux.configuration.auth_rule.call(user_id, chunk.credentials)
15
+ return stream.write(['authenticated', chunk.auth_id]) if authed
16
+
17
+ stream.write(['denied', chunk.auth_id])
18
+ end
19
+
20
+ private
21
+
22
+ def user_id
23
+ chunk.node_id.split(':').first
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ module Process
5
+ class Batch
6
+ attr_reader :stream, :batch
7
+
8
+ def initialize(stream:, batch:)
9
+ @stream = stream
10
+ @batch = batch
11
+ end
12
+
13
+ def call
14
+ last_chunk = batch.size - 1
15
+ preprocessed_batch.map.with_index do |chunk, index|
16
+ case chunk[:type]
17
+ when :action
18
+ process_action(chunk: chunk.slice(:action, :meta))
19
+ when :auth
20
+ process_auth(chunk: chunk[:auth])
21
+ end
22
+ stream.write(',') if index != last_chunk
23
+ end
24
+ end
25
+
26
+ def process_action(chunk:)
27
+ Logux::Process::Action.new(stream: stream, chunk: chunk).call
28
+ end
29
+
30
+ def process_auth(chunk:)
31
+ Logux::Process::Auth.new(stream: stream, chunk: chunk).call
32
+ end
33
+
34
+ def preprocessed_batch
35
+ @preprocessed_batch ||= batch.map do |chunk|
36
+ case chunk[0]
37
+ when 'action'
38
+ preprocess_action(chunk)
39
+ when 'auth'
40
+ preprocess_auth(chunk)
41
+ end
42
+ end
43
+ end
44
+
45
+ def preprocess_action(chunk)
46
+ { type: :action,
47
+ action: Logux::Actions.new(chunk[1]),
48
+ meta: Logux::Meta.new(chunk[2]) }
49
+ end
50
+
51
+ def preprocess_auth(chunk)
52
+ { type: :auth,
53
+ auth: Logux::Auth.new(node_id: chunk[1],
54
+ credentials: chunk[2],
55
+ auth_id: chunk[3]) }
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ class Response
5
+ attr_reader :status, :action, :meta, :custom_data
6
+
7
+ def initialize(status, action:, meta:, custom_data: nil)
8
+ @status = status
9
+ @action = action
10
+ @meta = meta
11
+ @custom_data = custom_data
12
+ end
13
+
14
+ def format
15
+ [status, custom_data || meta.id]
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ class Stream
5
+ attr_reader :stream
6
+
7
+ delegate :close, to: :stream
8
+
9
+ def initialize(stream)
10
+ @stream = stream
11
+ end
12
+
13
+ def write(payload)
14
+ processed_payload = process(payload)
15
+ Logux.logger.debug("Write to Logux response: #{processed_payload}")
16
+ stream.write(processed_payload)
17
+ end
18
+
19
+ private
20
+
21
+ def process(payload)
22
+ payload.is_a?(::String) ? payload : payload.to_json
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ module Test
5
+ class << self
6
+ attr_accessor :http_requests_enabled
7
+
8
+ def enable_http_requests!
9
+ raise ArgumentError unless block_given?
10
+
11
+ begin
12
+ self.http_requests_enabled = true
13
+ yield
14
+ ensure
15
+ self.http_requests_enabled = false
16
+ end
17
+ end
18
+ end
19
+
20
+ module Client
21
+ def post(params)
22
+ if Logux::Test.http_requests_enabled
23
+ super(params)
24
+ else
25
+ Logux::Test::Store.instance.add(params.to_json)
26
+ end
27
+ end
28
+ end
29
+
30
+ autoload :Helpers, 'logux/test/helpers'
31
+ autoload :Store, 'logux/test/store'
32
+ autoload :Matchers, 'logux/test/matchers'
33
+ end
34
+ end
35
+ Logux::Client.prepend Logux::Test::Client
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ module Test
5
+ module Helpers
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ before do
10
+ Logux::Test::Store.instance.reset!
11
+ end
12
+ end
13
+
14
+ def logux_store
15
+ Logux::Test::Store.instance.data
16
+ end
17
+
18
+ def send_to_logux(*commands)
19
+ Logux::Test::Matchers::SendToLogux.new(*commands)
20
+ end
21
+
22
+ def a_logux_meta_with(attributes = {})
23
+ RSpec::Matchers::BuiltIn::Include.new(attributes.stringify_keys)
24
+ end
25
+ alias a_logux_meta a_logux_meta_with
26
+
27
+ def a_logux_action_with(attributes = {})
28
+ RSpec::Matchers::BuiltIn::Include.new(attributes.stringify_keys)
29
+ end
30
+ alias a_logux_action a_logux_action_with
31
+
32
+ def logux_approved(meta = nil)
33
+ Logux::Test::Matchers::ResponseChunks.new(
34
+ meta: meta, includes: ['approved'], excludes: %w[forbidden error]
35
+ )
36
+ end
37
+
38
+ def logux_processed(meta = nil)
39
+ Logux::Test::Matchers::ResponseChunks.new(
40
+ meta: meta, includes: ['processed'], excludes: %w[forbidden error]
41
+ )
42
+ end
43
+
44
+ def logux_forbidden(meta = nil)
45
+ Logux::Test::Matchers::ResponseChunks.new(
46
+ meta: meta, includes: ['forbidden']
47
+ )
48
+ end
49
+
50
+ def logux_errored(meta = nil)
51
+ Logux::Test::Matchers::ResponseChunks.new(
52
+ meta: meta, includes: ['error']
53
+ )
54
+ end
55
+
56
+ def logux_authenticated(meta = nil)
57
+ Logux::Test::Matchers::ResponseChunks.new(
58
+ meta: meta, includes: ['authenticated']
59
+ )
60
+ end
61
+
62
+ def logux_unauthorized(meta = nil)
63
+ Logux::Test::Matchers::ResponseChunks.new(
64
+ meta: meta, includes: ['unauthorized']
65
+ )
66
+ end
67
+
68
+ def logux_denied(meta = nil)
69
+ Logux::Test::Matchers::ResponseChunks.new(
70
+ meta: meta, includes: ['denied']
71
+ )
72
+ end
73
+ end
74
+ end
75
+ end