From face847c6986223890660ff03267829ab215a3bf Mon Sep 17 00:00:00 2001 From: SvyatoslavArtymovych Date: Mon, 29 May 2023 16:14:00 +0300 Subject: [PATCH] require_permission, start connecting permissions to routes --- app/controllers/book_verify.py | 4 +- app/controllers/require_permission.py | 82 +++++++++++++++++++++++++++ app/views/book/book.py | 9 ++- app/views/book/comment.py | 2 +- app/views/book/interpretation.py | 2 +- app/views/book/settings.py | 25 ++++++++ tests/test_book.py | 35 ++++++------ tests/utils.py | 8 ++- 8 files changed, 143 insertions(+), 24 deletions(-) create mode 100644 app/controllers/require_permission.py diff --git a/app/controllers/book_verify.py b/app/controllers/book_verify.py index 959cddb..fc42936 100644 --- a/app/controllers/book_verify.py +++ b/app/controllers/book_verify.py @@ -40,9 +40,9 @@ def book_validator() -> Response | None: book_id = request_args.get("book_id") if book_id: book: m.Book = db.session.get(m.Book, book_id) - if not book or book.is_deleted or book.owner != current_user: + if not book or book.is_deleted: log(log.INFO, "User: [%s] is not owner of book: [%s]", current_user, book) - flash("You are not owner of this book!", "danger") + flash("Book not found!", "danger") return redirect(url_for("book.my_library")) collection_id = request_args.get("collection_id") diff --git a/app/controllers/require_permission.py b/app/controllers/require_permission.py new file mode 100644 index 0000000..9990fbe --- /dev/null +++ b/app/controllers/require_permission.py @@ -0,0 +1,82 @@ +from flask_login import current_user +from flask import flash, redirect, url_for, request, make_response +import functools + +from app import models as m, db +from app.logger import log + + +def check_permissions( + entity_type: m.Permission.Entity, + access: list[m.Permission.Access], + model: m, + entity_id_field: str, +): + request_args = ( + {**request.view_args, **request.args} if request.view_args else {**request.args} + ) + + entity_id = request_args.get(entity_id_field) + if entity_id is None: + raise ValueError("entity_id not found") + entity: m.Book | m.Collection | m.Section | m.Interpretation = db.session.get( + model, entity_id + ) + if not entity or not entity.access_groups: + flash("You do not have permission", "warning") + return make_response(redirect(url_for("home.get_all"))) + + # check if user is not owner of book + if entity.access_groups[0].book.user_id == current_user.id: + return None + + access_group_query = ( + m.AccessGroup.query.join( + m.PermissionAccessGroups, + m.PermissionAccessGroups.access_group_id == m.AccessGroup.id, + ) + .join(m.Permission, m.PermissionAccessGroups.permission_id == m.Permission.id) + .filter( + m.AccessGroup.id.in_( + [access_group.id for access_group in entity.access_groups] + ) + ) + .filter(m.AccessGroup.users.any(id=current_user.id)) + .filter(m.Permission.entity_type == entity_type) + ) + + for access in access: + access_group_query = access_group_query.filter( + m.Permission.access.op("&")(access) > 0 + ) + + access_groups = access_group_query.all() + + if access_groups: + return + + flash("You do not have permission", "danger") + return make_response(redirect(url_for("home.get_all"))) + + +def require_permission( + entity_type: m.Permission.Entity, + access: list[m.Permission.Access], + model: m, + entity_id_field: str, +): + def decorator(f): + @functools.wraps(f) + def permission_checker(*args, **kwargs): + if response := check_permissions( + entity_type=entity_type, + access=access, + model=model, + entity_id_field=entity_id_field, + ): + return response + return f(*args, **kwargs) + + return permission_checker + + return decorator diff --git a/app/views/book/book.py b/app/views/book/book.py index 1e5a8fa..7fe5fa9 100644 --- a/app/views/book/book.py +++ b/app/views/book/book.py @@ -21,6 +21,7 @@ from app.controllers.create_access_groups import ( create_editor_group, create_moderator_group, ) +from app.controllers.require_permission import require_permission from app import models as m, db, forms as f from app.logger import log from .bp import bp @@ -112,6 +113,12 @@ def create(): @bp.route("//edit", methods=["POST"]) @register_book_verify_route(bp.name) +@require_permission( + entity_type=m.Permission.Entity.BOOK, + access=[m.Permission.Access.U], + model=m.Book, + entity_id_field="book_id", +) @login_required def edit(book_id: int): form = f.EditBookForm() @@ -144,7 +151,7 @@ def delete(book_id: int): if not book or book.is_deleted: log(log.INFO, "User: [%s] is not owner of book: [%s]", current_user, book) - flash("You are not owner of this book!", "danger") + flash("Book not found!", "danger") return redirect(url_for("book.my_library")) book.is_deleted = True diff --git a/app/views/book/comment.py b/app/views/book/comment.py index e608791..4de20dd 100644 --- a/app/views/book/comment.py +++ b/app/views/book/comment.py @@ -35,7 +35,7 @@ def create_comment( book: m.Book = db.session.get(m.Book, book_id) if not book or book.is_deleted: log(log.INFO, "User: [%s] is not owner of book: [%s]", current_user, book) - flash("You are not owner of this book!", "danger") + flash("Book not found!", "danger") return redirect(url_for("book.my_library")) collection: m.Collection = db.session.get(m.Collection, collection_id) diff --git a/app/views/book/interpretation.py b/app/views/book/interpretation.py index 7070522..416746d 100644 --- a/app/views/book/interpretation.py +++ b/app/views/book/interpretation.py @@ -276,7 +276,7 @@ def qa_view( book: m.Book = db.session.get(m.Book, book_id) if not book or book.is_deleted: log(log.INFO, "User: [%s] is not owner of book: [%s]", current_user, book) - flash("You are not owner of this book!", "danger") + flash("Book not found!", "danger") return redirect(url_for("book.my_library")) collection: m.Collection = db.session.get(m.Collection, collection_id) diff --git a/app/views/book/settings.py b/app/views/book/settings.py index 473a693..593673d 100644 --- a/app/views/book/settings.py +++ b/app/views/book/settings.py @@ -10,12 +10,19 @@ from app.controllers import ( register_book_verify_route, ) from app import models as m, db, forms as f +from app.controllers.require_permission import require_permission from app.logger import log from .bp import bp @bp.route("//settings", methods=["GET"]) @register_book_verify_route(bp.name) +@require_permission( + entity_type=m.Permission.Entity.BOOK, + access=[m.Permission.Access.U], + model=m.Book, + entity_id_field="book_id", +) @login_required def settings(book_id: int): book: m.Book = db.session.get(m.Book, book_id) @@ -27,6 +34,12 @@ def settings(book_id: int): @bp.route("//add_contributor", methods=["POST"]) @register_book_verify_route(bp.name) +@require_permission( + entity_type=m.Permission.Entity.BOOK, + access=[m.Permission.Access.U], + model=m.Book, + entity_id_field="book_id", +) @login_required def add_contributor(book_id: int): form = f.AddContributorForm() @@ -71,6 +84,12 @@ def add_contributor(book_id: int): @bp.route("//delete_contributor", methods=["POST"]) @register_book_verify_route(bp.name) +@require_permission( + entity_type=m.Permission.Entity.BOOK, + access=[m.Permission.Access.U], + model=m.Book, + entity_id_field="book_id", +) @login_required def delete_contributor(book_id: int): form = f.DeleteContributorForm() @@ -124,6 +143,12 @@ def delete_contributor(book_id: int): @bp.route("//edit_contributor_role", methods=["POST"]) @register_book_verify_route(bp.name) +@require_permission( + entity_type=m.Permission.Entity.BOOK, + access=[m.Permission.Access.U], + model=m.Book, + entity_id_field="book_id", +) @login_required def edit_contributor_role(book_id: int): form = f.EditContributorRoleForm() diff --git a/tests/test_book.py b/tests/test_book.py index 22ce570..22480e9 100644 --- a/tests/test_book.py +++ b/tests/test_book.py @@ -87,7 +87,7 @@ def test_create_edit_delete_book(client: FlaskClient): ) assert response.status_code == 200 - assert b"You are not owner of this book!" in response.data + assert b"Book not found!" in response.data response: Response = client.post( f"/book/{book.id}/edit", @@ -132,7 +132,7 @@ def test_add_delete_contributor(client: FlaskClient): ) assert response.status_code == 200 - assert b"You are not owner of this book!" in response.data + assert b"You do not have permission" in response.data book: m.Book = create_test_book(user.id) m.BookVersion(semver="1.0.0", book_id=book.id).save() @@ -164,7 +164,7 @@ def test_add_delete_contributor(client: FlaskClient): user=moderator, book=book ).first() assert contributor.role == m.BookContributor.Roles.MODERATOR - assert len(book.contributors) == 1 + assert len(book.contributors) == 2 editor = m.User(username="Editor", password="test").save() response: Response = client.post( @@ -180,7 +180,7 @@ def test_add_delete_contributor(client: FlaskClient): user=editor, book=book ).first() assert contributor.role == m.BookContributor.Roles.EDITOR - assert len(book.contributors) == 2 + assert len(book.contributors) == 3 contributor_to_delete = m.BookContributor.query.filter_by( user_id=moderator.id, book_id=book.id @@ -214,17 +214,19 @@ def test_add_delete_contributor(client: FlaskClient): ) assert response.status_code == 200 - assert b"You are not owner of this book!" in response.data + assert b"Book not found!" in response.data def test_edit_contributor_role(client: FlaskClient, runner: FlaskCliRunner): _, user = login(client) user: m.User - # add dummmy data - runner.invoke(args=["db-populate"]) + book = create_test_book(user.id) + + # for contributor in m.BookContributor.query.all(): + # db.session.delete(contributor) + # db.session.commit() - book = db.session.get(m.Book, 1) book.user_id = user.id book.save() @@ -249,7 +251,7 @@ def test_edit_contributor_role(client: FlaskClient, runner: FlaskCliRunner): moderator = m.User(username="Moderator", password="test").save() - moderators_book: m.Book = m.Book(label="Test Book", user_id=moderator.id).save() + moderators_book: m.Book = create_test_book(moderator.id) response: Response = client.post( f"/book/{moderators_book.id}/add_contributor", data=dict(user_id=moderator.id, role=m.BookContributor.Roles.MODERATOR), @@ -257,7 +259,7 @@ def test_edit_contributor_role(client: FlaskClient, runner: FlaskCliRunner): ) assert response.status_code == 200 - assert b"You are not owner of this book!" in response.data + assert b"You do not have permission" in response.data response: Response = client.post( f"/book/999/add_contributor", @@ -266,7 +268,7 @@ def test_edit_contributor_role(client: FlaskClient, runner: FlaskCliRunner): ) assert response.status_code == 200 - assert b"You are not owner of this book!" in response.data + assert b"Book not found!" in response.data def test_crud_collection(client: FlaskClient): @@ -298,7 +300,7 @@ def test_crud_collection(client: FlaskClient): follow_redirects=True, ) assert response.status_code == 200 - assert b"You are not owner of this book!" in response.data + assert b"Book not found!" in response.data collection: m.Collection = m.Collection.query.filter_by( label="Test Collection #1 Label" @@ -349,7 +351,7 @@ def test_crud_collection(client: FlaskClient): follow_redirects=True, ) assert response.status_code == 200 - assert b"You are not owner of this book!" in response.data + assert b"Book not found!" in response.data edited_collection: m.Collection = m.Collection.query.filter_by( label=new_label, about=new_about @@ -394,7 +396,7 @@ def test_crud_collection(client: FlaskClient): ) assert response.status_code == 200 - assert b"You are not owner of this book!" in response.data + assert b"Book not found!" in response.data def test_crud_subcollection(client: FlaskClient): @@ -423,7 +425,7 @@ def test_crud_subcollection(client: FlaskClient): follow_redirects=True, ) assert response.status_code == 200 - assert b"You are not owner of this book!" in response.data + assert b"Book not found!" in response.data response: Response = client.post( f"/book/{book.id}/{leaf_collection.id}/create_sub_collection", @@ -1094,7 +1096,6 @@ def test_access_to_settings_page(client: FlaskClient): ) assert response.status_code == 200 - assert b"You are not owner of this book!" not in response.data logout(client) @@ -1104,7 +1105,7 @@ def test_access_to_settings_page(client: FlaskClient): ) assert response.status_code == 200 - assert b"You are not owner of this book!" in response.data + assert b"You do not have permission" in response.data def test_interpretation_in_home_last_inter_section( diff --git a/tests/utils.py b/tests/utils.py index 755fbe5..01d969c 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -30,7 +30,9 @@ def logout(client): return client.get("/logout", follow_redirects=True) -def create_test_book(owner_id: int, entity_id: int = randint(1, 100)): +def create_test_book(owner_id: int, entity_id: int = 0): + if not entity_id: + entity_id = randint(1, 100) book: m.Book = m.Book( label=f"Book {entity_id}", about=f"About {entity_id}", user_id=owner_id ).save() @@ -122,7 +124,9 @@ def create_test_book(owner_id: int, entity_id: int = randint(1, 100)): interpretation_id=section.id, access_group_id=access_group.id ).save() - # ------------- + # Contributors + u = m.User(username=f"Bob {entity_id}").save() + m.BookContributor(book_id=book.id, user_id=u.id).save() return book