authorization_next 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []