module AuthorizationNext module Base VALID_PREPOSITIONS = ['of', 'for', 'in', 'on', 'to', 'at', 'by'] BOOLEAN_OPS = ['not', 'or', 'and'] VALID_PREPOSITIONS_PATTERN = VALID_PREPOSITIONS.join('|') module EvalParser # Parses and evaluates an authorization expression and returns true or false. # # The authorization expression is defined by the following grammar: # ::= () | not | or | and | # ::= | # ::= of | for | in | on | to | at | by # ::= /:*\w+/ # ::= /\w+/ | /'.*'/ # # Instead of doing recursive descent parsing (not so fun when we support nested parentheses, etc), # we let Ruby do the work for us by inserting the appropriate permission calls and using eval. # This would not be a good idea if you were getting authorization expressions from the outside, # so in that case (e.g. somehow letting users literally type in permission expressions) you'd # be better off using the recursive descent parser in Module RecursiveDescentParser. # # We search for parts of our authorization evaluation that match or # and we ignore anything terminal in our grammar. # # 1) Replace all matches. # 2) Replace all matches that aren't one of our other terminals ('not', 'or', 'and', or preposition) # 3) Eval def parse_authorization_expression( str ) if str =~ /[^A-Za-z0-9_:'\(\)\s]/ raise AuthorizationExpressionInvalid, "Invalid authorization expression (#{str})" return false end @replacements = [] expr = replace_temporarily_role_of_model( str ) expr = replace_role( expr ) expr = replace_role_of_model( expr ) begin instance_eval( expr ) rescue raise AuthorizationExpressionInvalid, "Cannot parse authorization (#{str})" end end def replace_temporarily_role_of_model( str ) role_regex = '\s*(\'\s*(.+)\s*\'|(\w+))\s+' model_regex = '\s+(:*\w+)' parse_regex = Regexp.new(role_regex + '(' + VALID_PREPOSITIONS.join('|') + ')' + model_regex) str.gsub(parse_regex) do |match| @replacements.push " process_role_of_model('#{$2 || $3}', '#{$5}') " " <#{@replacements.length-1}> " end end def replace_role( str ) role_regex = '\s*(\'\s*(.+)\s*\'|([A-Za-z]\w*))\s*' parse_regex = Regexp.new(role_regex) str.gsub(parse_regex) do |match| if BOOLEAN_OPS.include?($3) " #{match} " else " process_role('#{$2 || $3}') " end end end def replace_role_of_model( str ) str.gsub(/<(\d+)>/) do |match| @replacements[$1.to_i] end end def process_role_of_model( role_name, model_name ) model = get_model( model_name ) raise( ModelDoesntImplementRoles, "Model (#{model_name}) doesn't implement #accepts_role?" ) if not model.respond_to? :accepts_role? model.send( :accepts_role?, role_name, @current_user ) end def process_role( role_name ) return false if @current_user.nil? raise( UserDoesntImplementRoles, "User doesn't implement #has_role?" ) if not @current_user.respond_to? :has_role? @current_user.has_role?( role_name ) end end # 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: # ::= () | not | or | and | # ::= | # ::= of | for | in | on | to | at | by # ::= /:*\w+/ # ::= /\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. module RecursiveDescentParser 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*$') def parse_authorization_expression( str ) @stack = [] raise AuthorizationExpressionInvalid, "Cannot parse authorization (#{str})" if not parse_expr( str ) return @stack.pop end def parse_expr( str ) parse_parenthesis( str ) or parse_not( str ) or parse_or( str ) or parse_and( str ) or parse_term( str ) end def parse_not( str ) if str =~ NOT_REGEX can_parse = parse_expr( $1 ) @stack.push( !@stack.pop ) if can_parse end false end 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 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 # Descend down parenthesis (allow up to 5 levels of nesting) def parse_parenthesis( str ) str =~ PARENTHESES_REGEX ? parse_expr( $1 ) : false end def parse_term( str ) parse_role_of_model( str ) or parse_role( str ) end # Parse of 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 of the User-like object def parse_role( str ) if str =~ ROLE_REGEX role_name = $1 if @current_user.nil? @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 end end end