feat: add self-modification protection for user roles

Protect owners from accidentally locking themselves out by:
- Adding RoleService.can_modify_user_role() to centralize validation logic
- Blocking edit/delete operations on own role in views
- Hiding edit/delete buttons for own role in template

This prevents owners from:
- Changing their own role to a lower privilege level
- Deactivating themselves
- Deleting their own access

Standard admin pattern used by GitHub, WordPress, Django Admin.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-01 23:06:54 +03:00
parent ffc3b0c42d
commit f2c1f7e02d
3 changed files with 46 additions and 6 deletions

View File

@@ -98,3 +98,25 @@ class RoleService:
if not user_role: if not user_role:
return False return False
return user_role.code in role_codes return user_role.code in role_codes
@staticmethod
def can_modify_user_role(modifier_user, target_user_role):
"""
Проверяет, может ли пользователь изменить указанную роль.
Args:
modifier_user: Пользователь, который хочет изменить роль
target_user_role: UserRole объект, который нужно изменить
Returns:
tuple: (bool, str) - (можно ли изменить, причина отказа)
"""
# Защита от самоблокировки
if target_user_role.user == modifier_user:
return False, "Вы не можете изменить свою собственную роль"
# Можно добавить другие проверки в будущем:
# - Запрет удаления последнего owner'а
# - Проверка иерархии ролей и т.д.
return True, ""

View File

@@ -67,12 +67,18 @@
{% endif %} {% endif %}
</td> </td>
<td> <td>
<a href="{% url 'user_roles:edit' user_role.pk %}" class="btn btn-sm btn-outline-primary"> {% if user_role.user != request.user %}
<i class="bi bi-pencil"></i> Изменить <a href="{% url 'user_roles:edit' user_role.pk %}" class="btn btn-sm btn-outline-primary">
</a> <i class="bi bi-pencil"></i> Изменить
<a href="{% url 'user_roles:delete' user_role.pk %}" class="btn btn-sm btn-outline-danger"> </a>
<i class="bi bi-trash"></i> Удалить <a href="{% url 'user_roles:delete' user_role.pk %}" class="btn btn-sm btn-outline-danger">
</a> <i class="bi bi-trash"></i> Удалить
</a>
{% else %}
<span class="text-muted small">
<i class="bi bi-lock"></i> Ваша роль
</span>
{% endif %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@@ -64,6 +64,12 @@ def user_role_edit(request, pk):
"""Изменение роли пользователя""" """Изменение роли пользователя"""
user_role = get_object_or_404(UserRole, pk=pk) user_role = get_object_or_404(UserRole, pk=pk)
# Защита от самоблокировки
can_modify, error_message = RoleService.can_modify_user_role(request.user, user_role)
if not can_modify:
messages.error(request, error_message)
return redirect('user_roles:list')
if request.method == 'POST': if request.method == 'POST':
role_code = request.POST.get('role') role_code = request.POST.get('role')
is_active = request.POST.get('is_active') == 'on' is_active = request.POST.get('is_active') == 'on'
@@ -98,6 +104,12 @@ def user_role_delete(request, pk):
"""Удаление роли пользователя (отключение доступа)""" """Удаление роли пользователя (отключение доступа)"""
user_role = get_object_or_404(UserRole, pk=pk) user_role = get_object_or_404(UserRole, pk=pk)
# Защита от самоблокировки
can_modify, error_message = RoleService.can_modify_user_role(request.user, user_role)
if not can_modify:
messages.error(request, error_message)
return redirect('user_roles:list')
if request.method == 'POST': if request.method == 'POST':
email = user_role.user.email email = user_role.user.email
user_role.delete() user_role.delete()