Given a table with an Python Enum requires different access pattterns depending on if we filter on the database site via SQL (with the SQLAlchemy integration) or on the Python side just with OSO.
import enum
from sqlalchemy import coveting
class AccessLevel(enum.Enum): # <- Python Enum
public = "private"
private = "public"
class Post(Model):
__tablename__ = "posts"
id = Column(Integer, primary_key=True)
content = Column(String, nullable=False)
access_level = Column(Enum(AccessLevel), nullable=False)
# ^- Enum DB column, but with a Python Enum as value (SQLAlchemy does the translation automatically).
For just checking permissions on Python (authorize(user_obj, "read", post_obj)
), I have to write this rule with .value
, as post.access_level
is of type enum.Enum
.
allow(_: User, "read", post: Post) if
post.access_level.value = "public";
However when I try to filter on the database side, with an authorized_sessionmaker
, the rule doesn't work any more.
OSO parses the rule wrong, and believes that access_level
is a relationship to another table with a column value
.
So you get stuck in some method converting the rule into an SQL query.
s.query(Post).all()
../../../.local/share/virtualenvs/MoaO0l74/lib/python3.9/site-packages/sqlalchemy/orm/query.py:2768: in all
return self._iter().all()
../../../.local/share/virtualenvs/MoaO0l74/lib/python3.9/site-packages/sqlalchemy/orm/query.py:2903: in _iter
result = self.session.execute(
../../../.local/share/virtualenvs/MoaO0l74/lib/python3.9/site-packages/sqlalchemy/orm/session.py:1693: in execute
result = fn(orm_exec_state)
../../../.local/share/virtualenvs/MoaO0l74/lib/python3.9/site-packages/sqlalchemy_oso/session.py:311: in do_orm_execute
filter = authorize_model(oso, user, action, session, entity)
../../../.local/share/virtualenvs/MoaO0l74/lib/python3.9/site-packages/sqlalchemy_oso/auth.py:111: in authorize_model
filter = partial_to_filter(
../../../.local/share/virtualenvs/MoaO0l74/lib/python3.9/site-packages/sqlalchemy_oso/partial.py:124: in partial_to_filter
return translate_expr(expression, session, model, get_model)
../../../.local/share/virtualenvs/MoaO0l74/lib/python3.9/site-packages/sqlalchemy_oso/partial.py:139: in translate_expr
return translate_and(expression, session, model, get_model)
../../../.local/share/virtualenvs/MoaO0l74/lib/python3.9/site-packages/sqlalchemy_oso/partial.py:154: in translate_and
translated = translate_expr(expression, session, model, get_model)
../../../.local/share/virtualenvs/MoaO0l74/lib/python3.9/site-packages/sqlalchemy_oso/partial.py:137: in translate_expr
return translate_in(expression, session, model, get_model)
../../../.local/share/virtualenvs/MoaO0l74/lib/python3.9/site-packages/sqlalchemy_oso/partial.py:293: in translate_in
return translate_dot(
../../../.local/share/virtualenvs/MoaO0l74/lib/python3.9/site-packages/sqlalchemy_oso/partial.py:338: in translate_dot
return property.any(translate_dot(path[1:], session, model, func))
../../../.local/share/virtualenvs/MoaO0l74/lib/python3.9/site-packages/sqlalchemy_oso/partial.py:332: in translate_dot
return func(session, model)
../../../.local/share/virtualenvs/MoaO0l74/lib/python3.9/site-packages/sqlalchemy_oso/partial.py:363: in emit_subexpression
return translate_expr(sub_expression, session, model, get_model)
../../../.local/share/virtualenvs/MoaO0l74/lib/python3.9/site-packages/sqlalchemy_oso/partial.py:139: in translate_expr
return translate_and(expression, session, model, get_model)
../../../.local/share/virtualenvs/MoaO0l74/lib/python3.9/site-packages/sqlalchemy_oso/partial.py:154: in translate_and
translated = translate_expr(expression, session, model, get_model)
../../../.local/share/virtualenvs/MoaO0l74/lib/python3.9/site-packages/sqlalchemy_oso/partial.py:133: in translate_expr
return translate_compare(expression, session, model, get_model)
../../../.local/share/virtualenvs/MoaO0l74/lib/python3.9/site-packages/sqlalchemy_oso/partial.py:236: in translate_compare
return translate_compare(
../../../.local/share/virtualenvs/MoaO0l74/lib/python3.9/site-packages/sqlalchemy_oso/partial.py:228: in translate_compare
return translate_dot(
../../../.local/share/virtualenvs/MoaO0l74/lib/python3.9/site-packages/sqlalchemy_oso/partial.py:334: in translate_dot
property, model, is_multi_valued = get_relationship(model, path[0])
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
model = <class 'tables.Post'>, field_name = 'access_level'
def get_relationship(model, field_name: str):
"""Get the property object for field on model. field must be a relationship field.
:returns: (property, model, is_multi_valued)
"""
property = getattr(model, field_name)
> assert isinstance(property.property, RelationshipProperty)
E AssertionError
../../../.local/share/virtualenvs/MoaO0l74/lib/python3.9/site-packages/sqlalchemy_oso/partial.py:347: AssertionError
It works with
allow(_: User, "read", post: Post) if
post.access_level = "public";
But then of course it doesn't work on the Python side anymore.
Obviously it works if you define an Enum directly as in the https://docs.osohq.com/reference/frameworks/data_filtering/sqlalchemy.html tutorial, by using strings. With access_level = Column(Enum("public", "private"), nullable=False)
. However I think it's quite common to define the Enum separately on the Python side, and then reuse that enum in the ORM definition, and SQLAlchemy supports this since 1.1. It's just more safer, if you have Enums in the Python business logic, instead of having to deal with strings.