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.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.pryrc +8 -0
- data/.rspec +3 -0
- data/.rubocop.yml +33 -0
- data/.rubocop_todo.yml +17 -0
- data/.travis.yml +11 -0
- data/Appraisals +13 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +68 -0
- data/Rakefile +10 -0
- data/app/controllers/logux_controller.rb +41 -0
- data/app/helpers/logux_helper.rb +4 -0
- data/app/logux/actions.rb +3 -0
- data/app/logux/policies.rb +3 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/config/routes.rb +5 -0
- data/docker-compose.yml +29 -0
- data/lib/generators/logux/model/USAGE +11 -0
- data/lib/generators/logux/model/model_generator.rb +28 -0
- data/lib/generators/logux/model/templates/migration.rb.erb +14 -0
- data/lib/logux.rb +107 -0
- data/lib/logux/action_caller.rb +42 -0
- data/lib/logux/action_controller.rb +6 -0
- data/lib/logux/actions.rb +29 -0
- data/lib/logux/add.rb +37 -0
- data/lib/logux/auth.rb +6 -0
- data/lib/logux/base_controller.rb +37 -0
- data/lib/logux/channel_controller.rb +24 -0
- data/lib/logux/class_finder.rb +61 -0
- data/lib/logux/client.rb +21 -0
- data/lib/logux/engine.rb +6 -0
- data/lib/logux/error_renderer.rb +40 -0
- data/lib/logux/meta.rb +36 -0
- data/lib/logux/model.rb +39 -0
- data/lib/logux/model/dsl.rb +15 -0
- data/lib/logux/model/proxy.rb +24 -0
- data/lib/logux/model/updater.rb +39 -0
- data/lib/logux/model/updates_deprecator.rb +54 -0
- data/lib/logux/node.rb +37 -0
- data/lib/logux/policy.rb +14 -0
- data/lib/logux/policy_caller.rb +34 -0
- data/lib/logux/process.rb +9 -0
- data/lib/logux/process/action.rb +60 -0
- data/lib/logux/process/auth.rb +27 -0
- data/lib/logux/process/batch.rb +59 -0
- data/lib/logux/response.rb +18 -0
- data/lib/logux/stream.rb +25 -0
- data/lib/logux/test.rb +35 -0
- data/lib/logux/test/helpers.rb +75 -0
- data/lib/logux/test/matchers.rb +10 -0
- data/lib/logux/test/matchers/base.rb +25 -0
- data/lib/logux/test/matchers/response_chunks.rb +48 -0
- data/lib/logux/test/matchers/send_to_logux.rb +51 -0
- data/lib/logux/test/store.rb +21 -0
- data/lib/logux/version.rb +5 -0
- data/lib/logux_rails.rb +3 -0
- data/lib/tasks/logux_tasks.rake +46 -0
- data/logux_rails.gemspec +46 -0
- 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
|
data/lib/logux/node.rb
ADDED
@@ -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
|
data/lib/logux/policy.rb
ADDED
@@ -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,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
|
data/lib/logux/stream.rb
ADDED
@@ -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
|
data/lib/logux/test.rb
ADDED
@@ -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
|