admission 0.1.6
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 +7 -0
- data/.gitignore +37 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/Gemfile +6 -0
- data/LICENSE +674 -0
- data/README.md +2 -0
- data/admission.gemspec +18 -0
- data/bin/rspec +8 -0
- data/lib/admission.rb +9 -0
- data/lib/admission/ability.rb +140 -0
- data/lib/admission/admission.rb +6 -0
- data/lib/admission/arbitration.rb +126 -0
- data/lib/admission/denied.rb +15 -0
- data/lib/admission/privilege.rb +129 -0
- data/lib/admission/rails.rb +94 -0
- data/lib/admission/resource_arbitration.rb +112 -0
- data/lib/admission/status.rb +38 -0
- data/lib/admission/version.rb +3 -0
- data/spec/integration/_helper.rb +2 -0
- data/spec/integration/action_arbitrating_spec.rb +119 -0
- data/spec/integration/resource_arbitrating_spec.rb +246 -0
- data/spec/rspec_config.rb +103 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/test_context/country.rb +24 -0
- data/spec/test_context/index.rb +7 -0
- data/spec/test_context/person.rb +31 -0
- data/spec/test_context/persons_fixtures.rb +43 -0
- data/spec/test_context/privileges_and_rules.rb +114 -0
- data/spec/unit/_helper.rb +1 -0
- data/spec/unit/ability_spec.rb +29 -0
- data/spec/unit/privilege/order_definer_spec.rb +184 -0
- data/spec/unit/privilege_spec.rb +146 -0
- data/spec/unit/request_arbitration_spec.rb +31 -0
- metadata +76 -0
data/README.md
ADDED
data/admission.gemspec
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative 'lib/admission/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
|
5
|
+
spec.name = 'admission'
|
6
|
+
spec.version = Admission::VERSION
|
7
|
+
spec.summary = 'admission library'
|
8
|
+
spec.license = 'GPL-3.0'
|
9
|
+
|
10
|
+
spec.homepage = 'https://github.com/doooby/admission'
|
11
|
+
spec.author = 'doooby'
|
12
|
+
spec.description = 'update-me'
|
13
|
+
spec.email = 'zelazk.o@email.cz'
|
14
|
+
|
15
|
+
library_files = (`git ls-files -z`).split "\x0"
|
16
|
+
spec.files = library_files
|
17
|
+
|
18
|
+
end
|
data/bin/rspec
ADDED
data/lib/admission.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
require_relative 'admission/admission'
|
2
|
+
require_relative 'admission/version'
|
3
|
+
|
4
|
+
require_relative 'admission/privilege'
|
5
|
+
require_relative 'admission/status'
|
6
|
+
require_relative 'admission/arbitration'
|
7
|
+
require_relative 'admission/resource_arbitration'
|
8
|
+
|
9
|
+
require_relative 'admission/denied'
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# class Admission::Ability
|
2
|
+
#
|
3
|
+
# attr_reader :status
|
4
|
+
#
|
5
|
+
# def initialize status, arbiter=Admission::RequestArbitration
|
6
|
+
# @status = status
|
7
|
+
# @no_privileges = @status.privileges.nil? || @status.privileges.empty?
|
8
|
+
# @arbiter = arbiter
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# # def can? *agrs
|
12
|
+
# # return false if @no_privileges
|
13
|
+
# # # subject, object = subject_and_object subject
|
14
|
+
# # process @arbiter.new(status)
|
15
|
+
# # process Arbitration.new(status.user, action), subject, object
|
16
|
+
# # end
|
17
|
+
#
|
18
|
+
# def cannot? *args
|
19
|
+
# !can?(*args)
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# class << self
|
23
|
+
#
|
24
|
+
# # attr_reader :rules_index
|
25
|
+
#
|
26
|
+
# # def define_for_privileges privileges, &block
|
27
|
+
# # end
|
28
|
+
#
|
29
|
+
# # def set_up_rules &block
|
30
|
+
# # @rules_index = []
|
31
|
+
# # self.instance_exec &block
|
32
|
+
# # remove_instance_variable :@privilege
|
33
|
+
# #
|
34
|
+
# # @rules_index = @rules_index.reduce Hash.new do |index, allowance|
|
35
|
+
# # privilege = allowance[:privilege]
|
36
|
+
# #
|
37
|
+
# # actions = allowance[:actions]
|
38
|
+
# # *actions = actions unless Array === actions
|
39
|
+
# # actions = %i[all] if actions.include? :all
|
40
|
+
# #
|
41
|
+
# # subject_index = (index[allowance[:subject]] ||= {})
|
42
|
+
# # resource_index = (index[allowance[:resource_type]] ||= {} if allowance[:resource_type])
|
43
|
+
# #
|
44
|
+
# # actions.each do |action|
|
45
|
+
# # subject_arbiter = allowance[:arbiter]
|
46
|
+
# #
|
47
|
+
# # if resource_index
|
48
|
+
# # action_index = (resource_index[action] ||= {})
|
49
|
+
# # action_index[privilege] = allowance[:arbiter]
|
50
|
+
# #
|
51
|
+
# # subject_arbiter = true
|
52
|
+
# # end
|
53
|
+
# #
|
54
|
+
# # action_index = (subject_index[action] ||= {})
|
55
|
+
# # action_index[privilege] = subject_arbiter
|
56
|
+
# # end
|
57
|
+
# #
|
58
|
+
# # index
|
59
|
+
# # end
|
60
|
+
# # end
|
61
|
+
#
|
62
|
+
# # def privilege name, level=nil, &block
|
63
|
+
# # @rules_index || raise('must be called within `set_rules` block')
|
64
|
+
# # @privilege = Admission.get_privilege(name, level) || raise("no such privilege: #{name}-#{level}")
|
65
|
+
# # self.instance_exec &block
|
66
|
+
# # @privilege = nil
|
67
|
+
# # end
|
68
|
+
# #
|
69
|
+
# # def allow subject, actions=:all
|
70
|
+
# # raise 'must be called within `privilege` block' unless @privilege
|
71
|
+
# # @rules_index << {
|
72
|
+
# # privilege: @privilege,
|
73
|
+
# # subject: subject,
|
74
|
+
# # actions: actions,
|
75
|
+
# # arbiter: true
|
76
|
+
# # }
|
77
|
+
# # end
|
78
|
+
# #
|
79
|
+
# # def forbid subject, actions=:all
|
80
|
+
# # raise 'must be called within `privilege` block' unless @privilege
|
81
|
+
# # @rules_index << {
|
82
|
+
# # privilege: @privilege,
|
83
|
+
# # subject: subject,
|
84
|
+
# # actions: actions,
|
85
|
+
# # arbiter: :forbid
|
86
|
+
# # }
|
87
|
+
# # end
|
88
|
+
# #
|
89
|
+
# # def allow_resource type, actions=:all, lambda=nil, &block
|
90
|
+
# # raise 'must be called within `privilege` block' unless @privilege
|
91
|
+
# # @rules_index << {
|
92
|
+
# # privilege: @privilege,
|
93
|
+
# # subject: resource_type_to_subject(type),
|
94
|
+
# # resource_type: type,
|
95
|
+
# # actions: actions,
|
96
|
+
# # arbiter: lambda || block || true
|
97
|
+
# # }
|
98
|
+
# # end
|
99
|
+
#
|
100
|
+
# private
|
101
|
+
#
|
102
|
+
# # def resource_type_to_subject type
|
103
|
+
# # raise "bad resource type, must respond to `#name` (should be class)" unless type.respond_to? :name
|
104
|
+
# # name = type.name.downcase
|
105
|
+
# # if name.respond_to? :pluralize
|
106
|
+
# # name.pluralize
|
107
|
+
# # else
|
108
|
+
# # raise 'Not implemented: unable to make subject id from resource without active_support'
|
109
|
+
# # end
|
110
|
+
# #
|
111
|
+
# # end
|
112
|
+
#
|
113
|
+
# end
|
114
|
+
#
|
115
|
+
# private
|
116
|
+
#
|
117
|
+
# # def subject_and_object object
|
118
|
+
# # Symbol === object ? object : [objec.class, object]
|
119
|
+
# # end
|
120
|
+
#
|
121
|
+
# # def process arbitration, subject, object
|
122
|
+
# # all_index = self.class.rules_index[:all]
|
123
|
+
# # subject_index = self.class.rules_index[subject]
|
124
|
+
# # type_index = (self.class.rules_index[object] if object)
|
125
|
+
# # arbitration.introduce_indices all_index, subject_index, type_index
|
126
|
+
# #
|
127
|
+
# # status.privileges.any? do |privilege|
|
128
|
+
# # arbitration.prepare_sitting object, *privilege.context
|
129
|
+
# # TrueClass === arbitration.rule(privilege)
|
130
|
+
# # end
|
131
|
+
# # end
|
132
|
+
#
|
133
|
+
# def process arbitration
|
134
|
+
# status.privileges.any? do |privilege|
|
135
|
+
# arbitration.prepare_sitting object, *privilege.context
|
136
|
+
# arbitration.rule_per_privilege(privilege).eql? true
|
137
|
+
# end
|
138
|
+
# end
|
139
|
+
#
|
140
|
+
# end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
class Admission::Arbitration
|
2
|
+
|
3
|
+
|
4
|
+
def initialize person, rules_index, request
|
5
|
+
@person = person
|
6
|
+
@rules_index = rules_index
|
7
|
+
@request = request.to_sym
|
8
|
+
end
|
9
|
+
|
10
|
+
def prepare_sitting *context
|
11
|
+
@context = context
|
12
|
+
@decisions = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def rule_per_privilege privilege
|
16
|
+
decision = @decisions[privilege]
|
17
|
+
return decision unless decision.nil?
|
18
|
+
|
19
|
+
decision = decide privilege
|
20
|
+
|
21
|
+
decision = false if decision.nil?
|
22
|
+
@decisions[privilege] = decision
|
23
|
+
end
|
24
|
+
|
25
|
+
def make_decision from_rules, privilege
|
26
|
+
if from_rules
|
27
|
+
decision = from_rules[privilege]
|
28
|
+
decision = @person.instance_exec *@context, &decision if Proc === decision
|
29
|
+
|
30
|
+
unless Admission::VALID_DECISION.include? decision
|
31
|
+
raise "invalid decision: #{decision}"
|
32
|
+
end
|
33
|
+
|
34
|
+
decision
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def decide_per_inheritance privilege
|
39
|
+
inherited = privilege.inherited
|
40
|
+
return nil if inherited.nil? || inherited.empty?
|
41
|
+
|
42
|
+
explicit_allowance = false
|
43
|
+
inherited.each do |p|
|
44
|
+
rule = rule_per_privilege p
|
45
|
+
return rule if rule == :forbidden
|
46
|
+
explicit_allowance ||= rule
|
47
|
+
end
|
48
|
+
explicit_allowance
|
49
|
+
end
|
50
|
+
|
51
|
+
def decide privilege
|
52
|
+
decision = make_decision @rules_index[@request], privilege
|
53
|
+
return decision if decision.eql?(:forbidden) || decision.eql?(true)
|
54
|
+
|
55
|
+
decision = decide_per_inheritance privilege
|
56
|
+
return decision if decision.eql?(:forbidden) || decision.eql?(true)
|
57
|
+
|
58
|
+
make_decision @rules_index[Admission::ALL_ACTION], privilege
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.define_rules privilege_order, &block
|
62
|
+
builder = self::RulesBuilder.new privilege_order
|
63
|
+
builder.instance_exec &block
|
64
|
+
builder.create_index
|
65
|
+
end
|
66
|
+
|
67
|
+
class RulesBuilder
|
68
|
+
|
69
|
+
attr_reader :privilege_order
|
70
|
+
|
71
|
+
def initialize privilege_order
|
72
|
+
@rules = []
|
73
|
+
@privilege_order = privilege_order
|
74
|
+
end
|
75
|
+
|
76
|
+
def privilege name, level=nil
|
77
|
+
@privilege = Admission::Privilege.get_from_order privilege_order, name, level
|
78
|
+
raise "no such privilege: #{name}-#{level}" unless @privilege
|
79
|
+
yield
|
80
|
+
@privilege = nil
|
81
|
+
end
|
82
|
+
|
83
|
+
def allow *actions, &block
|
84
|
+
raise "reserved action name #{Admission::ALL_ACTION}" if actions.include? Admission::ALL_ACTION
|
85
|
+
add_allowance_rule actions.flatten, (block || true)
|
86
|
+
end
|
87
|
+
|
88
|
+
def allow_all &block
|
89
|
+
add_allowance_rule [Admission::ALL_ACTION], (block || true)
|
90
|
+
end
|
91
|
+
|
92
|
+
def forbid *actions
|
93
|
+
raise "reserved action name #{Admission::ALL_ACTION}" if actions.include? Admission::ALL_ACTION
|
94
|
+
add_allowance_rule actions.flatten, :forbidden
|
95
|
+
end
|
96
|
+
|
97
|
+
def add_allowance_rule actions, arbiter, **options
|
98
|
+
raise 'must be called within `privilege` block' unless @privilege
|
99
|
+
|
100
|
+
@rules << options.merge!(
|
101
|
+
privilege: @privilege,
|
102
|
+
actions: actions,
|
103
|
+
arbiter: arbiter
|
104
|
+
)
|
105
|
+
end
|
106
|
+
|
107
|
+
def create_index
|
108
|
+
index_instance = @rules.reduce Hash.new do |index, allowance|
|
109
|
+
privilege = allowance[:privilege]
|
110
|
+
actions = allowance[:actions]
|
111
|
+
arbiter = allowance[:arbiter]
|
112
|
+
|
113
|
+
actions.each do |action|
|
114
|
+
action_index = (index[action] ||= {})
|
115
|
+
action_index[privilege] = arbiter
|
116
|
+
end
|
117
|
+
|
118
|
+
index
|
119
|
+
end
|
120
|
+
|
121
|
+
index_instance.freeze
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Admission::Denied < ::StandardError
|
2
|
+
|
3
|
+
attr_reader :status, :request_args
|
4
|
+
attr_accessor :message
|
5
|
+
|
6
|
+
def initialize status, *request_args
|
7
|
+
@status = status
|
8
|
+
@request_args = request_args
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
@message || 'Admission denied.'
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
class Admission::Privilege
|
2
|
+
|
3
|
+
RESERVED_ID = :'^'
|
4
|
+
TOP_LEVEL_KEY = RESERVED_ID
|
5
|
+
BASE_LEVEL_NAME = :base
|
6
|
+
|
7
|
+
attr_reader :name, :level, :hash
|
8
|
+
attr_reader :inherited, :context
|
9
|
+
|
10
|
+
def initialize name, level=nil
|
11
|
+
name = name.to_sym
|
12
|
+
@name = name
|
13
|
+
level = level ? level.to_sym : BASE_LEVEL_NAME
|
14
|
+
@level = level
|
15
|
+
@hash = [name, level].hash
|
16
|
+
end
|
17
|
+
|
18
|
+
def inherits_from *privileges
|
19
|
+
@inherited = privileges
|
20
|
+
end
|
21
|
+
|
22
|
+
def dup_with_context *context
|
23
|
+
context = context.flatten.compact
|
24
|
+
return self if context.empty?
|
25
|
+
with_context = dup
|
26
|
+
with_context.instance_variable_set :@context, context
|
27
|
+
with_context
|
28
|
+
end
|
29
|
+
|
30
|
+
def eql? other
|
31
|
+
hash == other.hash
|
32
|
+
end
|
33
|
+
|
34
|
+
def text_key
|
35
|
+
level == BASE_LEVEL_NAME ? name.to_s : "#{name}-#{level}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_s
|
39
|
+
"<#{[
|
40
|
+
'Privilege',
|
41
|
+
"key=#{text_key}",
|
42
|
+
(inherited && "inherited=[#{inherited.map(&:text_key).join ','}]")
|
43
|
+
].compact.join ' '}>"
|
44
|
+
end
|
45
|
+
alias :inspect :to_s
|
46
|
+
|
47
|
+
def self.define_order &block
|
48
|
+
Admission::Privilege::OrderDefiner.define &block
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.get_from_order index, name, level=nil
|
52
|
+
levels = index[name.to_sym] || return
|
53
|
+
if level && !level.empty?
|
54
|
+
levels[level.to_sym]
|
55
|
+
else
|
56
|
+
levels[Admission::Privilege::BASE_LEVEL_NAME]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class OrderDefiner
|
61
|
+
|
62
|
+
attr_reader :definitions
|
63
|
+
|
64
|
+
def initialize
|
65
|
+
@definitions = {}
|
66
|
+
end
|
67
|
+
|
68
|
+
def privilege name, levels: [], inherits: nil
|
69
|
+
name = name.to_sym
|
70
|
+
if ([name] + levels).any?{|id| id == Admission::Privilege::RESERVED_ID }
|
71
|
+
raise "reserved name `#{Admission::Privilege::RESERVED_ID}` !"
|
72
|
+
end
|
73
|
+
|
74
|
+
levels.unshift Admission::Privilege::BASE_LEVEL_NAME
|
75
|
+
levels.map!{|level| Admission::Privilege.new name, level}
|
76
|
+
|
77
|
+
inherits = nil if inherits && inherits.empty?
|
78
|
+
if inherits
|
79
|
+
inherits = *inherits
|
80
|
+
inherits = inherits.map(&:to_sym).uniq
|
81
|
+
end
|
82
|
+
|
83
|
+
@definitions[name] = {levels: levels, inherits: inherits}
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.define &block
|
87
|
+
definer = new
|
88
|
+
definer.instance_exec &block
|
89
|
+
|
90
|
+
definer.send :setup_inheritance
|
91
|
+
definer.send :build_index
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def setup_inheritance
|
97
|
+
# set inheritance for all privileges
|
98
|
+
definitions.values.each do |levels:, inherits:|
|
99
|
+
levels.each_with_index do |privilege, index|
|
100
|
+
if index > 0 # higher level of privilege, inherits one step lower level
|
101
|
+
privilege.inherits_from levels[index - 1]
|
102
|
+
|
103
|
+
elsif inherits # lowest level, inherits top level of other privileges
|
104
|
+
inherits = inherits.map{|name| definitions[name][:levels].last if definitions.has_key? name}
|
105
|
+
privilege.inherits_from *inherits
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def build_index
|
113
|
+
definitions.each_pair.reduce({}) do |h, pair|
|
114
|
+
name = pair[0]
|
115
|
+
levels = pair[1][:levels]
|
116
|
+
|
117
|
+
levels_hash = levels.reduce({Admission::Privilege::TOP_LEVEL_KEY => levels.last}) do |lh, privilege|
|
118
|
+
lh[privilege.level] = privilege
|
119
|
+
lh
|
120
|
+
end.freeze
|
121
|
+
|
122
|
+
h[name] = levels_hash
|
123
|
+
h
|
124
|
+
end.freeze
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|