Browse Source

admin panel permissions enhancement

master
Mohsen Taba 1 month ago
parent
commit
1b23de15b0
  1. 187
      apps/account/management/commands/audit_admin_panel_access.py
  2. 68
      apps/account/models/user.py
  3. 114
      apps/account/permissions.py
  4. 70
      apps/account/serializers/user.py
  5. 125
      apps/account/tests/test_admin_panel_access.py
  6. 27
      apps/account/views/user.py

187
apps/account/management/commands/audit_admin_panel_access.py

@ -0,0 +1,187 @@
from django.core.management.base import BaseCommand
from django.contrib.auth.models import Group
from apps.account.models import User
class Command(BaseCommand):
help = (
"Audit admin panel access consistency for users. "
"Shows users with panel access and highlights unsafe role/flag/group combinations. "
"Use --fix to automatically normalize unsafe records."
)
PANEL_GROUPS = {"Professor Group", "Admin Group", "Super Admin Group", "Super admin Group"}
def add_arguments(self, parser):
parser.add_argument(
"--fix",
action="store_true",
help="Automatically fix unsafe user records.",
)
parser.add_argument(
"--email",
action="append",
dest="emails",
help="Limit audit to one or more email addresses. Can be passed multiple times.",
)
def handle(self, *args, **options):
should_fix = options["fix"]
emails = options.get("emails") or []
queryset = User.objects.filter(email__isnull=False).exclude(email="").order_by("id")
if emails:
queryset = queryset.filter(email__in=emails)
panel_users = []
anomalies = []
fixed_count = 0
for user in queryset:
groups = list(user.groups.values_list("name", flat=True))
can_panel = user.can_access_admin_panel()
if can_panel or user.is_staff or user.is_superuser or any(g in self.PANEL_GROUPS for g in groups):
panel_users.append(
{
"id": user.id,
"email": user.email,
"user_type": user.user_type,
"is_staff": user.is_staff,
"is_superuser": user.is_superuser,
"groups": groups,
"can_panel": can_panel,
}
)
user_anomalies = self.get_anomalies(user, groups)
if user_anomalies:
anomalies.append(
{
"user": user,
"groups": groups,
"anomalies": user_anomalies,
}
)
self.stdout.write(self.style.MIGRATE_HEADING("Panel Access Users"))
if panel_users:
for item in panel_users:
self.stdout.write(str(item))
else:
self.stdout.write(self.style.SUCCESS("No panel-related users found."))
self.stdout.write("")
self.stdout.write(self.style.MIGRATE_HEADING("Anomalies"))
if anomalies:
for item in anomalies:
user = item["user"]
self.stdout.write(
self.style.WARNING(
str(
{
"id": user.id,
"email": user.email,
"user_type": user.user_type,
"is_staff": user.is_staff,
"is_superuser": user.is_superuser,
"groups": item["groups"],
"anomalies": item["anomalies"],
}
)
)
)
if should_fix:
if self.fix_user(user, item["anomalies"]):
fixed_count += 1
else:
self.stdout.write(self.style.SUCCESS("No anomalies found."))
if should_fix:
self.stdout.write("")
self.stdout.write(self.style.SUCCESS(f"Fixed {fixed_count} user(s)."))
def get_anomalies(self, user, groups):
anomalies = []
if user.user_type in [User.UserType.STUDENT, User.UserType.CLIENT, User.UserType.CONSULTANT]:
if user.is_staff or user.is_superuser:
anomalies.append("low_role_with_staff_flags")
if any(group in self.PANEL_GROUPS for group in groups):
anomalies.append("low_role_with_panel_group")
if user.can_access_admin_panel():
anomalies.append("low_role_can_access_panel")
if user.user_type == User.UserType.PROFESSOR and "Professor Group" not in groups:
anomalies.append("professor_missing_group")
if user.user_type == User.UserType.ADMIN and "Admin Group" not in groups:
anomalies.append("admin_missing_group")
if user.user_type == User.UserType.SUPER_ADMIN and not any(
group in groups for group in ["Super Admin Group", "Super admin Group"]
):
anomalies.append("super_admin_missing_group")
if user.user_type == User.UserType.SUPER_ADMIN and not user.is_superuser:
anomalies.append("super_admin_missing_superuser_flag")
return anomalies
def fix_user(self, user, anomalies):
changed = False
if user.user_type == User.UserType.SUPER_ADMIN:
if not user.is_staff:
user.is_staff = True
changed = True
if not user.is_superuser:
user.is_superuser = True
changed = True
changed = self.ensure_group(user, "Super Admin Group") or changed
changed = self.remove_groups(user, {"Professor Group", "Admin Group", "Super admin Group"}) or changed
elif user.user_type == User.UserType.ADMIN:
if user.is_superuser:
user.is_superuser = False
changed = True
changed = self.ensure_group(user, "Admin Group") or changed
changed = self.remove_groups(user, {"Professor Group", "Super Admin Group", "Super admin Group"}) or changed
elif user.user_type == User.UserType.PROFESSOR:
if user.is_superuser:
user.is_superuser = False
changed = True
changed = self.ensure_group(user, "Professor Group") or changed
changed = self.remove_groups(user, {"Admin Group", "Super Admin Group", "Super admin Group"}) or changed
elif user.user_type in [User.UserType.STUDENT, User.UserType.CLIENT, User.UserType.CONSULTANT]:
if user.is_staff:
user.is_staff = False
changed = True
if user.is_superuser:
user.is_superuser = False
changed = True
changed = self.remove_groups(user, self.PANEL_GROUPS) or changed
if changed:
user.save()
self.stdout.write(self.style.SUCCESS(f"Fixed user {user.id} <{user.email}>"))
return changed
def ensure_group(self, user, group_name):
group, _ = Group.objects.get_or_create(name=group_name)
if not user.groups.filter(id=group.id).exists():
user.groups.add(group)
return True
return False
def remove_groups(self, user, group_names):
groups = Group.objects.filter(name__in=group_names)
existing_ids = set(user.groups.filter(id__in=groups.values("id")).values_list("id", flat=True))
if existing_ids:
user.groups.remove(*groups)
return True
return False

