def test_nested_relationship_many_single(session, oso, tag_nested_test_fixture): """Test that nested relationships work. post - (many) -> tags - (single) -> User A user can read a post with a tag if the tag's creator is the user. """ oso.load_str(""" allow(user, "read", post: Post) if tag in post.tags and tag.created_by = user; """) posts = session.query(Post).filter( authorize_model(oso, tag_nested_test_fixture["user"], "read", session, Post)) assert tag_nested_test_fixture["user_eng_post"] in posts assert tag_nested_test_fixture["user_user_post"] in posts assert not tag_nested_test_fixture["random_post"] in posts assert not tag_nested_test_fixture["not_tagged_post"] in posts assert tag_nested_test_fixture["all_tagged_post"] in posts assert posts.count() == 3 posts = session.query(Post).filter( authorize_model(oso, tag_nested_test_fixture["other_user"], "read", session, Post)) assert not tag_nested_test_fixture["user_eng_post"] in posts assert not tag_nested_test_fixture["user_user_post"] in posts assert tag_nested_test_fixture["random_post"] in posts assert not tag_nested_test_fixture["not_tagged_post"] in posts assert tag_nested_test_fixture["all_tagged_post"] in posts assert posts.count() == 2
def test_authorize_model_basic(session, oso, fixture_data): """Test that a simple policy with checks on non-relationship attributes is correct.""" oso.load_str('allow("user", "read", post: Post) if post.access_level = "public";') oso.load_str('allow("user", "write", post: Post) if post.access_level = "private";') oso.load_str('allow("admin", "read", post: Post);') oso.load_str( 'allow("moderator", "read", post: Post) if ' '(post.access_level = "private" or post.access_level = "public") and ' "post.needs_moderation = true;" ) posts = authorize_model(oso, "user", "read", session, Post) assert posts.count() == 5 assert posts.all()[0].contents == "foo public post" assert posts.all()[0].id == 0 posts = authorize_model(oso, "user", "write", session, Post) assert posts.count() == 4 assert posts.all()[0].contents == "foo private post" assert posts.all()[1].contents == "foo private post 2" posts = authorize_model(oso, "admin", "read", session, Post) assert posts.count() == 9 posts = authorize_model(oso, "moderator", "read", session, Post) print_query(posts) assert posts.all()[0].contents == "private for moderation" assert posts.all()[1].contents == "public for moderation" posts = authorize_model(oso, "guest", "read", session, Post) assert posts.count() == 0
def test_partial_in_collection(session, oso, tag_nested_many_many_test_fixture): oso.load_str(""" allow(user, "read", post: Post) if post in user.posts; """) user = tag_nested_many_many_test_fixture["user"] posts = session.query(Post).filter( authorize_model(oso, user, "read", session, Post)) print_query(posts) posts = posts.all() assert tag_nested_many_many_test_fixture["user_eng_post"] in posts assert tag_nested_many_many_test_fixture["user_user_post"] in posts assert tag_nested_many_many_test_fixture["random_post"] not in posts assert tag_nested_many_many_test_fixture["not_tagged_post"] in posts assert tag_nested_many_many_test_fixture["all_tagged_post"] in posts assert len(posts) == 4 user = tag_nested_many_many_test_fixture["other_user"] posts = (session.query(Post).filter( authorize_model(oso, user, "read", session, Post)).all()) assert tag_nested_many_many_test_fixture["user_eng_post"] not in posts assert tag_nested_many_many_test_fixture["user_user_post"] not in posts assert tag_nested_many_many_test_fixture["random_post"] in posts assert tag_nested_many_many_test_fixture["not_tagged_post"] not in posts assert tag_nested_many_many_test_fixture["all_tagged_post"] not in posts assert len(posts) == 1
def test_nested_relationship_many_many(session, oso, tag_nested_many_many_test_fixture): """Test that nested relationships work. post - (many) -> tags - (many) -> User A user can read a post with a tag if the tag's creator is the user. """ # TODO This direction doesn't work, because tag in user.tags is a concrete object. # allow(user, "read", post: Post) if tag in post.tags and tag in user.tags; oso.load_str(""" allow(user, "read", post: Post) if tag in post.tags and user in tag.users; """) posts = session.query(Post).filter( authorize_model(oso, tag_nested_many_many_test_fixture["user"], "read", session, Post)) assert tag_nested_many_many_test_fixture["user_eng_post"] in posts assert tag_nested_many_many_test_fixture["user_user_post"] in posts assert not tag_nested_many_many_test_fixture["random_post"] in posts assert not tag_nested_many_many_test_fixture["not_tagged_post"] in posts assert tag_nested_many_many_test_fixture["all_tagged_post"] in posts posts = session.query(Post).filter( authorize_model(oso, tag_nested_many_many_test_fixture["other_user"], "read", session, Post)) assert not tag_nested_many_many_test_fixture["user_eng_post"] in posts assert not tag_nested_many_many_test_fixture["user_user_post"] in posts assert tag_nested_many_many_test_fixture["random_post"] in posts assert not tag_nested_many_many_test_fixture["not_tagged_post"] in posts assert tag_nested_many_many_test_fixture["all_tagged_post"] in posts
def _authorize_query(query: Query) -> Optional[Query]: """Authorize an existing query with an oso instance, user and action.""" # Get the query session. session = query.session # Check whether this is an oso session. if not isinstance(session, AuthorizedSessionBase): # Not an authorized session. return None oso = session.oso_context["oso"] user = session.oso_context["user"] action = session.oso_context["action"] # TODO (dhatch): This is necessary to allow ``authorize_query`` to work # on queries that have already been made. If a query has a LIMIT or OFFSET # applied, SQLAlchemy will by default throw an error if filters are applied. # This prevents these errors from occuring, but could result in some # incorrect queries. We should remove this if possible. query = query.enable_assertions(False) entities = {column["entity"] for column in query.column_descriptions} for entity in entities: # Only apply authorization to columns that represent a mapper entity. if entity is None: continue authorized_filter = authorize_model(oso, user, action, query.session, entity) if authorized_filter is not None: query = query.filter(authorized_filter) return query
def test_authorize_scalar_attribute_eq(session, oso, fixture_data): """Test authorization rules on a relationship with one object equaling another.""" # Object equals another object oso.load_str( 'allow(actor: User, "read", post: Post) if post.created_by = actor and ' 'post.access_level = "private";' ) oso.load_str( 'allow(actor: User, "read", post: Post) if ' 'post.access_level = "public";' ) oso.load_str( 'allow(actor: User{is_moderator: true}, "read", post: Post) if ' 'post.access_level = "public";' ) foo = session.query(User).filter(User.username == "foo").first() posts = authorize_model(oso, foo, "read", session, Post) print_query(posts) def allowed(post): return ( post.access_level == "public" or post.access_level == "private" and post.created_by == foo ) assert posts.count() == 8 assert all(allowed(post) for post in posts)
def test_authorize_scalar_attribute_condition(session, oso, fixture_data): """Scalar attribute condition checks.""" # Object equals another object oso.load_str( 'allow(actor: User, "read", post: Post) if post.created_by.is_banned = false and ' 'post.created_by.username = actor.username and post.access_level = "private";' ) oso.load_str( 'allow(actor: User, "read", post: Post) if post.created_by.is_banned = false and ' 'post.access_level = "public";' ) # moderator can see posts made by banned users. oso.load_str( """allow(actor: User, "read", post: Post) if actor.is_moderator = true and post.created_by.is_banned = true;""" ) foo = session.query(User).filter(User.username == "foo").first() posts = authorize_model(oso, foo, "read", session, Post) def allowed(post, user): return ( (post.access_level == "public" and not post.created_by.is_banned) or post.access_level == "private" and post.created_by == user ) assert posts.count() == 7 assert all(allowed(post, foo) for post in posts) admin = session.query(User).filter(User.username == "admin_user").first() posts = authorize_model(oso, admin, "read", session, Post) def allowed_admin(post): return post.created_by.is_banned assert posts.count() == 6 for post in posts: assert allowed(post, admin) or allowed_admin(post)
def test_redundant_in_on_same_field(session, oso, tag_nested_many_many_test_fixture): oso.load_str(""" allow(_, "read", post: Post) if tag in post.tags and tag2 in post.tags and tag.name = "random" and tag2.is_public = true; """) posts = session.query(Post).filter( authorize_model(oso, "user", "read", session, Post)) posts = posts.all() assert len(posts) == 2
def test_unify_ins(session, oso, tag_nested_many_many_test_fixture): oso.load_str(""" allow(_, _, post) if tag1 in post.tags and tag2 in post.tags and tag1.name = tag2.name and tag1.name > "a" and tag2.name <= "z"; """) posts = session.query(Post).filter( authorize_model(oso, "user", "read", session, Post)) assert posts.count() == 1
def test_empty_constraints_in(session, oso, tag_nested_many_many_test_fixture): oso.load_str("""allow(_, "read", post: Post) if _tag in post.tags;""") user = tag_nested_many_many_test_fixture["user"] posts = authorize_model(oso, user, "read", session, Post) assert str(posts) == ( "SELECT posts.id AS posts_id, posts.contents AS posts_contents, posts.access_level AS posts_access_level," + " posts.created_by_id AS posts_created_by_id, posts.needs_moderation AS posts_needs_moderation" + " \nFROM posts" + " \nWHERE (EXISTS (SELECT 1" + " \nFROM post_tags, tags" + " \nWHERE posts.id = post_tags.post_id AND tags.name = post_tags.tag_id))" ) posts = posts.all() assert len(posts) == 4 assert tag_nested_many_many_test_fixture["not_tagged_post"] not in posts
def test_in_intersection(session, oso, tag_nested_many_many_test_fixture): oso.load_str(""" allow(_, _, post: Post) if u in post.users and t in post.tags and u in t.users; """) posts = session.query(Post).filter( authorize_model(oso, "user", "read", session, Post)) # TODO (dhatch): Add query in here when this works. assert_query_equals(posts, "") assert posts.count() == 4
def test_partial_isa_with_path(session, oso, tag_nested_many_many_test_fixture): oso.load_str(""" allow(_, _, post: Post) if check_user(post.created_by); # User is not a tag. check_user(user: Tag) if user.username = "******"; check_user(user: User) if user.username = "******"; """) user = tag_nested_many_many_test_fixture["user"] posts = session.query(Post).filter( authorize_model(oso, user, "read", session, Post)) # Should only get posts created by user. posts = posts.all() for post in posts: assert post.created_by.username == "user" assert len(posts) == 4
def test_in_multiple_attribute_relationship(session, oso, tag_test_fixture): oso.load_str(""" allow(user, "read", post: Post) if post.access_level = "public"; allow(user, "read", post: Post) if post.access_level = "private" and post.created_by = user; allow(user, "read", post: Post) if tag in post.tags and 0 < post.id and (tag.is_public = true or tag.name = "foo"); """) posts = session.query(Post).filter( authorize_model(oso, tag_test_fixture["user"], "read", session, Post)) assert tag_test_fixture["user_public_post"] in posts assert tag_test_fixture["user_private_post"] in posts assert tag_test_fixture["other_user_public_post"] in posts assert not tag_test_fixture["other_user_private_post"] in posts assert tag_test_fixture["other_user_random_post"] in posts assert tag_test_fixture["other_user_foo_post"] in posts assert posts.count() == 5
def test_in_with_constraints_but_no_matching_objects( session, oso, tag_nested_many_many_test_fixture): oso.load_str(""" allow(_, "read", post: Post) if tag in post.tags and tag.name = "bloop"; """) user = tag_nested_many_many_test_fixture["user"] posts = session.query(Post).filter( authorize_model(oso, user, "read", session, Post)) assert str(posts) == ( "SELECT posts.id AS posts_id, posts.contents AS posts_contents, posts.access_level AS posts_access_level," + " posts.created_by_id AS posts_created_by_id, posts.needs_moderation AS posts_needs_moderation" + " \nFROM posts" + " \nWHERE (EXISTS (SELECT 1" + " \nFROM post_tags, tags" + " \nWHERE posts.id = post_tags.post_id AND tags.name = post_tags.tag_id AND tags.name = ?))" ) posts = posts.all() assert len(posts) == 0
def test_nested_relationship_many_many_constrained( session, oso, tag_nested_many_many_test_fixture): """Test that nested relationships work. post - (many) -> tags - (many) -> User A user can read a post with a tag if the tag's creator is the user. """ oso.load_str(""" allow(_, "read", post: Post) if tag in post.tags and user in tag.users and user.username = "******"; """) posts = session.query(Post).filter( authorize_model(oso, tag_nested_many_many_test_fixture["user"], "read", session, Post)) assert tag_nested_many_many_test_fixture["user_eng_post"] in posts assert tag_nested_many_many_test_fixture["user_user_post"] in posts assert not tag_nested_many_many_test_fixture["random_post"] in posts assert not tag_nested_many_many_test_fixture["not_tagged_post"] in posts assert tag_nested_many_many_test_fixture["all_tagged_post"] in posts
def test_deeply_nested_in(session, oso, tag_nested_many_many_test_fixture): oso.load_str(""" allow(_, _, post: Post) if foo in post.created_by.posts and foo.id > 1 and bar in foo.created_by.posts and bar.id > 2 and baz in bar.created_by.posts and baz.id > 3 and post in baz.created_by.posts and post.id > 4; """) posts = session.query(Post).filter( authorize_model(oso, "user", "read", session, Post)) query_str = """ SELECT posts.id AS posts_id, posts.contents AS posts_contents, posts.access_level AS posts_access_level, posts.created_by_id AS posts_created_by_id, posts.needs_moderation AS posts_needs_moderation FROM posts WHERE (EXISTS (SELECT 1 FROM users WHERE users.id = posts.created_by_id AND (EXISTS (SELECT 1 FROM posts WHERE users.id = posts.created_by_id AND posts.id > ? AND (EXISTS (SELECT 1 FROM users WHERE users.id = posts.created_by_id AND (EXISTS (SELECT 1 FROM posts WHERE users.id = posts.created_by_id AND posts.id > ? AND (EXISTS (SELECT 1 FROM users WHERE users.id = posts.created_by_id AND (EXISTS (SELECT 1 FROM posts WHERE users.id = posts.created_by_id AND posts.id > ?)))))))))))) AND (EXISTS (SELECT 1 FROM users WHERE users.id = posts.created_by_id AND (EXISTS (SELECT 1 FROM posts WHERE users.id = posts.created_by_id)))) AND posts.id > ? AND posts.id = """ assert_query_equals(posts, query_str) assert posts.count() == 1
def test_nested_relationship_many_many_many_constrained(session, engine, oso): """Test that nested relationships work. post - (many) -> tags - (many) -> category - (many) -> User """ foo = User(username="******") bar = User(username="******") foo_category = Category(name="foo_category", users=[foo]) bar_category = Category(name="bar_category", users=[bar]) both_category = Category(name="both_category", users=[foo, bar]) public_category = Category(name="public", users=[foo, bar]) foo_tag = Tag(name="foo", categories=[foo_category]) bar_tag = Tag(name="bar", categories=[bar_category]) both_tag = Tag( name="both", categories=[foo_category, bar_category, public_category], is_public=True, ) foo_post = Post(contents="foo_post", tags=[foo_tag]) bar_post = Post(contents="bar_post", tags=[bar_tag]) both_post = Post(contents="both_post", tags=[both_tag]) none_post = Post(contents="none_post", tags=[]) foo_post_2 = Post(contents="foo_post_2", tags=[foo_tag]) public_post = Post(contents="public_post", tags=[both_tag], access_level="public") session.add_all([ foo, bar, foo_category, bar_category, both_category, foo_tag, bar_tag, both_tag, foo_post, bar_post, both_post, none_post, foo_post_2, public_category, public_post, ]) session.commit() # A user can read a post that they are the moderator of the category of. oso.load_str(""" allow(user, "read", post: Post) if tag in post.tags and category in tag.categories and moderator in category.users and moderator = user; """) posts = session.query(Post).filter( authorize_model(oso, foo, "read", session, Post)) posts = posts.all() assert foo_post in posts assert both_post in posts assert public_post in posts assert foo_post_2 in posts assert bar_post not in posts assert len(posts) == 4 posts = session.query(Post).filter( authorize_model(oso, bar, "read", session, Post)) posts = posts.all() assert bar_post in posts assert both_post in posts assert public_post in posts assert foo_post not in posts assert foo_post_2 not in posts assert len(posts) == 3 # A user can read a post that they are the moderator of the category of if the # tag is public. oso.load_str(""" allow(user, "read_2", post: Post) if tag in post.tags and tag.is_public = true and category in tag.categories and moderator in category.users and moderator = user; """) posts = session.query(Post).filter( authorize_model(oso, bar, "read_2", session, Post)) posts = posts.all() # Only the both tag is public. assert both_post in posts assert public_post in posts assert bar_post not in posts assert foo_post not in posts assert foo_post_2 not in posts assert len(posts) == 2 # A user can read a post that they are the moderator of the category of if the # tag is public and the category name is public. oso.load_str(""" allow(user, "read_3", post: Post) if post.access_level = "public" and tag in post.tags and tag.is_public = true and category in tag.categories and category.name = "public" and moderator in category.users and moderator = user; """) posts = session.query(Post).filter( authorize_model(oso, bar, "read_3", session, Post)) print_query(posts) posts = posts.all() # Only the both tag is public but the category name is not correct. assert len(posts) == 1