diff --git a/app/apps/common/functions/permissions.py b/app/apps/common/functions/permissions.py new file mode 100644 index 0000000..560c3d7 --- /dev/null +++ b/app/apps/common/functions/permissions.py @@ -0,0 +1,36 @@ +from django.core.exceptions import PermissionDenied +from django.shortcuts import get_object_or_404 + + +def get_owned_object_or_403(klass, request, *args, owner_path="owner", **kwargs): + """Fetch an object like ``get_object_or_404`` while enforcing ownership. + + Returns the object when it has no owner, or when it is owned by + ``request.user``; otherwise raises :class:`~django.core.exceptions.PermissionDenied` + (HTTP 403). This mirrors the owner check used by ``transaction_rule_edit`` + (``if obj.owner and obj.owner != request.user``) so authorization is applied + uniformly across handlers that resolve an object from a URL id. + + An object with no owner stays accessible to everyone, preserving the + existing behaviour for legacy/unowned objects. + + ``owner_path`` is a dotted attribute path to the owning user, so nested + ownership is supported for objects owned through a relation, e.g. a rule + action owned via its parent rule:: + + get_owned_object_or_403( + TransactionRuleAction, request, id=pk, owner_path="rule.owner" + ) + """ + obj = get_object_or_404(klass, *args, **kwargs) + + owner = obj + for attr in owner_path.split("."): + owner = getattr(owner, attr, None) + if owner is None: + break + + if owner is not None and owner != request.user: + raise PermissionDenied + + return obj diff --git a/app/apps/rules/views.py b/app/apps/rules/views.py index 1fb44a6..72e7b97 100644 --- a/app/apps/rules/views.py +++ b/app/apps/rules/views.py @@ -7,6 +7,7 @@ from django.contrib.auth.decorators import login_required from django.db import transaction from django.http import HttpResponse from django.shortcuts import render, get_object_or_404, redirect +from apps.common.functions.permissions import get_owned_object_or_403 from django.utils.translation import gettext_lazy as _ from django.views.decorators.http import require_http_methods @@ -62,7 +63,7 @@ def rules_list(request): @disabled_on_demo @require_http_methods(["GET", "POST"]) def transaction_rule_toggle_activity(request, transaction_rule_id, **kwargs): - transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id) + transaction_rule = get_owned_object_or_403(TransactionRule, request, id=transaction_rule_id) current_active = transaction_rule.active transaction_rule.active = not current_active transaction_rule.save(update_fields=["active"]) @@ -151,7 +152,7 @@ def transaction_rule_edit(request, transaction_rule_id): @disabled_on_demo @require_http_methods(["GET", "POST"]) def transaction_rule_view(request, transaction_rule_id): - transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id) + transaction_rule = get_owned_object_or_403(TransactionRule, request, id=transaction_rule_id) edit_actions = transaction_rule.transaction_actions.all() update_or_create_actions = ( @@ -200,7 +201,7 @@ def transaction_rule_delete(request, transaction_rule_id): @disabled_on_demo @require_http_methods(["GET"]) def transaction_rule_take_ownership(request, transaction_rule_id): - transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id) + transaction_rule = get_owned_object_or_403(TransactionRule, request, id=transaction_rule_id) if not transaction_rule.owner: transaction_rule.owner = request.user @@ -261,7 +262,7 @@ def transaction_rule_share(request, pk): @disabled_on_demo @require_http_methods(["GET", "POST"]) def transaction_rule_action_add(request, transaction_rule_id): - transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id) + transaction_rule = get_owned_object_or_403(TransactionRule, request, id=transaction_rule_id) if request.method == "POST": form = TransactionRuleActionForm(request.POST, rule=transaction_rule) @@ -289,8 +290,8 @@ def transaction_rule_action_add(request, transaction_rule_id): @disabled_on_demo @require_http_methods(["GET", "POST"]) def transaction_rule_action_edit(request, transaction_rule_action_id): - transaction_rule_action = get_object_or_404( - TransactionRuleAction, id=transaction_rule_action_id + transaction_rule_action = get_owned_object_or_403( + TransactionRuleAction, request, id=transaction_rule_action_id, owner_path="rule.owner" ) transaction_rule = get_object_or_404( TransactionRule, id=transaction_rule_action.rule.id @@ -327,8 +328,8 @@ def transaction_rule_action_edit(request, transaction_rule_action_id): @disabled_on_demo @require_http_methods(["DELETE"]) def transaction_rule_action_delete(request, transaction_rule_action_id): - transaction_rule_action = get_object_or_404( - TransactionRuleAction, id=transaction_rule_action_id + transaction_rule_action = get_owned_object_or_403( + TransactionRuleAction, request, id=transaction_rule_action_id, owner_path="rule.owner" ) transaction_rule_action.delete() @@ -348,7 +349,7 @@ def transaction_rule_action_delete(request, transaction_rule_action_id): @disabled_on_demo @require_http_methods(["GET", "POST"]) def update_or_create_transaction_rule_action_add(request, transaction_rule_id): - transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id) + transaction_rule = get_owned_object_or_403(TransactionRule, request, id=transaction_rule_id) if request.method == "POST": form = UpdateOrCreateTransactionRuleActionForm( @@ -380,7 +381,9 @@ def update_or_create_transaction_rule_action_add(request, transaction_rule_id): @disabled_on_demo @require_http_methods(["GET", "POST"]) def update_or_create_transaction_rule_action_edit(request, pk): - linked_action = get_object_or_404(UpdateOrCreateTransactionRuleAction, id=pk) + linked_action = get_owned_object_or_403( + UpdateOrCreateTransactionRuleAction, request, id=pk, owner_path="rule.owner" + ) transaction_rule = linked_action.rule if request.method == "POST": @@ -415,7 +418,9 @@ def update_or_create_transaction_rule_action_edit(request, pk): @disabled_on_demo @require_http_methods(["DELETE"]) def update_or_create_transaction_rule_action_delete(request, pk): - linked_action = get_object_or_404(UpdateOrCreateTransactionRuleAction, id=pk) + linked_action = get_owned_object_or_403( + UpdateOrCreateTransactionRuleAction, request, id=pk, owner_path="rule.owner" + ) linked_action.delete() @@ -436,7 +441,7 @@ def update_or_create_transaction_rule_action_delete(request, pk): @disabled_on_demo @require_http_methods(["GET", "POST"]) def dry_run_rule_created(request, pk): - rule = get_object_or_404(TransactionRule, id=pk) + rule = get_owned_object_or_403(TransactionRule, request, id=pk) logs = None results = None @@ -481,7 +486,7 @@ def dry_run_rule_created(request, pk): @disabled_on_demo @require_http_methods(["GET", "POST"]) def dry_run_rule_deleted(request, pk): - rule = get_object_or_404(TransactionRule, id=pk) + rule = get_owned_object_or_403(TransactionRule, request, id=pk) logs = None results = None @@ -526,7 +531,7 @@ def dry_run_rule_deleted(request, pk): @disabled_on_demo @require_http_methods(["GET", "POST"]) def dry_run_rule_updated(request, pk): - rule = get_object_or_404(TransactionRule, id=pk) + rule = get_owned_object_or_403(TransactionRule, request, id=pk) logs = None results = None