jumpserver/apps/common/serializers/fields.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

307 lines
9.8 KiB
Python

# -*- coding: utf-8 -*-
#
import phonenumbers
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Model
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.fields import ChoiceField, empty
from common.db.fields import TreeChoices, JSONManyToManyField as ModelJSONManyToManyField
from common.utils import decrypt_password, is_uuid
__all__ = [
"ReadableHiddenField",
"EncryptedField",
"LabeledChoiceField",
"ObjectRelatedField",
"BitChoicesField",
"TreeChoicesField",
"LabeledMultipleChoiceField",
"PhoneField",
"JSONManyToManyField",
"LabelRelatedField",
]
# ReadableHiddenField
# -------------------
class ReadableHiddenField(serializers.HiddenField):
"""可读的 HiddenField"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.write_only = False
def to_representation(self, value):
if hasattr(value, "id"):
return getattr(value, "id")
return value
class EncryptedField(serializers.CharField):
def __init__(self, write_only=None, **kwargs):
if write_only is None:
write_only = True
kwargs["write_only"] = write_only
super().__init__(**kwargs)
def to_internal_value(self, value):
value = super().to_internal_value(value)
return decrypt_password(value)
class LabeledChoiceField(ChoiceField):
def to_representation(self, key):
if key is None:
return key
label = self.choices.get(key, key)
return {"value": key, "label": label}
def to_internal_value(self, data):
if isinstance(data, dict):
data = data.get("value")
if isinstance(data, str) and "(" in data and data.endswith(")"):
data = data.strip(")").split('(')[-1]
return super(LabeledChoiceField, self).to_internal_value(data)
class LabeledMultipleChoiceField(serializers.MultipleChoiceField):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.choice_mapper = {
key: value for key, value in self.choices.items()
}
def to_representation(self, keys):
if keys is None:
return keys
return [
{"value": key, "label": self.choice_mapper.get(key)}
for key in keys
]
def to_internal_value(self, data):
if not data:
return data
if isinstance(data[0], dict):
return [item.get("value") for item in data]
else:
return data
class LabelRelatedField(serializers.RelatedField):
def __init__(self, **kwargs):
queryset = kwargs.pop("queryset", None)
if queryset is None:
from labels.models import LabeledResource
queryset = LabeledResource.objects.all()
kwargs = {**kwargs}
read_only = kwargs.get("read_only", False)
if not read_only:
kwargs["queryset"] = queryset
super().__init__(**kwargs)
def to_file_representation(self, value):
if value is None:
return value
return "{}:{}".format(value.get('name'), value.get('value'))
def to_representation(self, value):
if value is None:
return value
label = value.label
if not label:
return None
return {'id': label.id, 'name': label.name, 'value': label.value, 'color': label.color}
def to_internal_value(self, data):
from labels.models import LabeledResource, Label
if data is None:
return data
if isinstance(data, dict) and (data.get("id") or data.get("pk")):
pk = data.get("id") or data.get("pk")
label = Label.objects.get(pk=pk)
elif is_uuid(data):
label = Label.objects.get(pk=data)
else:
if isinstance(data, dict):
k = data.get("name")
v = data.get("value")
elif isinstance(data, str) and ":" in data:
k, v = [x.strip() for x in data.split(":", 1)]
else:
raise serializers.ValidationError(_("Invalid data type"))
label, __ = Label.objects.get_or_create(name=k, value=v, defaults={'name': k, 'value': v})
return LabeledResource(label=label)
class ObjectRelatedField(serializers.RelatedField):
default_error_messages = {
"required": _("This field is required."),
"does_not_exist": _('Invalid pk "{pk_value}" - object does not exist.'),
"incorrect_type": _("Incorrect type. Expected pk value, received {data_type}."),
}
def __init__(self, **kwargs):
self.attrs = kwargs.pop("attrs", None) or ("id", "name")
self.many = kwargs.get("many", False)
super().__init__(**kwargs)
def to_representation(self, value):
data = {}
for attr in self.attrs:
if not hasattr(value, attr):
continue
data[attr] = getattr(value, attr)
return data
def to_internal_value(self, data):
queryset = self.get_queryset()
if isinstance(data, Model):
return queryset.get(pk=data.pk)
if not isinstance(data, dict):
pk = data
else:
pk = data.get("id") or data.get("pk") or data.get(self.attrs[0])
try:
if isinstance(data, bool):
raise TypeError
return queryset.get(pk=pk)
except ObjectDoesNotExist:
self.fail("does_not_exist", pk_value=pk)
except (TypeError, ValueError):
self.fail("incorrect_type", data_type=type(pk).__name__)
class TreeChoicesField(serializers.MultipleChoiceField):
def __init__(self, choice_cls, **kwargs):
assert issubclass(choice_cls, TreeChoices)
choices = [(c.name, c.label) for c in choice_cls]
self.tree = choice_cls.tree()
self._choice_cls = choice_cls
super().__init__(choices=choices, **kwargs)
def to_internal_value(self, data):
if not data:
return data
if isinstance(data[0], dict):
return [item.get("value") for item in data]
else:
return data
class BitChoicesField(TreeChoicesField):
"""
位字段
"""
def to_representation(self, value):
if isinstance(value, list) and len(value) == 1:
# Swagger 会使用 field.choices.keys() 迭代传递进来
return [
{"value": c.name, "label": c.label}
for c in self._choice_cls
if c.name == value[0]
]
return [
{"value": c.name, "label": c.label}
for c in self._choice_cls
if c.value & value == c.value
]
def to_internal_value(self, data):
if not isinstance(data, list):
raise serializers.ValidationError(_("Invalid data type, should be list"))
value = 0
if not data:
return value
if isinstance(data[0], dict):
data = [d["value"] for d in data]
# 所有的
if "all" in data:
for c in self._choice_cls:
value |= c.value
return value
name_value_map = {c.name: c.value for c in self._choice_cls}
for name in data:
if name not in name_value_map:
raise serializers.ValidationError(_("Invalid choice: {}").format(name))
value |= name_value_map[name]
return value
def run_validation(self, data=empty):
"""
备注:
创建授权规则不包含 actions 字段时, 会使用默认值(AssetPermission 中设置),
会直接使用 ['connect', '...'] 等字段保存到数据库,导致类型错误
这里将获取到的值再执行一下 to_internal_value 方法, 转化为内部值
"""
data = super().run_validation(data)
if isinstance(data, int):
return data
value = self.to_internal_value(data)
self.run_validators(value)
return value
class PhoneField(serializers.CharField):
def to_internal_value(self, data):
if isinstance(data, dict):
code = data.get('code')
phone = data.get('phone', '')
if code and phone:
code = code.replace('+', '')
data = '+{}{}'.format(code, phone)
else:
data = phone
if data:
try:
phone = phonenumbers.parse(data, 'CN')
data = '+{}{}'.format(phone.country_code, phone.national_number)
except phonenumbers.NumberParseException:
data = '+86{}'.format(data)
return super().to_internal_value(data)
def to_representation(self, value):
try:
phone = phonenumbers.parse(value, 'CN')
value = {'code': '+%s' % phone.country_code, 'phone': phone.national_number}
except phonenumbers.NumberParseException:
value = {'code': '+86', 'phone': value}
return value
class JSONManyToManyField(serializers.JSONField):
def to_representation(self, manager):
if manager is None:
return manager
value = manager.value
if not isinstance(value, dict):
return {"type": "ids", "ids": []}
if value.get("type") == "ids":
valid_ids = manager.all().values_list("id", flat=True)
valid_ids = [str(i) for i in valid_ids]
return {"type": "ids", "ids": valid_ids}
return value
def to_internal_value(self, data):
if not data:
data = {}
try:
data = super().to_internal_value(data)
ModelJSONManyToManyField.check_value(data)
except ValueError as e:
raise serializers.ValidationError(e)
return super().to_internal_value(data)