68
apps/account/models/user.py

@ -166,23 +166,14 @@ class User(AbstractUser):
def has_role(self, role_name): def has_role(self, role_name):
"""چک کردن داشتن نقش خاص""" """چک کردن داشتن نقش خاص"""
if isinstance(role_name, str):
# اگر نام نقش به صورت string داده شده
group_name = f"{role_name.capitalize()} Group"
else:
# اگر از enum استفاده شده
group_name = f"{role_name.value.capitalize()} Group"
group_name = self._role_to_group_name(role_name)
return self.groups.filter(name=group_name).exists() return self.groups.filter(name=group_name).exists()
def add_role(self, role_name): def add_role(self, role_name):
"""اضافه کردن نقش جدید بدون حذف نقش‌های قبلی""" """اضافه کردن نقش جدید بدون حذف نقش‌های قبلی"""
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
if isinstance(role_name, str):
group_name = f"{role_name.capitalize()} Group"
else:
group_name = f"{role_name.value.capitalize()} Group"
group_name = self._role_to_group_name(role_name)
group, created = Group.objects.get_or_create(name=group_name) group, created = Group.objects.get_or_create(name=group_name)
self.groups.add(group) self.groups.add(group)
@ -198,11 +189,7 @@ class User(AbstractUser):
"""حذف نقش خاص""" """حذف نقش خاص"""
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
if isinstance(role_name, str):
group_name = f"{role_name.capitalize()} Group"
else:
group_name = f"{role_name.value.capitalize()} Group"
group_name = self._role_to_group_name(role_name)
try: try:
group = Group.objects.get(name=group_name) group = Group.objects.get(name=group_name)
self.groups.remove(group) self.groups.remove(group)
@ -218,6 +205,19 @@ class User(AbstractUser):
return [group.name.replace(' Group', '').lower() return [group.name.replace(' Group', '').lower()
for group in self.groups.all()] for group in self.groups.all()]
def _role_to_group_name(self, role_name):
role_value = role_name.value if hasattr(role_name, 'value') else str(role_name)
role_key = role_value.strip().lower()
role_group_map = {
'student': 'Student Group',
'professor': 'Professor Group',
'consultant': 'Consultant Group',
'admin': 'Admin Group',
'super_admin': 'Super Admin Group',
'client': 'Client Group',
}
return role_group_map.get(role_key, f"{role_value.replace('_', ' ').title()} Group")
def can_teach_course(self): def can_teach_course(self):
"""آیا می‌تواند دوره تدریس کند؟""" """آیا می‌تواند دوره تدریس کند؟"""
# اولویت اول: staff یا admin # اولویت اول: staff یا admin
@ -226,6 +226,42 @@ class User(AbstractUser):
# اولویت دوم: professor # اولویت دوم: professor
return self.has_role('professor') return self.has_role('professor')
def is_super_admin_panel_user(self):
"""آیا کاربر دسترسی کامل پنل را به عنوان سوپر ادمین دارد؟"""
return bool(
self.is_active and (
self.is_superuser or
self.user_type == self.UserType.SUPER_ADMIN or
self.has_role('super_admin')
)
)
def is_admin_panel_user(self):
"""آیا کاربر دسترسی کامل پنل را به عنوان ادمین دارد؟"""
return bool(
self.is_active and (
self.user_type == self.UserType.ADMIN or
self.has_role('admin')
)
)
def is_professor_panel_user(self):
"""آیا کاربر از طریق نقش استاد اجازه ورود به پنل را دارد؟"""
return bool(
self.is_active and (
self.user_type == self.UserType.PROFESSOR or
self.has_role('professor')
)
)
def can_access_admin_panel(self):
"""سوپر ادمین‌ها، ادمین‌ها و استادها اجازه ورود به پنل را دارند."""
return (
self.is_super_admin_panel_user() or
self.is_admin_panel_user() or
self.is_professor_panel_user()
)
def can_enroll_course(self): def can_enroll_course(self):
"""آیا می‌تواند در دوره ثبت‌نام کند؟""" """آیا می‌تواند در دوره ثبت‌نام کند؟"""
return True # همه می‌توانند دانش‌آموز باشند return True # همه می‌توانند دانش‌آموز باشند

