casbin-ruby 1.0.3 → 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +242 -0
  3. data/lib/casbin-ruby.rb +11 -0
  4. data/lib/casbin-ruby/config/config.rb +115 -0
  5. data/lib/casbin-ruby/core_enforcer.rb +356 -0
  6. data/lib/casbin-ruby/effect/allow_and_deny_effector.rb +23 -0
  7. data/lib/casbin-ruby/effect/allow_override_effector.rb +23 -0
  8. data/lib/casbin-ruby/effect/default_effector.rb +37 -0
  9. data/lib/casbin-ruby/effect/deny_override_effector.rb +23 -0
  10. data/lib/casbin-ruby/effect/effector.rb +18 -0
  11. data/lib/casbin-ruby/effect/priority_effector.rb +25 -0
  12. data/lib/casbin-ruby/enforcer.rb +189 -0
  13. data/lib/casbin-ruby/internal_enforcer.rb +73 -0
  14. data/lib/casbin-ruby/management_enforcer.rb +297 -0
  15. data/lib/casbin-ruby/model/assertion.rb +33 -0
  16. data/lib/casbin-ruby/model/function_map.rb +30 -0
  17. data/lib/casbin-ruby/model/model.rb +80 -0
  18. data/lib/casbin-ruby/model/policy.rb +161 -0
  19. data/lib/casbin-ruby/persist/adapter.rb +39 -0
  20. data/lib/casbin-ruby/persist/adapters/file_adapter.rb +53 -0
  21. data/lib/casbin-ruby/persist/batch_adapter.rb +16 -0
  22. data/lib/casbin-ruby/persist/filtered_adapter.rb +17 -0
  23. data/lib/casbin-ruby/rbac/default_role_manager/role.rb +54 -0
  24. data/lib/casbin-ruby/rbac/default_role_manager/role_manager.rb +146 -0
  25. data/lib/casbin-ruby/rbac/role_manager.rb +22 -0
  26. data/lib/casbin-ruby/synced_enforcer.rb +39 -0
  27. data/lib/casbin-ruby/util.rb +80 -0
  28. data/lib/casbin-ruby/util/builtin_operators.rb +105 -0
  29. data/lib/casbin-ruby/util/evaluator.rb +27 -0
  30. data/lib/casbin-ruby/util/thread_lock.rb +19 -0
  31. data/lib/casbin-ruby/version.rb +5 -0
  32. data/spec/casbin/config/config_spec.rb +66 -0
  33. data/spec/casbin/core_enforcer_spec.rb +473 -0
  34. data/spec/casbin/enforcer_spec.rb +302 -0
  35. data/spec/casbin/model/function_map_spec.rb +28 -0
  36. data/spec/casbin/rbac/default_role_manager/role_manager_spec.rb +131 -0
  37. data/spec/casbin/rbac/default_role_manager/role_spec.rb +84 -0
  38. data/spec/casbin/util/builtin_operators_spec.rb +205 -0
  39. data/spec/casbin/util_spec.rb +98 -0
  40. data/spec/support/model_helper.rb +9 -0
  41. metadata +51 -3
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+ require 'casbin-ruby/rbac/role_manager'
5
+ require 'casbin-ruby/rbac/default_role_manager/role'
6
+
7
+ module Casbin
8
+ module Rbac
9
+ module DefaultRoleManager
10
+ # provides a default implementation for the RoleManager interface
11
+ class RoleManager < Rbac::RoleManager
12
+ attr_accessor :all_roles, :max_hierarchy_level, :matching_func, :has_domain_pattern, :domain_matching_func
13
+ attr_reader :logger
14
+
15
+ def initialize(max_hierarchy_level, logger: Logger.new($stdout))
16
+ super()
17
+ @logger = logger
18
+ @all_roles = {}
19
+ @max_hierarchy_level = max_hierarchy_level
20
+ end
21
+
22
+ def add_matching_func(fn)
23
+ @matching_func = fn
24
+ end
25
+
26
+ def add_domain_matching_func(fn)
27
+ self.has_domain_pattern = true
28
+ self.domain_matching_func = fn
29
+ end
30
+
31
+ def has_role(name)
32
+ return all_roles.key?(name) if matching_func.nil?
33
+
34
+ all_roles.each_key { |key| return true if matching_func.call(name, key) }
35
+ false
36
+ end
37
+
38
+ def create_role(name)
39
+ all_roles[name] = Role.new(name) unless all_roles.key?(name)
40
+ if matching_func
41
+ all_roles.each do |key, role|
42
+ all_roles[name].add_role(role) if matching_func.call(name, key) && name != key
43
+ end
44
+ end
45
+
46
+ all_roles[name]
47
+ end
48
+
49
+ def clear
50
+ @all_roles = {}
51
+ end
52
+
53
+ def add_link(name1, name2, *domain)
54
+ names = names_by_domain(name1, name2, *domain)
55
+
56
+ role1 = create_role(names[0])
57
+ role2 = create_role(names[1])
58
+ role1.add_role(role2)
59
+ end
60
+
61
+ def delete_link(name1, name2, *domain)
62
+ names = names_by_domain(name1, name2, *domain)
63
+
64
+ raise 'error: name1 or name2 does not exist' if !has_role(names[0]) || !has_role(names[1])
65
+
66
+ role1 = create_role(names[0])
67
+ role2 = create_role(names[1])
68
+ role1.delete_role(role2)
69
+ end
70
+
71
+ def has_link(name1, name2, *domain)
72
+ names = names_by_domain(name1, name2, *domain)
73
+
74
+ return true if names[0] == names[1]
75
+
76
+ return false if !has_role(names[0]) || !has_role(names[1])
77
+
78
+ if matching_func.nil?
79
+ role1 = create_role names[0]
80
+ role1.has_role names[1], max_hierarchy_level
81
+ else
82
+ all_roles.each do |key, role|
83
+ return true if matching_func.call(names[0], key) && role.has_role(names[1], max_hierarchy_level)
84
+ end
85
+
86
+ false
87
+ end
88
+ end
89
+
90
+ # gets the roles that a subject inherits.
91
+ # domain is a prefix to the roles.
92
+ def get_roles(name, *domain)
93
+ name = name_by_domain(name, *domain)
94
+ return [] unless has_role(name)
95
+
96
+ roles = create_role(name).get_roles
97
+ if domain.size == 1
98
+ roles.each_with_index { |value, index| roles[index] = value[domain[0].size + 2..value.size] }
99
+ end
100
+
101
+ roles
102
+ end
103
+
104
+ # gets the users that inherits a subject.
105
+ # domain is an unreferenced parameter here, may be used in other implementations.
106
+ def get_users(name, *domain)
107
+ name = name_by_domain(name, *domain)
108
+ return [] unless has_role(name)
109
+
110
+ all_roles.map do |_key, role|
111
+ next unless role.has_direct_role(name)
112
+
113
+ if domain.size == 1
114
+ role.name[domain[0].size + 2..role.name.size]
115
+ else
116
+ role.name
117
+ end
118
+ end.compact
119
+ end
120
+
121
+ def print_roles
122
+ line = all_roles.map { |_key, role| role.to_string }.compact
123
+ logger.info(line.join(', '))
124
+ end
125
+
126
+ private
127
+
128
+ def names_by_domain(name1, name2, *domain)
129
+ raise 'error: domain should be 1 parameter' if domain.size > 1
130
+
131
+ if domain.size.zero?
132
+ [name1, name2]
133
+ else
134
+ %W[#{domain[0]}::#{name1} #{domain[0]}::#{name2}]
135
+ end
136
+ end
137
+
138
+ def name_by_domain(name, *domain)
139
+ raise 'error: domain should be 1 parameter' if domain.size > 1
140
+
141
+ domain.size == 1 ? "#{domain[0]}::#{name}" : name
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Casbin
4
+ module Rbac
5
+ # provides interface to define the operations for managing roles.
6
+ class RoleManager
7
+ def clear; end
8
+
9
+ def add_link(_name1, _name2, *_domain); end
10
+
11
+ def delete_link(_name1, _name2, *_domain); end
12
+
13
+ def has_link(_name1, _name2, *_domain); end
14
+
15
+ def get_roles(_name, *_domain); end
16
+
17
+ def get_users(_name, *_domain); end
18
+
19
+ def print_roles; end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'casbin-ruby/util/thread_lock'
4
+
5
+ module Casbin
6
+ # SyncedEnforcer wraps Enforcer and provides synchronized access.
7
+ # It's also a drop-in replacement for Enforcer
8
+ class SyncedEnforcer < Enforcer
9
+ # check if SyncedEnforcer is auto loading policies
10
+ def auto_loading_running?
11
+ ThreadLock.lock?
12
+ end
13
+
14
+ # starts a thread that will call load_policy every interval seconds
15
+ def start_auto_load_policy(interval)
16
+ return if auto_loading_running?
17
+
18
+ ThreadLock.thread = Thread.new { auto_load_policy(interval) }
19
+ end
20
+
21
+ # stops the thread started by start_auto_load_policy
22
+ def stop_auto_load_policy
23
+ ThreadLock.thread.exit if auto_loading_running?
24
+ end
25
+
26
+ def build_incremental_role_links(op, ptype, rules)
27
+ model.build_incremental_role_links(role_manager, op, 'g', ptype, rules)
28
+ end
29
+
30
+ private
31
+
32
+ def auto_load_policy(interval)
33
+ while auto_loading_running?
34
+ sleep(interval)
35
+ load_policy
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Casbin
4
+ module Util
5
+ EVAL_REG = /\beval\(([^),]*)\)/.freeze
6
+
7
+ class << self
8
+ # removes the comments starting with # in the text.
9
+ def remove_comments(string)
10
+ string.split('#').first.strip
11
+ end
12
+
13
+ # Escapes the dots in the assertion, because the expression evaluation doesn't support such variable names.
14
+ # Also it replaces attributes with hash syntax (`r.obj.Owner` -> `r_obj['Owner']`,
15
+ # `r.obj.Owner.Position` -> `r_obj['Owner']['Position']`), because Keisan functions work
16
+ # in both regular `f(x)` and postfix `x.f()` notation, where for example `a.f(b,c)` is translated internally
17
+ # to `f(a,b,c)` - https://github.com/project-eutopia/keisan#specifying-functions
18
+ # For now we replace attributes for the request elements like `r.sub`, `r.obj`, `r.act`
19
+ # https://casbin.org/docs/en/abac#how-to-use-abac
20
+ # We support Unicode in attributes for the compatibility with Golang - https://golang.org/ref/spec#Identifiers
21
+ def escape_assertion(string)
22
+ string.gsub(/\br\.(\w+)((?:\.[[:alpha:]_][[:alnum:]_]*)+)/) do |_|
23
+ param = Regexp.last_match(1)
24
+ attrs = Regexp.last_match(2)[1..-1]&.split('.')&.map { |attr| "['#{attr}']" }
25
+ attrs = attrs&.join || ''
26
+ "r_#{param}#{attrs}"
27
+ end.gsub('r.', 'r_').gsub('p.', 'p_')
28
+ end
29
+
30
+ # removes any duplicated elements in a string array.
31
+ def array_remove_duplicates(arr)
32
+ arr.uniq
33
+ end
34
+
35
+ # gets a printable string for a string array.
36
+ def array_to_string(arr)
37
+ arr.join(', ')
38
+ end
39
+
40
+ # gets a printable string for variable number of parameters.
41
+ def params_to_string(*params)
42
+ params.join(', ')
43
+ end
44
+
45
+ # determine whether matcher contains function eval
46
+ def has_eval(string)
47
+ EVAL_REG.match?(string)
48
+ end
49
+
50
+ # replace all occurrences of function eval with rules
51
+ def replace_eval(expr, rules)
52
+ i = -1
53
+ expr.gsub EVAL_REG do |_|
54
+ i += 1
55
+ if rules.is_a? Hash
56
+ "(#{escape_assertion rules[Regexp.last_match 1]})"
57
+ else
58
+ "(#{escape_assertion rules[i]})"
59
+ end
60
+ end
61
+ end
62
+
63
+ # For now, it does not used.
64
+ # Returns the parameters of function eval.
65
+ def get_eval_value(string)
66
+ string.scan(EVAL_REG).flatten
67
+ end
68
+
69
+ # joins a string and a slice into a new slice.
70
+ def join_slice(a, *b)
71
+ Array.new(a).concat b
72
+ end
73
+
74
+ # returns the elements in `a` that aren't in `b`.
75
+ def set_subtract(a, b)
76
+ a - b
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Casbin
4
+ module Util
5
+ module BuiltinOperators
6
+ KEY_MATCH2_PATTERN = %r{:[^/]+}.freeze
7
+ KEY_MATCH3_PATTERN = %r{\{[^/]+\}}.freeze
8
+
9
+ class << self
10
+ # The wrapper for key_match.
11
+ def key_match_func(*args)
12
+ key_match(args[0], args[1])
13
+ end
14
+
15
+ def key_match2_func(*args)
16
+ key_match2(args[0], args[1])
17
+ end
18
+
19
+ def key_match3_func(*args)
20
+ key_match3(args[0], args[1])
21
+ end
22
+
23
+ # the wrapper for RegexMatch.
24
+ def regex_match_func(*args)
25
+ regex_match(args[0], args[1])
26
+ end
27
+
28
+ # the wrapper for globMatch.
29
+ def glob_match_func(*args)
30
+ glob_match(args[0], args[1])
31
+ end
32
+
33
+ # the wrapper for IPMatch.
34
+ def ip_match_func(*args)
35
+ ip_match(args[0], args[1])
36
+ end
37
+
38
+ # determines whether key1 matches the pattern of key2 (similar to RESTful path), key2 can contain a *.
39
+ # For example, "/foo/bar" matches "/foo/
40
+ def key_match(key1, key2)
41
+ i = key2.index('*')
42
+ return key1 == key2 if i.nil?
43
+ return key1[0...i] == key2[0...i] if key1.size > i
44
+
45
+ key1 == key2[0...i]
46
+ end
47
+
48
+ # determines whether key1 matches the pattern of key2 (similar to RESTful path), key2 can contain a *.
49
+ # For example, "/foo/bar" matches "/foo/*", "/resource1" matches "/:resource
50
+ def key_match2(key1, key2)
51
+ key2 = key2.gsub('/*', '/.*')
52
+ key2 = key2.gsub(KEY_MATCH2_PATTERN, '\1[^/]+\2')
53
+ regex_match(key1, "^#{key2}$")
54
+ end
55
+
56
+ # determines determines whether key1 matches the pattern of key2 (similar to RESTful path), key2 can contain
57
+ # a *.
58
+ # For example, "/foo/bar" matches "/foo/*", "/resource1" matches "/{resource}"
59
+ def key_match3(key1, key2)
60
+ key2 = key2.gsub('/*', '/.*')
61
+ key2 = key2.gsub(KEY_MATCH3_PATTERN, '\1[^\/]+\2')
62
+ regex_match(key1, "^#{key2}$")
63
+ end
64
+
65
+ # determines whether key1 matches the pattern of key2 in regular expression.
66
+ def regex_match(key1, key2)
67
+ (key1 =~ /#{key2}/)&.zero? || false
68
+ end
69
+
70
+ # determines whether string matches the pattern in glob expression.
71
+ def glob_match(string, pattern)
72
+ File.fnmatch(pattern, string, File::FNM_PATHNAME)
73
+ end
74
+
75
+ # IPMatch determines whether IP address ip1 matches the pattern of IP address ip2, ip2 can be an IP address or
76
+ # a CIDR pattern.
77
+ # For example, "192.168.2.123" matches "192.168.2.0/24
78
+ def ip_match(ip1, ip2)
79
+ ip = IPAddr.new(ip1)
80
+ network = IPAddr.new(ip2)
81
+ network.include?(ip)
82
+ rescue IPAddr::InvalidAddressError
83
+ ip1 == ip2
84
+ end
85
+
86
+ # the factory method of the g(_, _) function.
87
+ def generate_g_function(rm)
88
+ return ->(*args) { args[0] == args[1] } unless rm
89
+
90
+ lambda do |*args|
91
+ name1 = args[0]
92
+ name2 = args[1]
93
+
94
+ if args.length == 2
95
+ rm.has_link(name1, name2)
96
+ else
97
+ domain = args[2].to_s
98
+ rm.has_link(name1, name2, domain)
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'keisan'
4
+
5
+ module Casbin
6
+ module Util
7
+ class Evaluator
8
+ class NamesConflictError < StandardError; end
9
+
10
+ class << self
11
+ # evaluate an expression, using the operators, functions and names previously setup.
12
+ def eval(expr, funcs = {}, params = {})
13
+ validate_names funcs, params
14
+ Keisan::Calculator.new.evaluate expr, funcs.merge(params)
15
+ end
16
+
17
+ def validate_names(funcs = {}, params = {})
18
+ conflicted_names = funcs.keys & params.keys
19
+ return if conflicted_names.empty?
20
+
21
+ raise NamesConflictError, "You can't use function names as parameter names: " \
22
+ "#{conflicted_names.map { |name| "`#{name}`" }.join ', '}"
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ class ThreadLock
6
+ include Singleton
7
+
8
+ class << self
9
+ delegate :thread=, :lock?, to: :instance
10
+ end
11
+
12
+ attr_accessor :thread
13
+
14
+ def lock?
15
+ return false unless thread
16
+
17
+ thread.active?
18
+ end
19
+ end