Module: Authorization::Base::RecursiveDescentParser

Defined in:
lib/publishare/parser.rb

Overview

Parses and evaluates an authorization expression and returns true or false. This recursive descent parses uses two instance variables:

@stack --> a stack with the top holding the boolean expression resulting from the parsing

The authorization expression is defined by the following grammar:

       <expr> ::= (<expr>) | not <expr> | <term> or <expr> | <term> and <expr> | <term>
       <term> ::= <role> | <role> <preposition> <model>
<preposition> ::= of | for | in | on | to | at | by
      <model> ::= /:*\w+/
       <role> ::= /\w+/ | /'.*'/

There are really two values we must track: (1) whether the expression is valid according to the grammar (2) the evaluated results –> true/false on the permission queries The first is embedded in the control logic because we want short-circuiting. If an expression has been parsed and the permission is false, we don’t want to try different ways of parsing. Note that this implementation of a recursive descent parser is meant to be simple and doesn’t allow arbitrary nesting of parentheses. It supports up to 5 levels of nesting. It also won’t handle some types of expressions (A or B) and C, which has to be rewritten as C and (A or B) so the parenthetical expressions are in the tail.

Constant Summary collapse

OPT_PARENTHESES_PATTERN =
'(([^()]|\(([^()]|\(([^()]|\(([^()]|\(([^()]|\(([^()])*\))*\))*\))*\))*\))*)'
PARENTHESES_PATTERN =
'\(' + OPT_PARENTHESES_PATTERN + '\)'
NOT_PATTERN =
'^\s*not\s+' + OPT_PARENTHESES_PATTERN + '$'
AND_PATTERN =
'^\s*' + OPT_PARENTHESES_PATTERN + '\s+and\s+' + OPT_PARENTHESES_PATTERN + '\s*$'
OR_PATTERN =
'^\s*' + OPT_PARENTHESES_PATTERN + '\s+or\s+' + OPT_PARENTHESES_PATTERN + '\s*$'
ROLE_PATTERN =
'(\'\s*(.+)\s*\'|(\w+))'
MODEL_PATTERN =
'(:*\w+)'
PARENTHESES_REGEX =
Regexp.new('^\s*' + PARENTHESES_PATTERN + '\s*$')
NOT_REGEX =
Regexp.new(NOT_PATTERN)
AND_REGEX =
Regexp.new(AND_PATTERN)
OR_REGEX =
Regexp.new(OR_PATTERN)
ROLE_REGEX =
Regexp.new('^\s*' + ROLE_PATTERN + '\s*$')
ROLE_OF_MODEL_REGEX =
Regexp.new('^\s*' + ROLE_PATTERN + '\s+(' + VALID_PREPOSITIONS_PATTERN + ')\s+' + MODEL_PATTERN + '\s*$')

Instance Method Summary collapse

Instance Method Details

#parse_and(str) ⇒ Object



157
158
159
160
161
162
163
164
# File 'lib/publishare/parser.rb', line 157

def parse_and( str )
  if str =~ AND_REGEX
    can_parse = parse_expr( $1 ) and parse_expr( $8 )
    @stack.push(@stack.pop & @stack.pop) if can_parse
    return can_parse
  end
  false
end

#parse_authorization_expression(str) ⇒ Object



126
127
128
129
130
# File 'lib/publishare/parser.rb', line 126

def parse_authorization_expression( str )
  @stack = []
  raise AuthorizationExpressionInvalid, "Cannot parse authorization (#{str})" if not parse_expr( str )
  return @stack.pop
end

#parse_expr(str) ⇒ Object



132
133
134
135
136
137
138
# File 'lib/publishare/parser.rb', line 132

def parse_expr( str )
  parse_parenthesis( str ) or
  parse_not( str ) or
  parse_or( str ) or
  parse_and( str ) or
  parse_term( str )
end

#parse_not(str) ⇒ Object



140
141
142
143
144
145
146
# File 'lib/publishare/parser.rb', line 140

def parse_not( str )
  if str =~ NOT_REGEX
    can_parse = parse_expr( $1 )
    @stack.push( !@stack.pop ) if can_parse
  end
  false
end

#parse_or(str) ⇒ Object



148
149
150
151
152
153
154
155
# File 'lib/publishare/parser.rb', line 148

def parse_or( str )
  if str =~ OR_REGEX
    can_parse = parse_expr( $1 ) and parse_expr( $8 )
    @stack.push( @stack.pop | @stack.pop ) if can_parse
    return can_parse
  end
  false
end

#parse_parenthesis(str) ⇒ Object

Descend down parenthesis (allow up to 5 levels of nesting)



167
168
169
# File 'lib/publishare/parser.rb', line 167

def parse_parenthesis( str )
  str =~ PARENTHESES_REGEX ? parse_expr( $1 ) : false
end

#parse_role(str) ⇒ Object

Parse <role> of the User-like object



193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/publishare/parser.rb', line 193

def parse_role( str )
  if str =~ ROLE_REGEX
    role_name = $1
    if @current_user.nil? || @current_user == :false
      @stack.push(false)
    else
      raise( UserDoesntImplementRoles, "User doesn't implement #has_role?" ) if not @current_user.respond_to? :has_role?
      @stack.push( @current_user.has_role?(role_name) )
    end
    true
  else
    false
  end
end

#parse_role_of_model(str) ⇒ Object

Parse <role> of <model>



177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/publishare/parser.rb', line 177

def parse_role_of_model( str )
  if str =~ ROLE_OF_MODEL_REGEX
    role_name = $2 || $3
    model_name = $5
    model_obj = get_model( model_name )
    raise( ModelDoesntImplementRoles, "Model (#{model_name}) doesn't implement #accepts_role?" ) if not model_obj.respond_to? :accepts_role?

    has_permission = model_obj.send( :accepts_role?, role_name, @current_user )
    @stack.push( has_permission )
    true
  else
    false
  end
end

#parse_term(str) ⇒ Object



171
172
173
174
# File 'lib/publishare/parser.rb', line 171

def parse_term( str )
  parse_role_of_model( str ) or
  parse_role( str )
end