logux_rails 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|