114
apps/account/permissions.py

@ -1,58 +1,56 @@
from rest_framework.permissions import BasePermission, SAFE_METHODS
class IsActiveUser(BasePermission):
def has_permission(self, request, view):
return request.user and request.user.is_active
class IsSuperAdmin(BasePermission):
"""
Allows access only to super admins and staff admins.
"""
def has_permission(self, request, view):
return (
request.user and
request.user.is_authenticated and
request.user.is_active and
(request.user.is_superuser or request.user.user_type in ['super_admin', 'admin'])
)
class IsPanelUser(BasePermission):
"""
Allows access to all administrative panel roles: super admins, admins, and professors.
"""
def has_permission(self, request, view):
return (
request.user and
request.user.is_authenticated and
request.user.is_active and
(request.user.is_superuser or request.user.user_type in ['super_admin', 'admin', 'professor'])
)
class IsSuperAdminOrReadOnlyForProfessor(BasePermission):
"""
Allows full read-write access to super admins/admins,
but only read-only (GET, HEAD, OPTIONS) access to professors.
"""
def has_permission(self, request, view):
if not request.user or not request.user.is_authenticated or not request.user.is_active:
return False
# Super admin / Admin can do everything
if request.user.is_superuser or request.user.user_type in ['super_admin', 'admin']:
return True
# Professor has read-only access
if request.user.user_type == 'professor':
return request.method in SAFE_METHODS
return False
from rest_framework.permissions import BasePermission, SAFE_METHODS
class IsActiveUser(BasePermission):
def has_permission(self, request, view):
return request.user and request.user.is_active
class IsSuperAdmin(BasePermission):
"""
Allows access to super admins and admins with full panel privileges.
"""
def has_permission(self, request, view):
return (
request.user and
request.user.is_authenticated and
(request.user.is_super_admin_panel_user() or request.user.is_admin_panel_user())
)
class IsPanelUser(BasePermission):
"""
Allows access to super admins, admins, and professors.
"""
def has_permission(self, request, view):
return (
request.user and
request.user.is_authenticated and
request.user.can_access_admin_panel()
)
class IsSuperAdminOrReadOnlyForProfessor(BasePermission):
"""
Allows full read-write access to super admins and admins,
but only read-only (GET, HEAD, OPTIONS) access to professors.
"""
def has_permission(self, request, view):
if not request.user or not request.user.is_authenticated or not request.user.is_active:
return False
# Super admin / admin can do everything
if request.user.is_super_admin_panel_user() or request.user.is_admin_panel_user():
return True
# Professor has read-only access
if request.user.is_professor_panel_user():
return request.method in SAFE_METHODS
return False

70
apps/account/serializers/user.py

