casbin-ruby 1.0.3 → 1.0.4

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