mirror of
https://github.com/logos-co/open-law.git
synced 2025-01-09 14:25:58 +00:00
require_permission, start connecting permissions to routes
This commit is contained in:
parent
1554015e31
commit
face847c69
@ -40,9 +40,9 @@ def book_validator() -> Response | None:
|
|||||||
book_id = request_args.get("book_id")
|
book_id = request_args.get("book_id")
|
||||||
if book_id:
|
if book_id:
|
||||||
book: m.Book = db.session.get(m.Book, 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)
|
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"))
|
return redirect(url_for("book.my_library"))
|
||||||
|
|
||||||
collection_id = request_args.get("collection_id")
|
collection_id = request_args.get("collection_id")
|
||||||
|
82
app/controllers/require_permission.py
Normal file
82
app/controllers/require_permission.py
Normal file
@ -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
|
@ -21,6 +21,7 @@ from app.controllers.create_access_groups import (
|
|||||||
create_editor_group,
|
create_editor_group,
|
||||||
create_moderator_group,
|
create_moderator_group,
|
||||||
)
|
)
|
||||||
|
from app.controllers.require_permission import require_permission
|
||||||
from app import models as m, db, forms as f
|
from app import models as m, db, forms as f
|
||||||
from app.logger import log
|
from app.logger import log
|
||||||
from .bp import bp
|
from .bp import bp
|
||||||
@ -112,6 +113,12 @@ def create():
|
|||||||
|
|
||||||
@bp.route("/<int:book_id>/edit", methods=["POST"])
|
@bp.route("/<int:book_id>/edit", methods=["POST"])
|
||||||
@register_book_verify_route(bp.name)
|
@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
|
@login_required
|
||||||
def edit(book_id: int):
|
def edit(book_id: int):
|
||||||
form = f.EditBookForm()
|
form = f.EditBookForm()
|
||||||
@ -144,7 +151,7 @@ def delete(book_id: int):
|
|||||||
|
|
||||||
if not book or book.is_deleted:
|
if not book or book.is_deleted:
|
||||||
log(log.INFO, "User: [%s] is not owner of book: [%s]", current_user, book)
|
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"))
|
return redirect(url_for("book.my_library"))
|
||||||
|
|
||||||
book.is_deleted = True
|
book.is_deleted = True
|
||||||
|
@ -35,7 +35,7 @@ def create_comment(
|
|||||||
book: m.Book = db.session.get(m.Book, book_id)
|
book: m.Book = db.session.get(m.Book, book_id)
|
||||||
if not book or book.is_deleted:
|
if not book or book.is_deleted:
|
||||||
log(log.INFO, "User: [%s] is not owner of book: [%s]", current_user, book)
|
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"))
|
return redirect(url_for("book.my_library"))
|
||||||
|
|
||||||
collection: m.Collection = db.session.get(m.Collection, collection_id)
|
collection: m.Collection = db.session.get(m.Collection, collection_id)
|
||||||
|
@ -276,7 +276,7 @@ def qa_view(
|
|||||||
book: m.Book = db.session.get(m.Book, book_id)
|
book: m.Book = db.session.get(m.Book, book_id)
|
||||||
if not book or book.is_deleted:
|
if not book or book.is_deleted:
|
||||||
log(log.INFO, "User: [%s] is not owner of book: [%s]", current_user, book)
|
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"))
|
return redirect(url_for("book.my_library"))
|
||||||
|
|
||||||
collection: m.Collection = db.session.get(m.Collection, collection_id)
|
collection: m.Collection = db.session.get(m.Collection, collection_id)
|
||||||
|
@ -10,12 +10,19 @@ from app.controllers import (
|
|||||||
register_book_verify_route,
|
register_book_verify_route,
|
||||||
)
|
)
|
||||||
from app import models as m, db, forms as f
|
from app import models as m, db, forms as f
|
||||||
|
from app.controllers.require_permission import require_permission
|
||||||
from app.logger import log
|
from app.logger import log
|
||||||
from .bp import bp
|
from .bp import bp
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/<int:book_id>/settings", methods=["GET"])
|
@bp.route("/<int:book_id>/settings", methods=["GET"])
|
||||||
@register_book_verify_route(bp.name)
|
@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
|
@login_required
|
||||||
def settings(book_id: int):
|
def settings(book_id: int):
|
||||||
book: m.Book = db.session.get(m.Book, book_id)
|
book: m.Book = db.session.get(m.Book, book_id)
|
||||||
@ -27,6 +34,12 @@ def settings(book_id: int):
|
|||||||
|
|
||||||
@bp.route("/<int:book_id>/add_contributor", methods=["POST"])
|
@bp.route("/<int:book_id>/add_contributor", methods=["POST"])
|
||||||
@register_book_verify_route(bp.name)
|
@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
|
@login_required
|
||||||
def add_contributor(book_id: int):
|
def add_contributor(book_id: int):
|
||||||
form = f.AddContributorForm()
|
form = f.AddContributorForm()
|
||||||
@ -71,6 +84,12 @@ def add_contributor(book_id: int):
|
|||||||
|
|
||||||
@bp.route("/<int:book_id>/delete_contributor", methods=["POST"])
|
@bp.route("/<int:book_id>/delete_contributor", methods=["POST"])
|
||||||
@register_book_verify_route(bp.name)
|
@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
|
@login_required
|
||||||
def delete_contributor(book_id: int):
|
def delete_contributor(book_id: int):
|
||||||
form = f.DeleteContributorForm()
|
form = f.DeleteContributorForm()
|
||||||
@ -124,6 +143,12 @@ def delete_contributor(book_id: int):
|
|||||||
|
|
||||||
@bp.route("/<int:book_id>/edit_contributor_role", methods=["POST"])
|
@bp.route("/<int:book_id>/edit_contributor_role", methods=["POST"])
|
||||||
@register_book_verify_route(bp.name)
|
@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
|
@login_required
|
||||||
def edit_contributor_role(book_id: int):
|
def edit_contributor_role(book_id: int):
|
||||||
form = f.EditContributorRoleForm()
|
form = f.EditContributorRoleForm()
|
||||||
|
@ -87,7 +87,7 @@ def test_create_edit_delete_book(client: FlaskClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
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(
|
response: Response = client.post(
|
||||||
f"/book/{book.id}/edit",
|
f"/book/{book.id}/edit",
|
||||||
@ -132,7 +132,7 @@ def test_add_delete_contributor(client: FlaskClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
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)
|
book: m.Book = create_test_book(user.id)
|
||||||
m.BookVersion(semver="1.0.0", book_id=book.id).save()
|
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
|
user=moderator, book=book
|
||||||
).first()
|
).first()
|
||||||
assert contributor.role == m.BookContributor.Roles.MODERATOR
|
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()
|
editor = m.User(username="Editor", password="test").save()
|
||||||
response: Response = client.post(
|
response: Response = client.post(
|
||||||
@ -180,7 +180,7 @@ def test_add_delete_contributor(client: FlaskClient):
|
|||||||
user=editor, book=book
|
user=editor, book=book
|
||||||
).first()
|
).first()
|
||||||
assert contributor.role == m.BookContributor.Roles.EDITOR
|
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(
|
contributor_to_delete = m.BookContributor.query.filter_by(
|
||||||
user_id=moderator.id, book_id=book.id
|
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 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):
|
def test_edit_contributor_role(client: FlaskClient, runner: FlaskCliRunner):
|
||||||
_, user = login(client)
|
_, user = login(client)
|
||||||
user: m.User
|
user: m.User
|
||||||
|
|
||||||
# add dummmy data
|
book = create_test_book(user.id)
|
||||||
runner.invoke(args=["db-populate"])
|
|
||||||
|
# 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.user_id = user.id
|
||||||
book.save()
|
book.save()
|
||||||
|
|
||||||
@ -249,7 +251,7 @@ def test_edit_contributor_role(client: FlaskClient, runner: FlaskCliRunner):
|
|||||||
|
|
||||||
moderator = m.User(username="Moderator", password="test").save()
|
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(
|
response: Response = client.post(
|
||||||
f"/book/{moderators_book.id}/add_contributor",
|
f"/book/{moderators_book.id}/add_contributor",
|
||||||
data=dict(user_id=moderator.id, role=m.BookContributor.Roles.MODERATOR),
|
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 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(
|
response: Response = client.post(
|
||||||
f"/book/999/add_contributor",
|
f"/book/999/add_contributor",
|
||||||
@ -266,7 +268,7 @@ def test_edit_contributor_role(client: FlaskClient, runner: FlaskCliRunner):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
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):
|
def test_crud_collection(client: FlaskClient):
|
||||||
@ -298,7 +300,7 @@ def test_crud_collection(client: FlaskClient):
|
|||||||
follow_redirects=True,
|
follow_redirects=True,
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
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(
|
collection: m.Collection = m.Collection.query.filter_by(
|
||||||
label="Test Collection #1 Label"
|
label="Test Collection #1 Label"
|
||||||
@ -349,7 +351,7 @@ def test_crud_collection(client: FlaskClient):
|
|||||||
follow_redirects=True,
|
follow_redirects=True,
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
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(
|
edited_collection: m.Collection = m.Collection.query.filter_by(
|
||||||
label=new_label, about=new_about
|
label=new_label, about=new_about
|
||||||
@ -394,7 +396,7 @@ def test_crud_collection(client: FlaskClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
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):
|
def test_crud_subcollection(client: FlaskClient):
|
||||||
@ -423,7 +425,7 @@ def test_crud_subcollection(client: FlaskClient):
|
|||||||
follow_redirects=True,
|
follow_redirects=True,
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
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(
|
response: Response = client.post(
|
||||||
f"/book/{book.id}/{leaf_collection.id}/create_sub_collection",
|
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 response.status_code == 200
|
||||||
assert b"You are not owner of this book!" not in response.data
|
|
||||||
|
|
||||||
logout(client)
|
logout(client)
|
||||||
|
|
||||||
@ -1104,7 +1105,7 @@ def test_access_to_settings_page(client: FlaskClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
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(
|
def test_interpretation_in_home_last_inter_section(
|
||||||
|
@ -30,7 +30,9 @@ def logout(client):
|
|||||||
return client.get("/logout", follow_redirects=True)
|
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(
|
book: m.Book = m.Book(
|
||||||
label=f"Book {entity_id}", about=f"About {entity_id}", user_id=owner_id
|
label=f"Book {entity_id}", about=f"About {entity_id}", user_id=owner_id
|
||||||
).save()
|
).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
|
interpretation_id=section.id, access_group_id=access_group.id
|
||||||
).save()
|
).save()
|
||||||
|
|
||||||
# -------------
|
# Contributors
|
||||||
|
u = m.User(username=f"Bob {entity_id}").save()
|
||||||
|
m.BookContributor(book_id=book.id, user_id=u.id).save()
|
||||||
|
|
||||||
return book
|
return book
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user