Module: Authorization::Base::RecursiveDescentParser

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


125
126
127
128
129
130
# File 'lib/rails-authorization-plugin/lib/publishare/parser.rb', line 125

def parse_authorization_expression(str)
  @stack = []
  raise AuthorizationExpressionInvalid, "Cannot parse authorization (#{str})" unless parse_expr(str)

  @stack.pop
end

#parse_expr(str) ⇒ Object


132
133
134
135
136
137
138
# File 'lib/rails-authorization-plugin/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/rails-authorization-plugin/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/rails-authorization-plugin/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/rails-authorization-plugin/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
207
# File 'lib/rails-authorization-plugin/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?") unless @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/rails-authorization-plugin/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?") unless 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/rails-authorization-plugin/lib/publishare/parser.rb', line 171

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