authorization_next 0.1.0

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.
@@ -0,0 +1,118 @@
1
+ require File.dirname(__FILE__) + '/exceptions'
2
+ require File.dirname(__FILE__) + '/identity'
3
+
4
+ module AuthorizationNext
5
+ module ObjectRolesTable
6
+
7
+ module UserExtensions
8
+ def self.included( recipient )
9
+ recipient.extend( ClassMethods )
10
+ end
11
+
12
+ module ClassMethods
13
+ def acts_as_authorized_user
14
+ has_and_belongs_to_many :roles
15
+ attr_protected :role_ids
16
+ include AuthorizationNext::ObjectRolesTable::UserExtensions::InstanceMethods
17
+ include AuthorizationNext::Identity::UserExtensions::InstanceMethods # Provides all kinds of dynamic sugar via method_missing
18
+ end
19
+ end
20
+
21
+ module InstanceMethods
22
+ # If roles aren't explicitly defined in user class then check roles table
23
+ def has_role?( role_name, authorizable_obj = nil )
24
+ if authorizable_obj.nil?
25
+ self.roles.find_by_name( role_name ) ? true : false # If we ask a general role question, return true if any role is defined.
26
+ else
27
+ role = get_role( role_name, authorizable_obj )
28
+ role ? self.roles.exists?( role.id ) : false
29
+ end
30
+ end
31
+
32
+ def has_role( role_name, authorizable_obj = nil )
33
+ role = get_role( role_name, authorizable_obj )
34
+ if role.nil?
35
+ if authorizable_obj.is_a? Class
36
+ role = Role.create( :name => role_name, :authorizable_type => authorizable_obj.to_s )
37
+ elsif authorizable_obj
38
+ role = Role.create( :name => role_name, :authorizable => authorizable_obj )
39
+ else
40
+ role = Role.create( :name => role_name )
41
+ end
42
+ end
43
+ self.roles << role if role and not self.roles.exists?( role.id )
44
+ end
45
+
46
+ def has_no_role( role_name, authorizable_obj = nil )
47
+ role = get_role( role_name, authorizable_obj )
48
+ if role
49
+ self.roles.delete( role )
50
+ role.destroy if role.users.empty?
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def get_role( role_name, authorizable_obj )
57
+ if authorizable_obj.is_a? Class
58
+ Role.find( :first,
59
+ :conditions => [ 'name = ? and authorizable_type = ? and authorizable_id IS NULL', role_name, authorizable_obj.to_s ] )
60
+ elsif authorizable_obj
61
+ Role.find( :first,
62
+ :conditions => [ 'name = ? and authorizable_type = ? and authorizable_id = ?',
63
+ role_name, authorizable_obj.class.base_class.to_s, authorizable_obj.id ] )
64
+ else
65
+ Role.find( :first,
66
+ :conditions => [ 'name = ? and authorizable_type IS NULL and authorizable_id IS NULL', role_name ] )
67
+ end
68
+ end
69
+
70
+ end
71
+ end
72
+
73
+ module ModelExtensions
74
+ def self.included( recipient )
75
+ recipient.extend( ClassMethods )
76
+ end
77
+
78
+ module ClassMethods
79
+ def acts_as_authorizable
80
+ has_many :accepted_roles, :as => :authorizable, :class_name => 'Role'
81
+ attr_protected :accepted_role_ids
82
+
83
+ def accepts_role?( role_name, user )
84
+ user.has_role? role_name, self
85
+ end
86
+
87
+ def accepts_role( role_name, user )
88
+ user.has_role role_name, self
89
+ end
90
+
91
+ def accepts_no_role( role_name, user )
92
+ user.has_no_role role_name, self
93
+ end
94
+
95
+ include AuthorizationNext::ObjectRolesTable::ModelExtensions::InstanceMethods
96
+ include AuthorizationNext::Identity::ModelExtensions::InstanceMethods # Provides all kinds of dynamic sugar via method_missing
97
+ end
98
+ end
99
+
100
+ module InstanceMethods
101
+ # If roles aren't overriden in model then check roles table
102
+ def accepts_role?( role_name, user )
103
+ user.has_role? role_name, self
104
+ end
105
+
106
+ def accepts_role( role_name, user )
107
+ user.has_role role_name, self
108
+ end
109
+
110
+ def accepts_no_role( role_name, user )
111
+ user.has_no_role role_name, self
112
+ end
113
+ end
114
+ end
115
+
116
+ end
117
+ end
118
+
@@ -0,0 +1,210 @@
1
+ module AuthorizationNext
2
+ module Base
3
+
4
+ VALID_PREPOSITIONS = ['of', 'for', 'in', 'on', 'to', 'at', 'by']
5
+ BOOLEAN_OPS = ['not', 'or', 'and']
6
+ VALID_PREPOSITIONS_PATTERN = VALID_PREPOSITIONS.join('|')
7
+
8
+ module EvalParser
9
+ # Parses and evaluates an authorization expression and returns <tt>true</tt> or <tt>false</tt>.
10
+ #
11
+ # The authorization expression is defined by the following grammar:
12
+ # <expr> ::= (<expr>) | not <expr> | <term> or <expr> | <term> and <expr> | <term>
13
+ # <term> ::= <role> | <role> <preposition> <model>
14
+ # <preposition> ::= of | for | in | on | to | at | by
15
+ # <model> ::= /:*\w+/
16
+ # <role> ::= /\w+/ | /'.*'/
17
+ #
18
+ # Instead of doing recursive descent parsing (not so fun when we support nested parentheses, etc),
19
+ # we let Ruby do the work for us by inserting the appropriate permission calls and using eval.
20
+ # This would not be a good idea if you were getting authorization expressions from the outside,
21
+ # so in that case (e.g. somehow letting users literally type in permission expressions) you'd
22
+ # be better off using the recursive descent parser in Module RecursiveDescentParser.
23
+ #
24
+ # We search for parts of our authorization evaluation that match <role> or <role> <preposition> <model>
25
+ # and we ignore anything terminal in our grammar.
26
+ #
27
+ # 1) Replace all <role> <preposition> <model> matches.
28
+ # 2) Replace all <role> matches that aren't one of our other terminals ('not', 'or', 'and', or preposition)
29
+ # 3) Eval
30
+
31
+ def parse_authorization_expression( str )
32
+ if str =~ /[^A-Za-z0-9_:'\(\)\s]/
33
+ raise AuthorizationExpressionInvalid, "Invalid authorization expression (#{str})"
34
+ return false
35
+ end
36
+ @replacements = []
37
+ expr = replace_temporarily_role_of_model( str )
38
+ expr = replace_role( expr )
39
+ expr = replace_role_of_model( expr )
40
+ begin
41
+ instance_eval( expr )
42
+ rescue
43
+ raise AuthorizationExpressionInvalid, "Cannot parse authorization (#{str})"
44
+ end
45
+ end
46
+
47
+ def replace_temporarily_role_of_model( str )
48
+ role_regex = '\s*(\'\s*(.+)\s*\'|(\w+))\s+'
49
+ model_regex = '\s+(:*\w+)'
50
+ parse_regex = Regexp.new(role_regex + '(' + VALID_PREPOSITIONS.join('|') + ')' + model_regex)
51
+ str.gsub(parse_regex) do |match|
52
+ @replacements.push " process_role_of_model('#{$2 || $3}', '#{$5}') "
53
+ " <#{@replacements.length-1}> "
54
+ end
55
+ end
56
+
57
+ def replace_role( str )
58
+ role_regex = '\s*(\'\s*(.+)\s*\'|([A-Za-z]\w*))\s*'
59
+ parse_regex = Regexp.new(role_regex)
60
+ str.gsub(parse_regex) do |match|
61
+ if BOOLEAN_OPS.include?($3)
62
+ " #{match} "
63
+ else
64
+ " process_role('#{$2 || $3}') "
65
+ end
66
+ end
67
+ end
68
+
69
+ def replace_role_of_model( str )
70
+ str.gsub(/<(\d+)>/) do |match|
71
+ @replacements[$1.to_i]
72
+ end
73
+ end
74
+
75
+ def process_role_of_model( role_name, model_name )
76
+ model = get_model( model_name )
77
+ raise( ModelDoesntImplementRoles, "Model (#{model_name}) doesn't implement #accepts_role?" ) if not model.respond_to? :accepts_role?
78
+ model.send( :accepts_role?, role_name, @current_user )
79
+ end
80
+
81
+ def process_role( role_name )
82
+ return false if @current_user.nil?
83
+ raise( UserDoesntImplementRoles, "User doesn't implement #has_role?" ) if not @current_user.respond_to? :has_role?
84
+ @current_user.has_role?( role_name )
85
+ end
86
+
87
+ end
88
+
89
+ # Parses and evaluates an authorization expression and returns <tt>true</tt> or <tt>false</tt>.
90
+ # This recursive descent parses uses two instance variables:
91
+ # @stack --> a stack with the top holding the boolean expression resulting from the parsing
92
+ #
93
+ # The authorization expression is defined by the following grammar:
94
+ # <expr> ::= (<expr>) | not <expr> | <term> or <expr> | <term> and <expr> | <term>
95
+ # <term> ::= <role> | <role> <preposition> <model>
96
+ # <preposition> ::= of | for | in | on | to | at | by
97
+ # <model> ::= /:*\w+/
98
+ # <role> ::= /\w+/ | /'.*'/
99
+ #
100
+ # There are really two values we must track:
101
+ # (1) whether the expression is valid according to the grammar
102
+ # (2) the evaluated results --> true/false on the permission queries
103
+ # The first is embedded in the control logic because we want short-circuiting. If an expression
104
+ # has been parsed and the permission is false, we don't want to try different ways of parsing.
105
+ # Note that this implementation of a recursive descent parser is meant to be simple
106
+ # and doesn't allow arbitrary nesting of parentheses. It supports up to 5 levels of nesting.
107
+ # It also won't handle some types of expressions (A or B) and C, which has to be rewritten as
108
+ # C and (A or B) so the parenthetical expressions are in the tail.
109
+ module RecursiveDescentParser
110
+
111
+ OPT_PARENTHESES_PATTERN = '(([^()]|\(([^()]|\(([^()]|\(([^()]|\(([^()]|\(([^()])*\))*\))*\))*\))*\))*)'
112
+ PARENTHESES_PATTERN = '\(' + OPT_PARENTHESES_PATTERN + '\)'
113
+ NOT_PATTERN = '^\s*not\s+' + OPT_PARENTHESES_PATTERN + '$'
114
+ AND_PATTERN = '^\s*' + OPT_PARENTHESES_PATTERN + '\s+and\s+' + OPT_PARENTHESES_PATTERN + '\s*$'
115
+ OR_PATTERN = '^\s*' + OPT_PARENTHESES_PATTERN + '\s+or\s+' + OPT_PARENTHESES_PATTERN + '\s*$'
116
+ ROLE_PATTERN = '(\'\s*(.+)\s*\'|(\w+))'
117
+ MODEL_PATTERN = '(:*\w+)'
118
+
119
+ PARENTHESES_REGEX = Regexp.new('^\s*' + PARENTHESES_PATTERN + '\s*$')
120
+ NOT_REGEX = Regexp.new(NOT_PATTERN)
121
+ AND_REGEX = Regexp.new(AND_PATTERN)
122
+ OR_REGEX = Regexp.new(OR_PATTERN)
123
+ ROLE_REGEX = Regexp.new('^\s*' + ROLE_PATTERN + '\s*$')
124
+ ROLE_OF_MODEL_REGEX = Regexp.new('^\s*' + ROLE_PATTERN + '\s+(' + VALID_PREPOSITIONS_PATTERN + ')\s+' + MODEL_PATTERN + '\s*$')
125
+
126
+ def parse_authorization_expression( str )
127
+ @stack = []
128
+ raise AuthorizationExpressionInvalid, "Cannot parse authorization (#{str})" if not parse_expr( str )
129
+ return @stack.pop
130
+ end
131
+
132
+ def parse_expr( str )
133
+ parse_parenthesis( str ) or
134
+ parse_not( str ) or
135
+ parse_or( str ) or
136
+ parse_and( str ) or
137
+ parse_term( str )
138
+ end
139
+
140
+ def parse_not( str )
141
+ if str =~ NOT_REGEX
142
+ can_parse = parse_expr( $1 )
143
+ @stack.push( !@stack.pop ) if can_parse
144
+ end
145
+ false
146
+ end
147
+
148
+ def parse_or( str )
149
+ if str =~ OR_REGEX
150
+ can_parse = parse_expr( $1 ) and parse_expr( $8 )
151
+ @stack.push( @stack.pop | @stack.pop ) if can_parse
152
+ return can_parse
153
+ end
154
+ false
155
+ end
156
+
157
+ def parse_and( str )
158
+ if str =~ AND_REGEX
159
+ can_parse = parse_expr( $1 ) and parse_expr( $8 )
160
+ @stack.push(@stack.pop & @stack.pop) if can_parse
161
+ return can_parse
162
+ end
163
+ false
164
+ end
165
+
166
+ # Descend down parenthesis (allow up to 5 levels of nesting)
167
+ def parse_parenthesis( str )
168
+ str =~ PARENTHESES_REGEX ? parse_expr( $1 ) : false
169
+ end
170
+
171
+ def parse_term( str )
172
+ parse_role_of_model( str ) or
173
+ parse_role( str )
174
+ end
175
+
176
+ # Parse <role> of <model>
177
+ def parse_role_of_model( str )
178
+ if str =~ ROLE_OF_MODEL_REGEX
179
+ role_name = $2 || $3
180
+ model_name = $5
181
+ model_obj = get_model( model_name )
182
+ raise( ModelDoesntImplementRoles, "Model (#{model_name}) doesn't implement #accepts_role?" ) if not model_obj.respond_to? :accepts_role?
183
+
184
+ has_permission = model_obj.send( :accepts_role?, role_name, @current_user )
185
+ @stack.push( has_permission )
186
+ true
187
+ else
188
+ false
189
+ end
190
+ end
191
+
192
+ # Parse <role> of the User-like object
193
+ def parse_role( str )
194
+ if str =~ ROLE_REGEX
195
+ role_name = $1
196
+ if @current_user.nil?
197
+ @stack.push(false)
198
+ else
199
+ raise( UserDoesntImplementRoles, "User doesn't implement #has_role?" ) if not @current_user.respond_to? :has_role?
200
+ @stack.push( @current_user.has_role?(role_name) )
201
+ end
202
+ true
203
+ else
204
+ false
205
+ end
206
+ end
207
+
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,3 @@
1
+ module AuthorizationNext
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: authorization_next
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Pavan Agrawal
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2019-05-14 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.17'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.17'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '10.0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '10.0'
46
+ description: Converted plugin to gem which will work with association as well.
47
+ email:
48
+ - pavan.agrawala@gmail.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - .gitignore
54
+ - .idea/.rakeTasks
55
+ - .idea/authorization.iml
56
+ - .idea/inspectionProfiles/Project_Default.xml
57
+ - .idea/misc.xml
58
+ - .idea/modules.xml
59
+ - .idea/vcs.xml
60
+ - .idea/workspace.xml
61
+ - CODE_OF_CONDUCT.md
62
+ - Gemfile
63
+ - Gemfile.lock
64
+ - LICENSE.txt
65
+ - README.md
66
+ - Rakefile
67
+ - authorization.gemspec
68
+ - bin/console
69
+ - bin/setup
70
+ - lib/authorization_next.rb
71
+ - lib/authorization_next/publishare/exceptions.rb
72
+ - lib/authorization_next/publishare/hardwired_roles.rb
73
+ - lib/authorization_next/publishare/identity.rb
74
+ - lib/authorization_next/publishare/object_roles_table.rb
75
+ - lib/authorization_next/publishare/parser.rb
76
+ - lib/authorization_next/version.rb
77
+ homepage: https://github.com/pavanagrawal/authorization_next
78
+ licenses:
79
+ - MIT
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ! '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ requirements: []
97
+ rubyforge_project:
98
+ rubygems_version: 1.8.23.2
99
+ signing_key:
100
+ specification_version: 3
101
+ summary: Converted plugin to gem which will work with association as well.
102
+ test_files: []