@ -1,5 +1,6 @@
from rest_framework import serializers from rest_framework import serializers
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from django.contrib.auth.models import Group
from django.contrib.auth.password_validation import validate_password from django.contrib.auth.password_validation import validate_password
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from apps.account.models import User from apps.account.models import User
@ -250,6 +251,70 @@ class AdminUserSerializer(serializers.ModelSerializer):
] ]
read_only_fields = ['id', 'date_joined', 'last_login', 'auth_token', 'device_id', 'device_os', 'user_agent', 'client_ip'] read_only_fields = ['id', 'date_joined', 'last_login', 'auth_token', 'device_id', 'device_os', 'user_agent', 'client_ip']
ROLE_GROUP_MAP = {
User.UserType.STUDENT: "Student Group",
User.UserType.PROFESSOR: "Professor Group",
User.UserType.ADMIN: "Admin Group",
User.UserType.SUPER_ADMIN: "Super Admin Group",
User.UserType.CONSULTANT: "Consultant Group",
}
PANEL_GROUPS = {"Professor Group", "Admin Group", "Super admin Group", "Super Admin Group"}
def _ensure_group(self, instance, group_name):
group, _ = Group.objects.get_or_create(name=group_name)
instance.groups.add(group)
def _remove_groups(self, instance, group_names):
if not group_names:
return
instance.groups.remove(*Group.objects.filter(name__in=group_names))
def _normalize_role_access(self, instance, requested_user_type):
if not requested_user_type:
return
requested_user_type = str(requested_user_type)
if requested_user_type == User.UserType.SUPER_ADMIN:
instance.is_staff = True
instance.is_superuser = True
self._ensure_group(instance, "Super Admin Group")
self._remove_groups(instance, {"Professor Group", "Admin Group", "Super admin Group"})
return
if requested_user_type == User.UserType.PROFESSOR:
instance.is_staff = False
instance.is_superuser = False
self._ensure_group(instance, "Professor Group")
self._remove_groups(instance, {"Admin Group", "Super admin Group", "Super Admin Group"})
return
if requested_user_type == User.UserType.ADMIN:
instance.is_staff = False
instance.is_superuser = False
self._ensure_group(instance, "Admin Group")
self._remove_groups(instance, {"Professor Group", "Super admin Group", "Super Admin Group"})
return
if requested_user_type == User.UserType.STUDENT:
instance.is_staff = False
instance.is_superuser = False
self._ensure_group(instance, "Student Group")
self._remove_groups(instance, self.PANEL_GROUPS)
return
if requested_user_type == User.UserType.CONSULTANT:
instance.is_staff = False
instance.is_superuser = False
self._ensure_group(instance, "Consultant Group")
self._remove_groups(instance, self.PANEL_GROUPS)
return
if requested_user_type == User.UserType.CLIENT:
instance.is_staff = False
instance.is_superuser = False
self._remove_groups(instance, self.PANEL_GROUPS)
def create(self, validated_data): def create(self, validated_data):
password = validated_data.pop('password', None) password = validated_data.pop('password', None)
user = User(**validated_data) user = User(**validated_data)
@ -259,13 +324,16 @@ class AdminUserSerializer(serializers.ModelSerializer):
else: else:
user.set_unusable_password() user.set_unusable_password()
user.save() user.save()
self._normalize_role_access(user, validated_data.get('user_type', user.user_type))
user.save()
return user return user
def update(self, instance, validated_data): def update(self, instance, validated_data):
password = validated_data.pop('password', None) password = validated_data.pop('password', None)
user = super().update(instance, validated_data) user = super().update(instance, validated_data)
self._normalize_role_access(user, validated_data.get('user_type', user.user_type))
if password: if password:
user.set_password(password) user.set_password(password)
user.set_plain_password(password) user.set_plain_password(password)
user.save()
user.save()
return user return user

125
apps/account/tests/test_admin_panel_access.py

