Skip to content

Instantly share code, notes, and snippets.

@meziantou
Last active October 31, 2023 08:37
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save meziantou/10603804 to your computer and use it in GitHub Desktop.
Save meziantou/10603804 to your computer and use it in GitHub Desktop.
Custom `AuthorizeAttribute` that allows boolean operators such as AND, OR, XOR, NOT
/// <summary>
/// [CustomAuthorize(Roles = "A && (!B || C) ^ D")]
/// </summary>
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
/*
* Exp -> SubExp '&&' Exp // AND
* Exp -> SubExp '||' Exp // OR
* Exp -> SubExp '^' Exp // XOR
* SubExp -> '(' Exp ')'
* SubExp -> '!' Exp // NOT
* SubExp -> RoleName
* RoleName -> [a-z0-9]
*/
abstract class Node
{
public abstract bool Eval(IPrincipal principal);
public abstract void PrettyPrint(TextWriter writer);
public string PrettyPrint()
{
using (StringWriter writer = new StringWriter())
{
PrettyPrint(writer);
return writer.ToString();
}
}
}
abstract class UnaryNode : Node
{
private readonly Node _expression;
public Node Expression
{
get { return _expression; }
}
protected UnaryNode(Node expression)
{
_expression = expression;
}
}
abstract class BinaryNode : Node
{
private readonly Node _leftExpression;
private readonly Node _rightExpression;
public Node LeftExpression
{
get { return _leftExpression; }
}
public Node RightExpression
{
get { return _rightExpression; }
}
protected BinaryNode(Node leftExpression, Node rightExpression)
{
_leftExpression = leftExpression;
_rightExpression = rightExpression;
}
}
class AndNode : BinaryNode
{
public AndNode(Node leftExpression, Node rightExpression)
: base(leftExpression, rightExpression)
{
}
public override bool Eval(IPrincipal principal)
{
return LeftExpression.Eval(principal) && RightExpression.Eval(principal);
}
public override void PrettyPrint(TextWriter writer)
{
writer.Write("(");
LeftExpression.PrettyPrint(writer);
writer.Write(" && ");
RightExpression.PrettyPrint(writer);
writer.Write(")");
}
}
class OrNode : BinaryNode
{
public OrNode(Node leftExpression, Node rightExpression)
: base(leftExpression, rightExpression)
{
}
public override bool Eval(IPrincipal principal)
{
return LeftExpression.Eval(principal) || RightExpression.Eval(principal);
}
public override void PrettyPrint(TextWriter writer)
{
writer.Write("(");
LeftExpression.PrettyPrint(writer);
writer.Write(" || ");
RightExpression.PrettyPrint(writer);
writer.Write(")");
}
}
class XorNode : BinaryNode
{
public XorNode(Node leftExpression, Node rightExpression)
: base(leftExpression, rightExpression)
{
}
public override bool Eval(IPrincipal principal)
{
return LeftExpression.Eval(principal) ^ RightExpression.Eval(principal);
}
public override void PrettyPrint(TextWriter writer)
{
writer.Write("(");
LeftExpression.PrettyPrint(writer);
writer.Write(" ^ ");
RightExpression.PrettyPrint(writer);
writer.Write(")");
}
}
class NotNode : UnaryNode
{
public NotNode(Node expression)
: base(expression)
{
}
public override bool Eval(IPrincipal principal)
{
return !Expression.Eval(principal);
}
public override void PrettyPrint(TextWriter writer)
{
writer.Write("(");
writer.Write("!");
Expression.PrettyPrint(writer);
writer.Write(")");
}
}
class RoleNode : Node
{
private readonly string _roleName;
public string RoleName
{
get { return _roleName; }
}
public RoleNode(string roleName)
{
_roleName = roleName;
}
public override bool Eval(IPrincipal principal)
{
return principal.IsInRole(RoleName);
}
public override void PrettyPrint(TextWriter writer)
{
writer.Write("(");
writer.Write(RoleName);
writer.Write(")");
}
}
static Node Parse(string text)
{
if (text == null) throw new ArgumentNullException("text");
List<string> tokens = new List<string>();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < text.Length; i++)
{
char c = text[i];
switch (c)
{
case ' ':
case '\t':
case '\r':
case '\n':
if (sb.Length == 0)
continue;
sb.Append(c);
break;
case '(':
case ')':
case '^':
case '!':
if (sb.Length > 0)
{
tokens.Add(sb.ToString());
sb.Clear();
}
tokens.Add(c.ToString(CultureInfo.InvariantCulture));
break;
case '&':
case '|':
if (sb.Length != 0)
{
char prev = sb[sb.Length - 1];
if (c == prev) // && or ||
{
sb.Remove(sb.Length - 1, 1); // remove last char
tokens.Add(sb.ToString().Trim());
sb.Clear();
tokens.Add(c == '&' ? "&&" : "||");
break;
}
}
sb.Append(c);
break;
default:
sb.Append(c);
break;
}
}
if (sb.Length > 0)
{
tokens.Add(sb.ToString());
}
return Parse(tokens.ToArray());
}
static Node Parse(string[] tokens)
{
int index = 0;
return ParseExp(tokens, ref index);
}
static Node ParseExp(string[] tokens, ref int index)
{
Node leftExp = ParseSubExp(tokens, ref index);
if (index >= tokens.Length)
return leftExp;
string token = tokens[index];
if (token == "&&")
{
index++;
Node rightExp = ParseExp(tokens, ref index);
return new AndNode(leftExp, rightExp);
}
else if (token == "||")
{
index++;
Node rightExp = ParseExp(tokens, ref index);
return new OrNode(leftExp, rightExp);
}
else if (token == "^")
{
index++;
Node rightExp = ParseExp(tokens, ref index);
return new XorNode(leftExp, rightExp);
}
else
{
throw new Exception("Expected '&&' or '||' or '^' or EOF");
}
}
static Node ParseSubExp(string[] tokens, ref int index)
{
string token = tokens[index];
if (token == "(")
{
index++;
Node node = ParseExp(tokens, ref index);
if (tokens[index] != ")")
throw new Exception("Expected ')'");
index++; // Skip ')'
return node;
}
else if (token == "!")
{
index++;
Node node = ParseExp(tokens, ref index);
return new NotNode(node);
}
else
{
index++;
return new RoleNode(token);
}
}
private Node _expression;
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (httpContext == null) throw new ArgumentNullException("httpContext");
IPrincipal user = httpContext.User;
if (!user.Identity.IsAuthenticated)
{
return false;
}
if (_expression == null)
{
_expression = Parse(Roles);
}
if (_expression != null)
{
return _expression.Eval(user);
}
return true;
}
}
@SeysT
Copy link

SeysT commented Jan 6, 2018

Hello @meziantou!

Firstly I want to thank you for your work! It helps me a lot to understand and write my own boolean parser. I translate parts of yours in python for one of my project.

However I find some parts of code that could be improved in my opinion.

  1. Line 259 in your ParseExp method you have a terminal condition for the end of the entire boolean expression but you don't have one if we are at the end of a parenthesis expression. I think it could be corrected by adding the following if statement:
if (tokens[index] == ')')
    return leftExp;
  1. In your grammar, you define the not operator as followed: SubExp -> '!' Exp. This means that the not operator will apply to the complete following expression, whereas the main behaviour for boolean parser would be to apply to only a subexpression (a rolename or an expression between parenthesis). I think the rule should be SubExp -> '!'SubExp and could be corrected in the code by replacing line 305 by:
Node node = ParseSubExp(tokens, ref index);

I hope this could be a good contribution to your code :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment