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.
- checksums.yaml +4 -4
- data/README.md +242 -0
- data/lib/casbin-ruby.rb +11 -0
- data/lib/casbin-ruby/config/config.rb +115 -0
- data/lib/casbin-ruby/core_enforcer.rb +356 -0
- data/lib/casbin-ruby/effect/allow_and_deny_effector.rb +23 -0
- data/lib/casbin-ruby/effect/allow_override_effector.rb +23 -0
- data/lib/casbin-ruby/effect/default_effector.rb +37 -0
- data/lib/casbin-ruby/effect/deny_override_effector.rb +23 -0
- data/lib/casbin-ruby/effect/effector.rb +18 -0
- data/lib/casbin-ruby/effect/priority_effector.rb +25 -0
- data/lib/casbin-ruby/enforcer.rb +189 -0
- data/lib/casbin-ruby/internal_enforcer.rb +73 -0
- data/lib/casbin-ruby/management_enforcer.rb +297 -0
- data/lib/casbin-ruby/model/assertion.rb +33 -0
- data/lib/casbin-ruby/model/function_map.rb +30 -0
- data/lib/casbin-ruby/model/model.rb +80 -0
- data/lib/casbin-ruby/model/policy.rb +161 -0
- data/lib/casbin-ruby/persist/adapter.rb +39 -0
- data/lib/casbin-ruby/persist/adapters/file_adapter.rb +53 -0
- data/lib/casbin-ruby/persist/batch_adapter.rb +16 -0
- data/lib/casbin-ruby/persist/filtered_adapter.rb +17 -0
- data/lib/casbin-ruby/rbac/default_role_manager/role.rb +54 -0
- data/lib/casbin-ruby/rbac/default_role_manager/role_manager.rb +146 -0
- data/lib/casbin-ruby/rbac/role_manager.rb +22 -0
- data/lib/casbin-ruby/synced_enforcer.rb +39 -0
- data/lib/casbin-ruby/util.rb +80 -0
- data/lib/casbin-ruby/util/builtin_operators.rb +105 -0
- data/lib/casbin-ruby/util/evaluator.rb +27 -0
- data/lib/casbin-ruby/util/thread_lock.rb +19 -0
- data/lib/casbin-ruby/version.rb +5 -0
- data/spec/casbin/config/config_spec.rb +66 -0
- data/spec/casbin/core_enforcer_spec.rb +473 -0
- data/spec/casbin/enforcer_spec.rb +302 -0
- data/spec/casbin/model/function_map_spec.rb +28 -0
- data/spec/casbin/rbac/default_role_manager/role_manager_spec.rb +131 -0
- data/spec/casbin/rbac/default_role_manager/role_spec.rb +84 -0
- data/spec/casbin/util/builtin_operators_spec.rb +205 -0
- data/spec/casbin/util_spec.rb +98 -0
- data/spec/support/model_helper.rb +9 -0
- 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
|