kantox-roles 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/README.md +224 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/kantox-roles.gemspec +40 -0
- data/lib/kantox/roles.rb +199 -0
- data/lib/kantox/roles/helpers.rb +138 -0
- data/lib/kantox/roles/logger.rb +102 -0
- data/lib/kantox/roles/strategies/aspect.rb +13 -0
- data/lib/kantox/roles/strategies/cancancan.rb +48 -0
- data/lib/kantox/roles/strategies/pundit.rb +70 -0
- data/lib/kantox/roles/strategies/strategy_error.rb +12 -0
- data/lib/kantox/roles/strategies/wrapper.rb +15 -0
- data/lib/kantox/roles/version.rb +5 -0
- data/lib/rails/generators/kantox/policy_spec_helper.rb.tmpl +35 -0
- data/lib/rails/generators/kantox/pundit_policy_generator.rb +234 -0
- metadata +178 -0
@@ -0,0 +1,138 @@
|
|
1
|
+
module Kantox
|
2
|
+
module Helpers
|
3
|
+
# Fakes hash values
|
4
|
+
# @param [Hash] hash to fake
|
5
|
+
# @return [Hash] faked hash
|
6
|
+
def fake_data hash_or_array_or_json, deep = true
|
7
|
+
(hash_or_array_or_json.is_a?(String) ? JSON.parse(hash_or_array_or_json) : hash_or_array_or_json).each_with_index.map do |kv, index|
|
8
|
+
case kv
|
9
|
+
when Array # we iterate hash
|
10
|
+
[ kv.first.respond_to?(:to_sym) ? kv.first.to_sym : kv.first,
|
11
|
+
deep ? case kv.last
|
12
|
+
when Array then fake_data(kv.last).values
|
13
|
+
when Hash then fake_hash kv.last
|
14
|
+
else Kantox::LOCK_EMOJI
|
15
|
+
end
|
16
|
+
: Kantox::LOCK_EMOJI
|
17
|
+
]
|
18
|
+
else [index, Kantox::LOCK_EMOJI]
|
19
|
+
end
|
20
|
+
end.to_h
|
21
|
+
end
|
22
|
+
def fake_hash hash_or_array_or_json, deep = true
|
23
|
+
fake_data(hash_or_array_or_json, deep).merge({ Kantox::LOCK_EMOJI => true })
|
24
|
+
end
|
25
|
+
def fake_array array
|
26
|
+
array.zip(fake_data(array, false).values).to_h
|
27
|
+
end
|
28
|
+
module_function :fake_data, :fake_hash, :fake_array
|
29
|
+
private :fake_data
|
30
|
+
|
31
|
+
# Returns class, method and parameters as s string.
|
32
|
+
# @return [Hash|nil]
|
33
|
+
def get_instance_method name
|
34
|
+
k, m = name.split('#')
|
35
|
+
|
36
|
+
unless Kernel.const_defined? k
|
37
|
+
Kantox::Helpers.debug "Bad handler: «#{k}##{m}» not found. Error message: “#{e}”.\nUsually it’s OK meaning that the respective class is not yet initialized."
|
38
|
+
return nil
|
39
|
+
end
|
40
|
+
|
41
|
+
klazz = Kernel.const_get(k)
|
42
|
+
|
43
|
+
meth = (
|
44
|
+
klazz.private_method_defined?(m) ||
|
45
|
+
klazz.protected_method_defined?(m) ||
|
46
|
+
klazz.public_method_defined?(m)
|
47
|
+
) && klazz.instance_method(m)
|
48
|
+
|
49
|
+
unless meth
|
50
|
+
Kantox::Helpers.err "Class [#{klazz}] has no instance method [#{m}]."
|
51
|
+
return nil
|
52
|
+
end
|
53
|
+
|
54
|
+
params_arr = meth.parameters
|
55
|
+
params_str = [
|
56
|
+
params_arr.select { |tv| tv.first == :req }.map(&:last).map(&:to_s),
|
57
|
+
params_arr.select { |tv| tv.first == :rest }.map(&:last).map { |m| "*#{m}"},
|
58
|
+
params_arr.select { |tv| tv.first == :block }.map(&:last).map { |m| "&#{m}"}
|
59
|
+
].flatten.join(', ')
|
60
|
+
|
61
|
+
Hashie::Mash.new(
|
62
|
+
class: klazz,
|
63
|
+
method: { name: m, instance: meth },
|
64
|
+
params: { string: params_str }
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
# @param mash [Hash|Hashie::Mash|NilClass] hash to merge `hos` into
|
69
|
+
# @param hos [Hash|Hashie::Mash|String] the new values taken from hash,
|
70
|
+
# mash or string (when string, should be either valid YAML file name or
|
71
|
+
# string with valid YAML)
|
72
|
+
def merge_hash_or_string mash, hos
|
73
|
+
case mash
|
74
|
+
when Hashie::Mash then mash
|
75
|
+
when Hash then Hashie::Mash.new(mash)
|
76
|
+
when NilClass then Hashie::Mash.new
|
77
|
+
else
|
78
|
+
fail ArgumentError.new "#{__callee__} expects [Hash|Hashie::Mash|NilClass] as first parameter"
|
79
|
+
end.deep_merge case hos
|
80
|
+
when NilClass then {} # aka skip
|
81
|
+
when Hash then hos
|
82
|
+
when String
|
83
|
+
begin
|
84
|
+
File.exists?(hos) ? Hashie::Mash.load(hos) : Hashie::Mash.new(YAML.load(hos))
|
85
|
+
rescue ArgumentError => ae
|
86
|
+
fail ArgumentError.new "#{__callee__} expects valid YAML configuration file. [#{hos}] contains invalid syntax."
|
87
|
+
rescue Psych::SyntaxError => pse
|
88
|
+
fail ArgumentError.new "#{__callee__} expects valid YAML configuration string. Got:\n#{hos}"
|
89
|
+
end
|
90
|
+
else
|
91
|
+
Kantox::Helpers.warn 'Kantox::Roles#configure accepts either String or Hash as parameter.'
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# @return [Method|nil]
|
96
|
+
def get_runner_strategy name
|
97
|
+
im = get_instance_method(name)
|
98
|
+
im.nil? ? im : im[:method][:instance].bind(im[:class])
|
99
|
+
end
|
100
|
+
|
101
|
+
# @return [Method|nil]
|
102
|
+
def get_simple_strategy name
|
103
|
+
get_runner_strategy("Kantox::Strategies##{name}")
|
104
|
+
end
|
105
|
+
|
106
|
+
# @return [Proc]
|
107
|
+
def get_lambda_strategy lmbd
|
108
|
+
begin
|
109
|
+
eval(lmbd)
|
110
|
+
rescue LocalJumpError => lje
|
111
|
+
Kantox::Helpers.err "Lambda strategy failed: «return» is not allowed from this lambda.\n#{lmbd}\nOriginal error: #{lje}"
|
112
|
+
rescue NameError => ne
|
113
|
+
Kantox::Helpers.err "Lambda strategy failed: syntax error in lambda.\n#{lmbd}\nOriginal error: #{ne}"
|
114
|
+
rescue SyntaxError => se
|
115
|
+
Kantox::Helpers.err "Lambda strategy failed: syntax error in lambda.\n#{lmbd}\nOriginal error: #{se}"
|
116
|
+
rescue RuntimeError => re
|
117
|
+
Kantox::Helpers.err "Lambda strategy failed: undetermined error in lambda.\n#{lmbd}\nOriginal error: #{re}"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# @return [Object]
|
122
|
+
def get_object_strategy name, mash
|
123
|
+
klazz = name.split('_').map { |n| n.capitalize }.join('::')
|
124
|
+
return nil unless Module.const_defined?(klazz)
|
125
|
+
klazz = Module.const_get(klazz)
|
126
|
+
return nil unless klazz.is_a?(Class) && klazz.instance_methods(false).include?(:to_proc)
|
127
|
+
klazz.new(mash)
|
128
|
+
end
|
129
|
+
|
130
|
+
module_function :get_instance_method, :merge_hash_or_string
|
131
|
+
|
132
|
+
Helpers.instance_methods.select do |m|
|
133
|
+
m.to_s =~ /\Aget_(.*)_strategy\z/
|
134
|
+
end.each do |m|
|
135
|
+
module_function m
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Kantox
|
5
|
+
module Helpers
|
6
|
+
BACKTRACE_LENGTH = 12
|
7
|
+
SEV_COLORS = {
|
8
|
+
'INFO' => ['01;38;05;21', '00;38;05;152'],
|
9
|
+
'WARN' => ['01;38;05;226', '00;38;05;222'],
|
10
|
+
'ERROR' => ['01;38;05;196', '01;38;05;174'],
|
11
|
+
'DEBUG' => ['01;38;05;242', '00;38;05;246'],
|
12
|
+
'ANY' => ['01;38;05;222;48;05;238', '01;38;05;253;48;05;238']
|
13
|
+
}
|
14
|
+
SEV_SYMBOLS = {
|
15
|
+
'INFO' => '✔',
|
16
|
+
'WARN' => '✗',
|
17
|
+
'ERROR' => '✘',
|
18
|
+
'DEBUG' => '✓',
|
19
|
+
'ANY' => '▶'
|
20
|
+
}
|
21
|
+
|
22
|
+
@stopwords = []
|
23
|
+
def logger_stopwords file
|
24
|
+
if File.exist?(file)
|
25
|
+
@stopwords += File.read(file).split($/).map { |l| Regexp.new l }
|
26
|
+
else
|
27
|
+
Kantox::Helpers.warn "Bad stopwords file: [#{file}]."
|
28
|
+
end
|
29
|
+
end
|
30
|
+
module_function :logger_stopwords
|
31
|
+
|
32
|
+
def self.clrz txt, clr
|
33
|
+
return txt unless @tty
|
34
|
+
|
35
|
+
"\e[#{clr}m#{txt.gsub(/«(.*?)»/, "\e[01;38;05;51m\\1\e[#{clr}m")}\e[0m"
|
36
|
+
end
|
37
|
+
|
38
|
+
def logger
|
39
|
+
unless @logger
|
40
|
+
@logger, @logdevice = if Kernel.const_defined? 'Rails'
|
41
|
+
[
|
42
|
+
::Rails.logger,
|
43
|
+
::Rails.logger
|
44
|
+
.instance_variable_get(:@logger)
|
45
|
+
.instance_variable_get(:@log)
|
46
|
+
]
|
47
|
+
else
|
48
|
+
l = Logger.new($stdout)
|
49
|
+
[l, l]
|
50
|
+
end
|
51
|
+
@tty = @logdevice.instance_variable_get(:@logdev).instance_variable_get(:@dev).tty? ||
|
52
|
+
Kernel.const_defined?('Rails') && ::Rails.env.development?
|
53
|
+
|
54
|
+
unless Kernel.const_defined?('Rails') && ::Rails.env.production? || ENV['RAILS_PRETTY_LOG'] != '42'
|
55
|
+
@logdevice.formatter = proc { |severity, datetime, progname, message|
|
56
|
+
message = message.join($/) if message[0] == '[' && message[-1] == ']' && (arr_msg = JSON.parse(message)).is_a?(Array) rescue message
|
57
|
+
|
58
|
+
message.strip! # strip
|
59
|
+
message.gsub! "\n", "\n⮩#{' ' * 29}" # align
|
60
|
+
if message.empty? || @stopwords.any? { |sw| sw =~ message }
|
61
|
+
nil
|
62
|
+
else
|
63
|
+
'' << clrz("#{SEV_SYMBOLS[severity]} ", SEV_COLORS[severity].first)\
|
64
|
+
<< clrz(severity[0..2], SEV_COLORS[severity].first) \
|
65
|
+
<< ' | ' \
|
66
|
+
<< clrz(datetime.strftime('%Y%m%d-%H%M%S.%3N'), '01;38;05;238') \
|
67
|
+
<< ' | ' \
|
68
|
+
<< clrz(message, SEV_COLORS[severity].last) \
|
69
|
+
<< "\n"
|
70
|
+
end
|
71
|
+
}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
@logger
|
75
|
+
end
|
76
|
+
module_function :logger
|
77
|
+
def log message
|
78
|
+
logger.unknown { message }
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
|
82
|
+
def catched message, e
|
83
|
+
bt = e.backtrace.is_a?(Array) ? e.backtrace : caller(1)
|
84
|
+
msg = "#{message}\nOriginal exception «#{e.class}»: #{e.message}\n" \
|
85
|
+
"Stacktrace: #{bt.take(BACKTRACE_LENGTH).join($/)}\n" \
|
86
|
+
"[...#{bt.length - BACKTRACE_LENGTH} more]"
|
87
|
+
warn msg
|
88
|
+
end
|
89
|
+
module_function :log, :catched
|
90
|
+
%i(warn info error debug).each do |m|
|
91
|
+
class_eval "
|
92
|
+
def #{m} message
|
93
|
+
logger.#{m}(message)
|
94
|
+
nil
|
95
|
+
end
|
96
|
+
module_function :#{m}
|
97
|
+
"
|
98
|
+
end
|
99
|
+
alias_method :err, :error
|
100
|
+
module_function :err
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require_relative 'strategy_error'
|
2
|
+
|
3
|
+
module Kantox
|
4
|
+
module Strategies
|
5
|
+
class AspectError < Kantox::Strategies::StrategyError ; end
|
6
|
+
def aspect context, im, *args
|
7
|
+
# fail AspectError.new(context, im)
|
8
|
+
Kantox::Helpers.info "Aspect strategy applyed to #{context}"
|
9
|
+
Kantox::Helpers.warn 'Aspect strategy does not expect block passed.' if block_given?
|
10
|
+
end
|
11
|
+
module_function :aspect
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require_relative 'strategy_error'
|
2
|
+
|
3
|
+
module Kantox
|
4
|
+
module Strategies
|
5
|
+
class CanCanCanError < Kantox::Strategies::StrategyError
|
6
|
+
attr_reader :ability
|
7
|
+
def initialize context, im, policy = nil
|
8
|
+
super(context, im)
|
9
|
+
@policy = policy
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# Ability factory for cancancan.
|
14
|
+
module AbilityFactory
|
15
|
+
def lookup klz, mth
|
16
|
+
meth = mth.capitalize.gsub(/_./) { |m|
|
17
|
+
m[-1].capitalize
|
18
|
+
}
|
19
|
+
klazz = "#{klz.capitalize}#{meth}Ability"
|
20
|
+
if Kantox::Abilities.const_defined? klazz
|
21
|
+
Kantox::Abilities.const_get klazz
|
22
|
+
else
|
23
|
+
Kantox::Helpers.error "Missing ability for «#{self.class.name}##{m}». You should define it explicitly."
|
24
|
+
end
|
25
|
+
end
|
26
|
+
module_function :lookup
|
27
|
+
end
|
28
|
+
|
29
|
+
def cancancan context, im, *args
|
30
|
+
Kantox::Helpers.debug "CanCanCaning object: [#{context}], «#{im}»"
|
31
|
+
begin # Whoever does not reply on classify would be punished :)
|
32
|
+
model = const_defined?('Rails') ?
|
33
|
+
context.instance_eval('controller_path.classify.singularize') :
|
34
|
+
context.class.name
|
35
|
+
ability = AbilityFactory.lookup model.split('::').last, im.split('#').last
|
36
|
+
user = Kernel.const_defined?('User') && User.respond_to?(:current) ?
|
37
|
+
User.current :
|
38
|
+
(context.respond_to?(:current_user) ? context.current_user : '@fixme')
|
39
|
+
fail CanCanCanError.new(context, im, ability) unless ability.new(user, '@todo').can *im.split('#')
|
40
|
+
rescue NameError => e
|
41
|
+
Kantox::Helpers.err "Error cancancaning «#{context}». Will reject request.\nOriginal error: #{e}"
|
42
|
+
throw CanCanCanError.new(context, im)
|
43
|
+
end
|
44
|
+
Kantox::Helpers.warn "CanCanCan strategy does not expect block passed.\nFailed: «#{ability}»" if block_given?
|
45
|
+
end
|
46
|
+
module_function :cancancan
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require_relative 'strategy_error'
|
2
|
+
|
3
|
+
module Kantox
|
4
|
+
module Strategies
|
5
|
+
class PunditError < Kantox::Strategies::StrategyError
|
6
|
+
attr_reader :policy, :user
|
7
|
+
def initialize context, im, policy = nil, user = nil
|
8
|
+
super(context, im)
|
9
|
+
@policy = policy
|
10
|
+
@user = user
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
ctx = @context ? ", context: #{@context}" : ''
|
15
|
+
usr = @user ? ", user: #{@user.login rescue nil} #{@user.class}" : ''
|
16
|
+
"(strategy: «pundit»#{usr}, policy: #{@policy}#{ctx})"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Policy factory for pundit.
|
21
|
+
module PolicyFactory
|
22
|
+
def lookup name
|
23
|
+
klazz = "#{name}Policy"
|
24
|
+
if Kantox::Policies.const_defined? klazz
|
25
|
+
Kantox::Policies.const_get klazz
|
26
|
+
else
|
27
|
+
c = Kantox::Policies.const_set(klazz, Class.new(ApplicationPolicy) {
|
28
|
+
def method_missing m, *args, &cb
|
29
|
+
if m.to_s[-1] == '?'
|
30
|
+
Kantox::Helpers.error "Missing policy for «#{self.class.name}##{m}». You should define it explicitly."
|
31
|
+
nil
|
32
|
+
else
|
33
|
+
super
|
34
|
+
end
|
35
|
+
end
|
36
|
+
})
|
37
|
+
c.const_set('Scope', Class.new(ApplicationPolicy::Scope) {
|
38
|
+
def resolve
|
39
|
+
scope
|
40
|
+
end
|
41
|
+
})
|
42
|
+
c
|
43
|
+
end
|
44
|
+
end
|
45
|
+
module_function :lookup
|
46
|
+
end
|
47
|
+
|
48
|
+
def pundit context, im, *args
|
49
|
+
context_as_string = "#{context}"
|
50
|
+
context_as_string = "#{context_as_string[0..58]} [...]" if context_as_string.length > 65
|
51
|
+
Kantox::Helpers.debug "Punditing object: [#{context_as_string}], «#{im}»"
|
52
|
+
begin # Whoever does not reply on classify would be punished :)
|
53
|
+
model = const_defined?('Rails') && context.respond_to?(:controller_path) ?
|
54
|
+
context.instance_eval('controller_path.classify.singularize') :
|
55
|
+
context.class.name
|
56
|
+
model << 's' if model[-5..-1] == 'Statu' # Fucking Rails
|
57
|
+
policy = PolicyFactory.lookup model.split('::')[2..-1].join('::')
|
58
|
+
user = Kernel.const_defined?('User') && User.respond_to?(:current) ?
|
59
|
+
User.current :
|
60
|
+
(context.respond_to?(:current_user) ? context.current_user : nil)
|
61
|
+
fail PunditError.new(context, im, policy, user) unless policy.new(user, context).send "#{im.split('#').last}?"
|
62
|
+
rescue NameError => e
|
63
|
+
Kantox::Helpers.err "Error punditing «#{context.class}». Will reject request.\nOriginal error: #{e}"
|
64
|
+
throw PunditError.new(context, im)
|
65
|
+
end
|
66
|
+
Kantox::Helpers.warn "Pundit strategy does not expect block passed.\nFailed: «#{policy}»" if block_given?
|
67
|
+
end
|
68
|
+
module_function :pundit
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Kantox
|
2
|
+
module Strategies
|
3
|
+
class StrategyError < RuntimeError
|
4
|
+
def initialize context, im
|
5
|
+
# FIXME Patch the stacktrace
|
6
|
+
# failed = caller(4).first[/`(\w+)'/, 1]
|
7
|
+
meth = Kantox::Helpers.get_instance_method(im.sub('#', '#∃'))[:method][:instance]
|
8
|
+
super("StrategyError @ #{meth.source_location.join(':')}:in: `#{im}'\n ==> induced by #{self.to_s}")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require_relative 'strategy_error'
|
2
|
+
|
3
|
+
module Kantox
|
4
|
+
module Strategies
|
5
|
+
module Wrapper
|
6
|
+
def wrap context, *params
|
7
|
+
Kantox::Helpers.info "Wrapped. Context: #{context}. Params: #{params.inspect}"
|
8
|
+
# Kantox::Helpers.debug "Caller: #{caller(0)[0..5]}"
|
9
|
+
# fail Kantox::Strategies::StrategyError, "@ #{context}"
|
10
|
+
yield *params if block_given?
|
11
|
+
end
|
12
|
+
module_function :wrap
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
ENV['RAILS_ENV'] ||= 'test'
|
2
|
+
|
3
|
+
require 'pundit/rspec'
|
4
|
+
|
5
|
+
RSpec.configure do |c|
|
6
|
+
c.treat_symbols_as_metadata_keys_with_true_values = true
|
7
|
+
end
|
8
|
+
|
9
|
+
UserTypes = ApplicationPolicy::UserTypes.call
|
10
|
+
|
11
|
+
def examples_for_policy klazz
|
12
|
+
describe klazz do
|
13
|
+
extend Pundit::RSpec::DSL
|
14
|
+
subject { klazz }
|
15
|
+
[:read, :write].each do |access|
|
16
|
+
klazz::METHODS[access].each do |meth|
|
17
|
+
permissions(meth) do
|
18
|
+
it "denies access if user is not allowed", :roles do
|
19
|
+
extend Pundit::RSpec::Matchers
|
20
|
+
(UserTypes - klazz::ALLOWED[access]).each do |user|
|
21
|
+
expect(subject).not_to permit(user.send(:new), controller)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
it "grants access if user is allowed", :roles do
|
26
|
+
extend Pundit::RSpec::Matchers
|
27
|
+
klazz::ALLOWED[access].each do |user|
|
28
|
+
expect(subject).to permit(user.send(:new), controller)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|