jumpserver/apps/users/api/user.py
fit2bot 3f4141ca0b
merge: with pam (#14911)
* perf: change i18n

* perf: pam

* perf: change translate

* perf: add check account

* perf: add date field

* perf: add account filter

* perf: remove some js

* perf: add account status action

* perf: update pam

* perf: 修改 discover account

* perf: update filter

* perf: update gathered account

* perf: 修改账号同步

* perf: squash migrations

* perf: update pam

* perf: change i18n

* perf: update account risk

* perf: 更新风险发现

* perf: remove css

* perf: Admin connection token

* perf: Add a switch to check connectivity after changing the password, and add a custom ssh command for push tasks

* perf: Modify account migration files

* perf: update pam

* perf: remove to check account dir

* perf: Admin connection token

* perf: update check account

* perf: 优化发送结果

* perf: update pam

* perf: update bulk update create

* perf: prepaire using thread timer for bulk_create_decorator

* perf: update bulk create decorator

* perf: 优化 playbook manager

* perf: 优化收集账号的报表

* perf: Update poetry

* perf: Update Dockerfile with new base image tag

* fix: Account migrate 0012 file

* perf: 修改备份

* perf: update pam

* fix: Expand resource_type filter to include raw type

* feat: PAM Service (#14552)

* feat: PAM Service

* perf: import package name

---------

Co-authored-by: jiangweidong <1053570670@qq.com>

* perf: Change secret dashboard (#14551)

Co-authored-by: feng <1304903146@qq.com>

* perf: update migrations

* perf: 修改支持 pam

* perf: Change secret record table dashboard

* perf: update status

* fix: Automation send report

* perf: Change secret report

* feat: windows accounts gather

* perf: update change status

* perf: Account backup

* perf: Account backup report

* perf: Account migrate

* perf: update service to application

* perf: update migrations

* perf: update logo

* feat: oracle accounts gather (#14571)

* feat: oracle accounts gather

* feat: sqlserver accounts gather

* feat: postgresql accounts gather

* feat: mysql accounts gather

---------

Co-authored-by: wangruidong <940853815@qq.com>

* feat: mongodb accounts gather

* perf: Change secret

* perf: Migrate

* perf: Merge conflicting migration files

* perf: Change secret

* perf: Automation filter org

* perf: Account push

* perf: Random secret string

* perf: Enhance SQL query and update risk handling in accounts

* perf: Ticket filter assignee_id

* perf: 修改 account remote

* perf: 修改一些 adhoc 任务

* perf: Change secret

* perf: Remove push account extra api

* perf: update status

* perf: The entire organization can view activity log

* fix: risk field check

* perf: add account details api

* perf: add demo mode

* perf: Delete gather_account

* perf: Perfect solution to account version problem

* perf: Update status action to handle multiple accounts

* perf: Add GatherAccountDetailField and update serializers

* perf: Display account history in combination with password change records

* perf: Lina translate

* fix: Update mysql_filter to handle nested user info

* perf: Admin connection token validate_permission account

* perf: copy move account

* perf: account filter risk

* perf: account risk filter

* perf: Copy move account failed message

* fix: gather account sync account to asset

* perf: Pam dashboard

* perf: Account dashboard total accounts

* perf: Pam dashboard

* perf: Change secret filter account secret_reset

* perf: 修改 risk filter

* perf: pam translate

* feat: Check for leaked duplicate passwords. (#14711)

* feat: Check for leaked duplicate passwords.

* perf: Use SQLite instead of txt as leak password database

---------

Co-authored-by: jiangweidong <1053570670@qq.com>
Co-authored-by: 老广 <ibuler@qq.com>

* perf: merge with remote

* perf: Add risk change_password_add handle

* perf: Pam dashboard

* perf: check account manager import

* perf: 重构扫描

* perf: 修改 db

* perf: Gather account manager

* perf: update change db lib

* perf: dashboard

* perf: Account gather

* perf: 修改 asset get queryset

* perf: automation report

* perf: Pam account

* perf: Pam dashboard api

* perf: risk add account

* perf: 修改 risk check

* perf: Risk account

* perf: update risk add reopen action

* perf: add pylintrc

* Revert "perf: automation report"

This reverts commit 22aee54207.

* perf: check account engine

* perf: Perf: Optimism Gather Report Style

* Perf: Remove unuser actions

* Perf: Perf push account

* perf: perf gather account

* perf: Automation report

* perf: Push account recorder

* perf: Push account record

* perf: Pam dashboard

* perf: perf

* perf: update intergration

* perf: integrations application detail add account tab page

* feat: Custom change password supports configuration of interactive items

* perf: Go and Python demo code

* perf: Custom secret change

* perf: add user filter

* perf: translate

* perf: Add demo code docs

* perf: update some i18n

* perf: update some i18n

* perf: Add Java, Node, Go, and cURL demo code

* perf: Translate

* perf: Change secret translate

* perf: Translate

* perf: update some i18n

* perf: translate

* perf: Ansible playbook

* perf: update some choice

* perf: update some choice

* perf: update account serializer remote unused code

* perf: conflict

* perf: update import

---------

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: feng <1304903146@qq.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: wangruidong <940853815@qq.com>
Co-authored-by: jiangweidong <1053570670@qq.com>
Co-authored-by: feng626 <57284900+feng626@users.noreply.github.com>
Co-authored-by: zhaojisen <1301338853@qq.com>
2025-02-21 16:39:57 +08:00

224 lines
8.1 KiB
Python

# ~*~ coding: utf-8 ~*~
from collections import defaultdict
from django.utils.translation import gettext as _
from rest_framework import generics
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from rest_framework_bulk.generics import BulkModelViewSet
from common.api import CommonApiMixin, SuggestionMixin
from common.drf.filters import AttrRulesFilterBackend
from common.utils import get_logger
from orgs.utils import current_org, tmp_to_root_org
from rbac.models import Role, RoleBinding
from rbac.permissions import RBACPermission
from users.utils import LoginBlockUtil, MFABlockUtils
from .mixins import UserQuerysetMixin
from .. import serializers
from ..exceptions import UnableToDeleteAllUsers
from ..filters import UserFilter
from ..models import User
from ..notifications import ResetMFAMsg
from ..permissions import UserObjectPermission
from ..serializers import (
UserSerializer, MiniUserSerializer, InviteSerializer, UserRetrieveSerializer
)
from ..signals import post_user_create
logger = get_logger(__name__)
__all__ = [
'UserViewSet', 'UserChangePasswordApi',
'UserUnblockPKApi', 'UserResetMFAApi',
]
class UserViewSet(CommonApiMixin, UserQuerysetMixin, SuggestionMixin, BulkModelViewSet):
filterset_class = UserFilter
extra_filter_backends = [AttrRulesFilterBackend]
search_fields = ('username', 'email', 'name')
permission_classes = [RBACPermission, UserObjectPermission]
serializer_classes = {
'default': UserSerializer,
'suggestion': MiniUserSerializer,
'invite': InviteSerializer,
'retrieve': UserRetrieveSerializer,
}
rbac_perms = {
'match': 'users.match_user',
'invite': 'users.invite_user',
'remove': 'users.remove_user',
'bulk_remove': 'users.remove_user',
}
def allow_bulk_destroy(self, qs, filtered):
is_valid = filtered.count() < qs.count()
if not is_valid:
raise UnableToDeleteAllUsers()
return True
def perform_destroy(self, instance):
if instance.username == 'admin':
raise PermissionDenied(_("Cannot delete the admin user. Please disable it instead."))
super().perform_destroy(instance)
@action(methods=['get'], detail=False, url_path='suggestions')
def match(self, request, *args, **kwargs):
with tmp_to_root_org():
return super().match(request, *args, **kwargs)
def get_serializer(self, *args, **kwargs):
"""重写 get_serializer, 用于设置用户的角色缓存
放到 paginate_queryset 里面会导致 导出有问题, 因为导出的时候,没有 pager
"""
if len(args) == 1 and kwargs.get('many'):
queryset = self.set_users_roles_for_cache(args[0])
queryset = self.set_users_orgs_roles(args[0])
args = (queryset,)
return super().get_serializer(*args, **kwargs)
@staticmethod
def set_users_roles_for_cache(queryset):
# Todo: 未来有机会用 SQL 实现
queryset_list = queryset
user_ids = [u.id for u in queryset_list]
role_bindings = RoleBinding.objects.filter(user__in=user_ids) \
.values('user_id', 'role_id', 'scope')
role_mapper = {r.id: r for r in Role.objects.all()}
user_org_role_mapper = defaultdict(set)
user_system_role_mapper = defaultdict(set)
for binding in role_bindings:
role_id = binding['role_id']
user_id = binding['user_id']
if binding['scope'] == RoleBinding.Scope.system:
user_system_role_mapper[user_id].add(role_mapper[role_id])
else:
user_org_role_mapper[user_id].add(role_mapper[role_id])
for u in queryset_list:
system_roles = user_system_role_mapper[u.id]
org_roles = user_org_role_mapper[u.id]
u.org_roles.cache_set(org_roles)
u.system_roles.cache_set(system_roles)
return queryset_list
@staticmethod
def set_users_orgs_roles(queryset):
user_ids = [u.id for u in queryset]
rbs = RoleBinding.objects_raw.filter(
user__in=user_ids, scope='org'
).prefetch_related('user', 'role', 'org')
user_rbs_mapper = defaultdict(set)
for rb in rbs:
user_rbs_mapper[rb.user_id].add(rb)
for u in queryset:
user_rbs = user_rbs_mapper[u.id]
orgs_roles = defaultdict(set)
for rb in user_rbs:
orgs_roles[rb.org_name].add(rb.role.display_name)
setattr(u, 'orgs_roles', orgs_roles)
return queryset
def perform_create(self, serializer):
users = serializer.save()
if isinstance(users, User):
users = [users]
self.send_created_signal(users)
def perform_bulk_update(self, serializer):
user_ids = [
d.get("id") or d.get("pk") for d in serializer.validated_data
]
users = current_org.get_members().filter(id__in=user_ids)
for user in users:
self.check_object_permissions(self.request, user)
return super().perform_bulk_update(serializer)
def perform_bulk_destroy(self, objects):
for obj in objects:
self.check_object_permissions(self.request, obj)
self.perform_destroy(obj)
@action(methods=['post'], detail=False)
def invite(self, request):
if not current_org or current_org.is_root():
error = {"error": "Not a valid org"}
return Response(error, status=400)
serializer_cls = self.get_serializer_class()
serializer = serializer_cls(data=request.data)
serializer.is_valid(raise_exception=True)
validated_data = serializer.validated_data
users = validated_data['users']
org_roles = validated_data['org_roles']
has_self = any([str(u.id) == str(request.user.id) for u in users])
if has_self and not request.user.is_superuser:
error = {"error": _("Can not invite self")}
return Response(error, status=400)
for user in users:
user.org_roles.set(org_roles)
return Response(serializer.data, status=201)
@action(methods=['post'], detail=True)
def remove(self, request, *args, **kwargs):
instance = self.get_object()
instance.remove()
return Response(status=204)
@action(methods=['post'], detail=False, url_path='remove')
def bulk_remove(self, request, *args, **kwargs):
qs = self.get_queryset()
filtered = self.filter_queryset(qs)
for instance in filtered:
instance.remove()
return Response(status=204)
def send_created_signal(self, users):
if not isinstance(users, list):
users = [users]
for user in users:
post_user_create.send(self.__class__, user=user)
class UserChangePasswordApi(UserQuerysetMixin, generics.UpdateAPIView):
serializer_class = serializers.ChangeUserPasswordSerializer
def perform_update(self, serializer):
user = self.get_object()
user.password_raw = serializer.validated_data["password"]
user.save()
class UserUnblockPKApi(UserQuerysetMixin, generics.UpdateAPIView):
serializer_class = serializers.UserSerializer
def perform_update(self, serializer):
user = self.get_object()
username = user.username if user else ''
LoginBlockUtil.unblock_user(username)
MFABlockUtils.unblock_user(username)
class UserResetMFAApi(UserQuerysetMixin, generics.RetrieveAPIView):
serializer_class = serializers.ResetOTPSerializer
def retrieve(self, request, *args, **kwargs):
user = self.get_object() if kwargs.get('pk') else request.user
if user == request.user:
msg = _("Could not reset self otp, use profile reset instead")
return Response({"error": msg}, status=400)
backends = user.active_mfa_backends_mapper
for backend in backends.values():
if backend.can_disable():
backend.disable()
ResetMFAMsg(user).publish_async()
return Response({"msg": "success"})