RBACMiddleware only guarded non-staff users, so any staff account stepped past the role routing for /admin/ — an ops or buyer account with the flag set could reach the admin index, which embeds the model registry, and a later permission grant would have been inherited. Restrict /admin/ to the admin role (superuser excepted) and hide the ops Admin link from non-admins. Also close the second finding: the seed set is_staff only on creation, so a buyer whose flag had drifted stayed staff on every re-run. Derive it from the role on the update path too, and align existing rows with a one-off migration so the flag never disagrees with the role.
51 lines
2.1 KiB
Python
51 lines
2.1 KiB
Python
import re
|
|
|
|
from django.conf import settings
|
|
from django.shortcuts import redirect
|
|
|
|
# The administration area is decided by role, not by the staff flag alone.
|
|
ADMIN_PATH = re.compile(r"^/admin/")
|
|
|
|
|
|
class RBACMiddleware:
|
|
ROLE_ROUTES = {
|
|
"admin": [r"^/admin/", r"^/ops/", r"^/portal/"],
|
|
"ops": [r"^/ops/"],
|
|
"buyer": [r"^/portal/"],
|
|
}
|
|
|
|
def __init__(self, get_response):
|
|
self.get_response = get_response
|
|
|
|
def __call__(self, request):
|
|
user = request.user
|
|
if user.is_authenticated and not user.is_superuser:
|
|
path = request.path_info
|
|
user_role = getattr(request.user, "role", None)
|
|
|
|
# /admin/ is for the admin role. is_staff alone is not enough: an ops
|
|
# or buyer account carrying the flag must not reach the
|
|
# administration area, whose index embeds the model registry, and
|
|
# where a later group or permission grant would be inherited. A
|
|
# superuser (above) is the deliberate exception.
|
|
if ADMIN_PATH.match(path) and user_role != "admin":
|
|
return redirect(settings.LOGIN_REDIRECT_URL)
|
|
|
|
if not user.is_staff:
|
|
allowed = self.ROLE_ROUTES.get(user_role, [])
|
|
if any(re.match(pattern, path) for pattern in allowed):
|
|
return self.get_response(request)
|
|
# The path belongs to a portal this role may not enter. Send them
|
|
# to the role-aware landing view rather than straight into a
|
|
# portal they cannot use. It used to redirect if *any* role's
|
|
# pattern matched the path without belonging to this role, which
|
|
# bounced a buyer off /portal/ itself (the admin patterns include
|
|
# /portal/), and looped with the landing redirect.
|
|
if any(
|
|
re.match(pattern, path)
|
|
for patterns in self.ROLE_ROUTES.values()
|
|
for pattern in patterns
|
|
):
|
|
return redirect(settings.LOGIN_REDIRECT_URL)
|
|
return self.get_response(request)
|
|
|