def test_nested_relationship_many_many(tag_nested_many_many_fixtures): """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(user: test_app2::User, "read", post: test_app2::Post) if tag in post.tags and user in tag.users; """) user = User.objects.get(username="******") authorize_filter = authorize_model(None, Post, actor=user, action="read") posts = Post.objects.filter(authorize_filter) assert tag_nested_many_many_fixtures["user_eng_post"] in posts assert tag_nested_many_many_fixtures["user_user_post"] in posts assert tag_nested_many_many_fixtures["random_post"] not in posts assert tag_nested_many_many_fixtures["not_tagged_post"] not in posts assert tag_nested_many_many_fixtures["all_tagged_post"] in posts user = User.objects.get(username="******") authorize_filter = authorize_model(None, Post, actor=user, action="read") posts = Post.objects.filter(authorize_filter) assert tag_nested_many_many_fixtures["user_eng_post"] not in posts assert tag_nested_many_many_fixtures["user_user_post"] not in posts assert tag_nested_many_many_fixtures["random_post"] in posts assert tag_nested_many_many_fixtures["not_tagged_post"] not in posts assert tag_nested_many_many_fixtures["all_tagged_post"] in posts
def test_empty_constraints_in(tag_nested_many_many_fixtures): """Test that ``unbound in partial.field`` without any further constraints on unbound translates into a check that COUNT(partial.field) > 0.""" Oso.load_str(""" allow(_: test_app2::User, "read", post: test_app2::Post) if _tag in post.tags; """) user = User.objects.get(username="******") authorize_filter = authorize_model(None, Post, actor=user, action="read") assert str(authorize_filter).startswith( "(AND: (NOT (AND: ('pk__in', []))), ('pk__in', <django.db.models.expressions.Subquery object at " ) posts = Post.objects.filter(authorize_filter) assert ( str(posts.query) == 'SELECT "test_app2_post"."id", "test_app2_post"."contents", "test_app2_post"."access_level",' + ' "test_app2_post"."created_by_id", "test_app2_post"."needs_moderation"' + ' FROM "test_app2_post"' + ' WHERE "test_app2_post"."id" IN' + ' (SELECT U0."id"' + ' FROM "test_app2_post" U0 LEFT OUTER JOIN "test_app2_post_tags" U1 ON (U0."id" = U1."post_id")' + ' GROUP BY U0."id", U0."contents", U0."access_level", U0."created_by_id", U0."needs_moderation"' + ' HAVING COUNT(U1."tag_id") > 0)') assert len(posts) == 4 assert tag_nested_many_many_fixtures["not_tagged_post"] not in posts
def test_redundant_in_on_same_field(tag_nested_many_many_fixtures): Oso.load_str(""" allow(_, "read", post) if tag1 in post.tags and tag2 in post.tags and tag1.name = "random" and tag2.is_public = true; """) user = User.objects.get(username="******") authorize_filter = authorize_model(None, Post, actor=user, action="read") posts = Post.objects.filter(authorize_filter) expected = f""" SELECT "test_app2_post"."id", "test_app2_post"."contents", "test_app2_post"."access_level", "test_app2_post"."created_by_id", "test_app2_post"."needs_moderation" FROM "test_app2_post" WHERE "test_app2_post"."id" IN (SELECT V0."id" FROM "test_app2_post" V0 LEFT OUTER JOIN "test_app2_post_tags" V1 ON (V0."id" = V1."post_id") WHERE (EXISTS(SELECT U0."id" FROM "test_app2_tag" U0 WHERE (U0."id" = {parenthesize('V1."tag_id"')} AND U0."name" = random)){is_true()} AND EXISTS(SELECT U0."id" FROM "test_app2_tag" U0 WHERE (U0."id" = {parenthesize('V1."tag_id"')} AND U0."is_public"{is_true()})){is_true()})) """ assert str(posts.query) == " ".join(expected.split()) assert len(posts) == 2
def test_unify_ins(tag_nested_many_many_fixtures): Oso.load_str(""" allow(_, _, post) if user1 in post.users and user2 in post.users and user1.id = user2.id and user1.id > 1 and user2.id <= 2; """) user = User.objects.get(username="******") authorize_filter = authorize_model(None, Post, actor=user, action="read") posts = Post.objects.filter(authorize_filter) expected = """ SELECT "test_app2_post"."id", "test_app2_post"."contents", "test_app2_post"."access_level", "test_app2_post"."created_by_id", "test_app2_post"."needs_moderation" FROM "test_app2_post" LEFT OUTER JOIN "test_app2_user_posts" ON ("test_app2_post"."id" = "test_app2_user_posts"."post_id") WHERE (EXISTS(SELECT U0."id" FROM "test_app2_user" U0 INNER JOIN "test_app2_user" V0 ON (U0."id" = V0."id") WHERE (U0."id" = "test_app2_user_posts"."user_id" AND V0."id" = "test_app2_user_posts"."user_id" AND U0."id" <= 2 AND V0."id" > 1))) """ assert str(posts.query) == " ".join(expected.split()) assert len(posts) == 1
def test_in_intersection(tag_nested_many_many_fixtures): Oso.load_str(""" allow(_, _, post) if u in post.users and t in post.tags and u in t.users; """) user = User.objects.get(username="******") authorize_filter = authorize_model(None, Post, actor=user, action="read") posts = Post.objects.filter(authorize_filter) expected = f""" SELECT "test_app2_post"."id", "test_app2_post"."contents", "test_app2_post"."access_level", "test_app2_post"."created_by_id", "test_app2_post"."needs_moderation" FROM "test_app2_post" WHERE "test_app2_post"."id" IN (SELECT X0."id" FROM "test_app2_post" X0 LEFT OUTER JOIN "test_app2_user_posts" X1 ON (X0."id" = X1."post_id") LEFT OUTER JOIN "test_app2_post_tags" X3 ON (X0."id" = X3."post_id") WHERE (EXISTS(SELECT U0."id" FROM "test_app2_user" U0 WHERE U0."id" = {parenthesize('X1."user_id"')}){is_true()} AND EXISTS(SELECT W0."id" FROM "test_app2_tag" W0 WHERE (W0."id" = {parenthesize('X3."tag_id"')} AND W0."id" IN (SELECT V0."id" FROM "test_app2_tag" V0 LEFT OUTER JOIN "test_app2_tag_users" V1 ON (V0."id" = V1."tag_id") WHERE EXISTS(SELECT U0."id" FROM "test_app2_user" U0 WHERE U0."id" = {parenthesize('V1."user_id"')}){is_true()}))){is_true()})) """ assert str(posts.query) == " ".join(expected.split()) assert len(posts) == 4
def test_empty_constraints_in(tag_nested_many_many_fixtures): """Test that ``unbound in partial.field`` without any further constraints on unbound translates into an existence check.""" Oso.load_str(""" allow(_: test_app2::User, "read", post: test_app2::Post) if _tag in post.tags; """) user = User.objects.get(username="******") authorize_filter = authorize_model(None, Post, actor=user, action="read") posts = Post.objects.filter(authorize_filter).distinct() expected = f""" SELECT DISTINCT "test_app2_post"."id", "test_app2_post"."contents", "test_app2_post"."access_level", "test_app2_post"."created_by_id", "test_app2_post"."needs_moderation" FROM "test_app2_post" WHERE "test_app2_post"."id" IN (SELECT V0."id" FROM "test_app2_post" V0 LEFT OUTER JOIN "test_app2_post_tags" V1 ON (V0."id" = V1."post_id") WHERE EXISTS(SELECT U0."id" FROM "test_app2_tag" U0 WHERE U0."id" = {parenthesize('V1."tag_id"')}){is_true()}) """ assert str(posts.query) == " ".join(expected.split()) assert len(posts) == 4 assert tag_nested_many_many_fixtures["not_tagged_post"] not in posts
def test_authorize_scalar_attribute_condition(post_fixtures): """Scalar attribute condition checks.""" Oso.load_str(""" # Object equals another object allow(actor: test_app2::User, "read", post: test_app2::Post) if post.created_by.is_banned = false and post.created_by = actor and post.access_level = "private"; allow(_actor: test_app2::User, "read", post: test_app2::Post) if post.created_by.is_banned = false and post.access_level = "public"; # moderator can see posts made by banned users. allow(actor: test_app2::User, "read", post: test_app2::Post) if actor.is_moderator = true and post.created_by.is_banned = true; """) foo = User.objects.get(username="******") authorize_filter = authorize_model(None, Post, actor=foo, action="read") posts = Post.objects.filter(authorize_filter) 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 = User.objects.get(username="******") authorize_filter = authorize_model(None, Post, actor=admin, action="read") posts = Post.objects.filter(authorize_filter) 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_partial_in_collection(tag_nested_many_many_fixtures): Oso.load_str(""" allow(user: test_app2::User, "read", post: test_app2::Post) if post in user.posts.all(); """) user = User.objects.get(username="******") authorize_filter = authorize_model(None, Post, actor=user, action="read") posts = Post.objects.filter(authorize_filter) assert tag_nested_many_many_fixtures["user_eng_post"] in posts assert tag_nested_many_many_fixtures["user_user_post"] in posts assert tag_nested_many_many_fixtures["random_post"] not in posts assert tag_nested_many_many_fixtures["not_tagged_post"] in posts assert tag_nested_many_many_fixtures["all_tagged_post"] in posts user = User.objects.get(username="******") authorize_filter = authorize_model(None, Post, actor=user, action="read") posts = Post.objects.filter(authorize_filter) assert tag_nested_many_many_fixtures["user_eng_post"] not in posts assert tag_nested_many_many_fixtures["user_user_post"] not in posts assert tag_nested_many_many_fixtures["random_post"] in posts assert tag_nested_many_many_fixtures["not_tagged_post"] not in posts assert tag_nested_many_many_fixtures["all_tagged_post"] not in posts
def test_this_in_var(tag_nested_many_many_fixtures): Oso.load_str(""" # _this in var allow(_, _, post: test_app2::Post) if post in x and x in post.created_by.posts; """) user = User.objects.get(username="******") authorize_filter = authorize_model(None, Post, actor=user, action="read") posts = Post.objects.filter(authorize_filter) expected = """ """ assert str(posts.query) == " ".join(expected.split()) assert len(posts) == 5050
def test_authorize_model_basic(post_fixtures): """Test that a simple policy with checks on non-relationship attributes is correct.""" Oso.load_str(""" allow(u, "read", post: test_app2::Post) if u in ["admin", "user"] and post.access_level = "public"; allow("user", "write", post: test_app2::Post) if post.access_level = "private"; allow("admin", "read", _post: test_app2::Post); allow("moderator", "read", post: test_app2::Post) if (post.access_level = "private" or post.access_level = "public") and post.needs_moderation = true; """) authorize_filter = authorize_model(None, Post, actor="user", action="read") assert str(authorize_filter) == "(AND: ('access_level', 'public'))" posts = Post.objects.filter(authorize_filter) assert posts.count() == 5 assert posts.all()[0].contents == "foo public post" authorize_filter = authorize_model(None, Post, actor="user", action="write") assert str(authorize_filter) == "(AND: ('access_level', 'private'))" posts = Post.objects.filter(authorize_filter) assert posts.count() == 4 assert posts.all()[0].contents == "foo private post" assert posts.all()[1].contents == "foo private post 2" authorize_filter = authorize_model(None, Post, actor="admin", action="read") assert str(authorize_filter) == str(TRUE_FILTER) posts = Post.objects.filter(authorize_filter) assert posts.count() == 9 authorize_filter = authorize_model(None, Post, actor="moderator", action="read") expected = """ (OR: (AND: ('access_level', 'private'), ('needs_moderation', True)), (AND: ('access_level', 'public'), ('needs_moderation', True))) """ assert str(authorize_filter) == " ".join(expected.split()) posts = Post.objects.filter(authorize_filter) assert posts.count() == 4 assert posts.all()[0].contents == "private for moderation" assert posts.all()[1].contents == "public for moderation" # Not authorized with pytest.raises(PermissionDenied): authorize_model(None, Post, actor="guest", action="read")
def test_deeply_nested_in(tag_nested_many_many_fixtures): Oso.load_str(""" allow(_, _, post: test_app2::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; """) user = User.objects.get(username="******") authorize_filter = authorize_model(None, Post, actor=user, action="read") posts = Post.objects.filter(authorize_filter).distinct() expected = """ SELECT DISTINCT "test_app2_post"."id", "test_app2_post"."contents", "test_app2_post"."access_level", "test_app2_post"."created_by_id", "test_app2_post"."needs_moderation" FROM "test_app2_post" INNER JOIN "test_app2_user" ON ("test_app2_post"."created_by_id" = "test_app2_user"."id") LEFT OUTER JOIN "test_app2_user_posts" ON ("test_app2_user"."id" = "test_app2_user_posts"."user_id") WHERE (EXISTS(SELECT W0."id" FROM "test_app2_post" W0 INNER JOIN "test_app2_user" W1 ON (W0."created_by_id" = W1."id") LEFT OUTER JOIN "test_app2_user_posts" W2 ON (W1."id" = W2."user_id") WHERE (EXISTS(SELECT V0."id" FROM "test_app2_post" V0 INNER JOIN "test_app2_user" V1 ON (V0."created_by_id" = V1."id") LEFT OUTER JOIN "test_app2_user_posts" V2 ON (V1."id" = V2."user_id") WHERE (EXISTS(SELECT U0."id" FROM "test_app2_post" U0 INNER JOIN "test_app2_user" U1 ON (U0."created_by_id" = U1."id") INNER JOIN "test_app2_user_posts" U2 ON (U1."id" = U2."user_id") WHERE (U0."id" = V2."post_id" AND U0."id" > 3 # This is not the sql that is generated. # Instead U0."id" is the LHS of below. AND "test_app2_post"."id" = U2."post_id")) AND V0."id" = W2."post_id" AND V0."id" > 2)) AND W0."id" = "test_app2_user_posts"."post_id" AND W0."id" > 1)) AND "test_app2_post"."id" > 4) """ assert str(posts.query) == " ".join(expected.split()) assert len(posts) == 1
def test_authorize_scalar_attribute_eq(post_fixtures): """Test authorization rules on a relationship with one object equaling another.""" # Object equals another object Oso.load_str(""" allow(actor: test_app2::User, "read", _: test_app2::Post{created_by: actor, access_level: "private"}); allow(_: test_app2::User, "read", post) if post matches test_app2::Post{access_level: "public"}; allow(_: test_app2::User{is_moderator: true}, "read", post: test_app2::Post) if post matches {access_level: "public"}; """) foo = User.objects.get(username="******") authorize_filter = authorize_model(None, Post, actor=foo, action="read") posts = Post.objects.filter(authorize_filter) 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_reverse_many_relationship(tag_nested_many_many_fixtures): """Test an authorization rule over a reverse relationship""" Oso.load_str(""" allow(actor, _, post: test_app2::Post) if post.users matches test_app2::User and actor in post.users; """) user = User.objects.get(username="******") authorize_filter = authorize_model(None, Post, actor=user, action="read") assert str(authorize_filter) == "(AND: ('users', <User: User object (1)>))" posts = Post.objects.filter(authorize_filter) expected = """ SELECT "test_app2_post"."id", "test_app2_post"."contents", "test_app2_post"."title", "test_app2_post"."access_level", "test_app2_post"."created_by_id", "test_app2_post"."needs_moderation" FROM "test_app2_post" INNER JOIN "test_app2_user_posts" ON ("test_app2_post"."id" = "test_app2_user_posts"."post_id") WHERE "test_app2_user_posts"."user_id" = 1 """ assert str(posts.query) == " ".join(expected.split()) assert len(posts) == 5
def test_in_with_constraints_but_no_matching_objects( tag_nested_many_many_fixtures): Oso.load_str(""" allow(_: test_app2::User, "read", post: test_app2::Post) if tag in post.tags and tag.name = "bloop"; """) user = User.objects.get(username="******") authorize_filter = authorize_model(None, Post, actor=user, action="read") assert (str(authorize_filter) == "(AND: (NOT (AND: ('pk__in', []))), ('tags__name', 'bloop'))") posts = Post.objects.filter(authorize_filter) assert ( str(posts.query) == 'SELECT "test_app2_post"."id", "test_app2_post"."contents", "test_app2_post"."access_level",' + ' "test_app2_post"."created_by_id", "test_app2_post"."needs_moderation"' + ' FROM "test_app2_post"' + ' INNER JOIN "test_app2_post_tags"' + ' ON ("test_app2_post"."id" = "test_app2_post_tags"."post_id")' + ' INNER JOIN "test_app2_tag"' + ' ON ("test_app2_post_tags"."tag_id" = "test_app2_tag"."id")' + ' WHERE "test_app2_tag"."name" = bloop') assert len(posts) == 0
def test_in_multiple_attribute_relationship(tag_fixtures): Oso.load_str(""" allow(_: test_app2::User, "read", post: test_app2::Post) if post.access_level = "public"; allow(user: test_app2::User, "read", post: test_app2::Post) if post.access_level = "private" and post.created_by = user; allow(_: test_app2::User, "read", post: test_app2::Post) if tag in post.tags and 0 < tag.id and (tag.is_public = true or tag.name = "foo"); """) user = User.objects.get(username="******") authorize_filter = authorize_model(None, Post, actor=user, action="read") posts = Post.objects.filter(authorize_filter) assert tag_fixtures["user_public_post"] in posts assert tag_fixtures["user_private_post"] in posts assert tag_fixtures["other_user_public_post"] in posts assert tag_fixtures["other_user_private_post"] not in posts assert tag_fixtures["other_user_random_post"] in posts assert tag_fixtures["other_user_foo_post"] in posts