Module: Constellation::Authorization::Base::RecursiveDescentParser

Defined in:
lib/constellation/authorization/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



158
159
160
161
162
163
164
165
# File 'lib/constellation/authorization/parser.rb', line 158

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



127
128
129
130
131
# File 'lib/constellation/authorization/parser.rb', line 127

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



133
134
135
136
137
138
139
# File 'lib/constellation/authorization/parser.rb', line 133

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



141
142
143
144
145
146
147
# File 'lib/constellation/authorization/parser.rb', line 141

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



149
150
151
152
153
154
155
156
# File 'lib/constellation/authorization/parser.rb', line 149

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)



168
169
170
# File 'lib/constellation/authorization/parser.rb', line 168

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

#parse_role(str) ⇒ Object

Parse <role> of the User-like object



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

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>



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

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



172
173
174
175
# File 'lib/constellation/authorization/parser.rb', line 172

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