@ -0,0 +1,125 @@
from django.contrib.auth.models import Group
from django.urls import reverse
from rest_framework import status
from rest_framework.authtoken.models import Token
from rest_framework.test import APITestCase
from apps.account.models import User
class AdminPanelAccessTests(APITestCase):
def setUp(self):
self.student_group = Group.objects.create(name="Student Group")
self.professor_group = Group.objects.create(name="Professor Group")
self.admin_group = Group.objects.create(name="Admin Group")
self.super_admin_group = Group.objects.create(name="Super Admin Group")
self.super_admin = User.objects.create_superuser(
email="superadmin@example.com",
password="SuperSecret123!",
fullname="Super Admin",
)
self.super_admin.language = None
self.super_admin.save()
self.super_admin_token = Token.objects.create(user=self.super_admin)
def _create_user(self, **kwargs):
password = kwargs.pop("password")
user = User.objects.create_user(password=password, **kwargs)
user.language = None
user.save()
return user
def test_student_with_staff_flag_cannot_login_to_admin_panel(self):
user = self._create_user(
email="student@example.com",
password="StudentPass123!",
fullname="Student User",
user_type=User.UserType.STUDENT,
is_staff=True,
)
response = self.client.post(
reverse("admin-login"),
{"email": user.email, "password": "StudentPass123!"},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_professor_can_login_to_admin_panel(self):
professor = self._create_user(
email="professor@example.com",
password="ProfessorPass123!",
fullname="Professor User",
user_type=User.UserType.PROFESSOR,
)
professor.add_role("professor")
response = self.client.post(
reverse("admin-login"),
{"email": professor.email, "password": "ProfessorPass123!"},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data["user_type"], "professor")
def test_admin_can_login_to_admin_panel(self):
admin = self._create_user(
email="admin@example.com",
password="AdminPass123!",
fullname="Admin User",
user_type=User.UserType.ADMIN,
)
admin.add_role("admin")
response = self.client.post(
reverse("admin-login"),
{"email": admin.email, "password": "AdminPass123!"},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data["user_type"], "admin")
def test_updating_user_to_student_revokes_panel_access(self):
target_user = self._create_user(
email="target@example.com",
password="TargetPass123!",
fullname="Target User",
user_type=User.UserType.PROFESSOR,
is_staff=True,
)
target_user.add_role("professor")
target_user.add_role("admin")
target_user.is_superuser = True
target_user.save(update_fields=["is_superuser", "is_staff"])
self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.super_admin_token.key}")
update_response = self.client.patch(
reverse("admin-users-detail", args=[target_user.id]),
{
"user_type": User.UserType.STUDENT,
"is_staff": True,
"is_superuser": True,
},
format="json",
)
self.assertEqual(update_response.status_code, status.HTTP_200_OK)
target_user.refresh_from_db()
self.assertEqual(target_user.user_type, User.UserType.STUDENT)
self.assertFalse(target_user.is_staff)
self.assertFalse(target_user.is_superuser)
self.assertFalse(target_user.has_role("professor"))
self.assertFalse(target_user.has_role("admin"))
self.client.credentials()
login_response = self.client.post(
reverse("admin-login"),
{"email": target_user.email, "password": "TargetPass123!"},
format="json",
)
self.assertEqual(login_response.status_code, status.HTTP_401_UNAUTHORIZED)

27
apps/account/views/user.py

@ -589,24 +589,14 @@ class AdminLoginView(CreateAPIView):
except User.DoesNotExist: except User.DoesNotExist:
raise ValidationError({"email": "No admin found with that information."}) raise ValidationError({"email": "No admin found with that information."})
# --- PANEL ACCESS CHECK ---
# Allow super_admin, admin (is_staff/is_superuser), professors
# Check both user_type field AND group membership (in case they differ)
ALLOWED_TYPES = ('professor', 'admin', 'super_admin')
is_panel_user = (
user_obj.is_staff or
user_obj.is_superuser or
getattr(user_obj, 'user_type', None) in ALLOWED_TYPES or
user_obj.groups.filter(name="Professor Group").exists()
)
if not is_panel_user:
raise AuthenticationFailed("No admin found with that information.")
# Use the actual stored email for authentication (ensures case matches DB) # Use the actual stored email for authentication (ensures case matches DB)
user = authenticate(request, username=user_obj.email, password=data['password']) user = authenticate(request, username=user_obj.email, password=data['password'])
if not user: if not user:
raise ValidationError({"password": "Password is incorrect"}) raise ValidationError({"password": "Password is incorrect"})
if not user.can_access_admin_panel():
raise AuthenticationFailed("No admin found with that information.")
user_timezone = serializer.validated_data.pop('timezone', None) user_timezone = serializer.validated_data.pop('timezone', None)
user.last_login = timezone.now() user.last_login = timezone.now()
user.is_active = True user.is_active = True
@ -621,11 +611,12 @@ class AdminLoginView(CreateAPIView):
device_os='web_admin' device_os='web_admin'
) )
# Determine effective user_type (user_type field may lag behind groups)
effective_user_type = user.user_type
if effective_user_type not in ('professor', 'admin', 'super_admin'):
if user.groups.filter(name="Professor Group").exists():
effective_user_type = 'professor'
if user.is_super_admin_panel_user():
effective_user_type = 'super_admin'
elif user.is_admin_panel_user():
effective_user_type = 'admin'
else:
effective_user_type = 'professor'
return Response({ return Response({
"id": user.id, "id": user.id,

Loading…
Cancel
Save