def translate_compare(expression: Expression, session: Session, model, get_model): (left, right) = expression.args left_path = dot_path(left) if left_path: path, field_name = left_path[:-1], left_path[-1] return translate_dot( path, session, model, functools.partial(emit_compare, field_name, right, expression.operator), ) else: assert left == Variable("_this") if not isinstance(right, model): return sql.false() if expression.operator not in ("Eq", "Unify"): raise UnsupportedError( f"Unsupported comparison: {expression}. Models can only be compared" " with `=` or `==`" ) primary_keys = [pk.name for pk in inspect(model).primary_key] pk_filter = sql.true() for key in primary_keys: pk_filter &= getattr(model, key) == getattr(right, key) return pk_filter
def translate_isa(expression: Expression, session: Session, model, get_model): assert expression.operator == "Isa" left, right = expression.args if dot_path(left) == (): assert left == Variable("_this") else: for field_name in dot_path(left): _, model, __ = get_relationship(model, field_name) assert not right.fields, "Unexpected fields in isa expression" constraint_type = get_model(right.tag) return sql.true() if issubclass(model, constraint_type) else sql.false()
def translate_isa(expression: Expression, session: Session, model, get_model): assert expression.operator == "Isa" left, right = expression.args left_path = dot_path(left) assert left_path[0] == Variable("_this") left_path = left_path[1:] # Drop _this. if left_path: for field_name in left_path: _, model, __ = get_relationship(model, field_name) assert not right.fields, "Unexpected fields in isa expression" constraint_type = get_model(right.tag) model_type = inspect(model, raiseerr=True).class_ return sql.true() if issubclass(model_type, constraint_type) else sql.false()
def test_preprocess_nested_many_many(): _this = Variable("_this") expression = Expression( "And", [ Expression("Isa", [_this, Pattern("Post", {})]), Expression( "In", [Variable("_tag_16"), Expression("Dot", [_this, "tags"])]), Expression( "In", [ Variable("_user_18"), Expression("Dot", [Variable("_tag_16"), "users"]), ], ), Expression( "Unify", [ "admin", Expression("Dot", [Variable("_user_18"), "username"]) ], ), ], ) vars = defaultdict(list) new_expression = preprocess_expression(expression, vars) assert new_expression == Expression( "And", [ Expression("Isa", [_this, Pattern("Post", {})]), Expression( "In", [Variable("_tag_16"), Expression("Dot", [_this, "tags"])]), ], ) assert vars == { Variable("_tag_16"): [ Expression( "In", [ Variable("_user_18"), Expression("Dot", [Variable("_tag_16"), "users"]), ], ) ], Variable("_user_18"): [ Expression( "Unify", [ "admin", Expression("Dot", [Variable("_user_18"), "username"]) ], ) ], } users_expr = Expression("And", [ Expression("Unify", ["admin", Expression("Dot", [_this, "username"])]) ]) tags_expr = Expression( "And", [Expression( "In", [users_expr, Expression("Dot", [_this, "users"])])]) assert preprocess(expression) == Expression( "And", [ Expression("Isa", [_this, Pattern("Post", {})]), Expression("In", [tags_expr, Expression("Dot", [_this, "tags"])]), ], )
def dot_op_field(expr): """Get the field from dot op ``expr`` or return ``False``.""" return (isinstance(expr, Expression) and expr.operator == "Dot" and isinstance(expr.args[0], Variable) and expr.args[0] == Variable("_this") and expr.args[1])
def sub_this(variable: Variable, expression: Expression) -> Expression: """Substitute _this for ``variable`` in ``expression``.""" return sub_var(variable, Variable("_this"), expression)
def is_this(variable): """Return true if ``variable`` is ``_this``.""" return variable == Variable("_this")
def authorize_model(request, model, *, actor=None, action=None) -> Q: """Authorize ``request`` for django model ``model``, ``actor``, and ``action``. .. warning:: This feature is currently in preview. Partially evaluates the Polar rule ``allow(actor, action, Variable(model))``. If authorization fails, raises a :py:class:`django.core.exceptions.PermissionDenied` exception. Otherwise, returns a django ``Q`` object representing a filter that must be applied to ``model``. This object can be applied to filter query results to only contain authorized objects. For example:: post_filter = authorize_model(request, Post) authorized_posts = Post.objects.filter(post_filter) See also: - :py:class:`django_oso.models.AuthorizedModel` :param actor: The actor making the request. Defaults to ``request.user``. :param action: The action to authorize the actor to perform. Defaults to ``request.method``. :param model: The model to authorize access for. :raises django.core.exceptions.PermissionDenied: If the request is not authorized. :returns: A django ``Q`` object representing the authorization filter. """ if actor is None: actor = request.user if action is None: action = request.method assert issubclass(model, Model), f"Expected a model; received: {model}" resource = Variable("resource") constraint = TypeConstraint(resource, polar_model_name(model)) results = Oso.query_rule( "allow", actor, action, resource, bindings={resource: constraint}, accept_expression=True, ) filter = None for result in results: resource_partial = result["bindings"]["resource"] if filter is None: filter = Q() next_filter = partial_to_query_filter(resource_partial, model) if next_filter == TRUE_FILTER: return TRUE_FILTER filter |= next_filter if filter is None: raise PermissionDenied() return filter