logux_rails 0.1.0

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 (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