Browse Source

feat: 个人设置 (#11494)

Co-authored-by: feng <1304903146@qq.com>
pull/11534/head
fit2bot 1 year ago committed by GitHub
parent
commit
a41909ec8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      apps/jumpserver/conf.py
  2. 2
      apps/jumpserver/settings/custom.py
  3. 2
      apps/settings/serializers/public.py
  4. 13
      apps/settings/serializers/terminal.py
  5. 1
      apps/users/api/__init__.py
  6. 93
      apps/users/api/preference.py
  7. 17
      apps/users/api/profile.py
  8. 34
      apps/users/const.py
  9. 77
      apps/users/migrations/0043_remove_user_secret_key_preference.py
  10. 3
      apps/users/models/__init__.py
  11. 39
      apps/users/models/preference.py
  12. 10
      apps/users/models/user.py
  13. 5
      apps/users/serializers/__init__.py
  14. 3
      apps/users/serializers/preference/__init__.py
  15. 15
      apps/users/serializers/preference/koko.py
  16. 30
      apps/users/serializers/preference/lina.py
  17. 62
      apps/users/serializers/preference/luna.py
  18. 24
      apps/users/serializers/profile.py
  19. 2
      apps/users/urls/api_urls.py

3
apps/jumpserver/conf.py

@ -457,9 +457,6 @@ class Config(dict):
'TERMINAL_SESSION_KEEP_DURATION': 200,
'TERMINAL_HOST_KEY': '',
'TERMINAL_COMMAND_STORAGE': {},
# Luna 页面
# 默认图形化分辨率
'TERMINAL_GRAPHICAL_RESOLUTION': 'Auto',
# 未来废弃(目前迁移会用)
'TERMINAL_RDP_ADDR': '',
# 保留(Luna还在用)

2
apps/jumpserver/settings/custom.py

@ -91,8 +91,6 @@ TERMINAL_HOST_KEY = CONFIG.TERMINAL_HOST_KEY
TERMINAL_HEADER_TITLE = CONFIG.TERMINAL_HEADER_TITLE
# TERMINAL_TELNET_REGEX = CONFIG.TERMINAL_TELNET_REGEX
# 默认图形化分辨率
TERMINAL_GRAPHICAL_RESOLUTION = CONFIG.TERMINAL_GRAPHICAL_RESOLUTION
# Asset user auth external backend, default AuthBook backend
BACKEND_ASSET_USER_AUTH_VAULT = False

2
apps/settings/serializers/public.py

@ -45,8 +45,6 @@ class PrivateSettingSerializer(PublicSettingSerializer):
TERMINAL_KOKO_SSH_ENABLED = serializers.BooleanField()
TERMINAL_OMNIDB_ENABLED = serializers.BooleanField()
TERMINAL_GRAPHICAL_RESOLUTION = serializers.CharField()
ANNOUNCEMENT_ENABLED = serializers.BooleanField()
ANNOUNCEMENT = serializers.DictField()

13
apps/settings/serializers/terminal.py

@ -39,16 +39,3 @@ class TerminalSettingSerializer(serializers.Serializer):
TERMINAL_MAGNUS_ENABLED = serializers.BooleanField(label=_("Enable database proxy"))
TERMINAL_RAZOR_ENABLED = serializers.BooleanField(label=_("Enable Razor"))
TERMINAL_KOKO_SSH_ENABLED = serializers.BooleanField(label=_("Enable SSH Client"))
RESOLUTION_CHOICES = (
('Auto', 'Auto'),
('1024x768', '1024x768'),
('1366x768', '1366x768'),
('1600x900', '1600x900'),
('1920x1080', '1920x1080')
)
TERMINAL_GRAPHICAL_RESOLUTION = serializers.ChoiceField(
default='Auto', choices=RESOLUTION_CHOICES, required=False,
label=_('Default graphics resolution'),
help_text=_('Tip: Default resolution to use when connecting graphical assets in Luna pages')
)

1
apps/users/api/__init__.py

@ -2,6 +2,7 @@
#
from .group import *
from .preference import *
from .profile import *
from .relation import *
from .service import *

93
apps/users/api/preference.py

@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
#
from rest_framework import generics
from rest_framework.serializers import Serializer
from common.permissions import IsValidUser
from common.utils import get_logger
from .. import serializers
from ..models import Preference
logger = get_logger(__file__)
class PreferenceApi(generics.RetrieveUpdateAPIView):
permission_classes = (IsValidUser,)
queryset = Preference.objects.all()
serializer_class_mapper = {
'lina': serializers.LinaSerializer,
'luna': serializers.LunaSerializer,
'koko': serializers.KokoSerializer,
}
def check_permissions(self, request):
if self.category not in self.serializer_class_mapper:
return self.permission_denied(request, 'category is invalid')
return super().check_permissions(request)
@property
def user(self):
return self.request.user
@property
def category(self):
return self.request.query_params.get('category')
def get_serializer_class(self):
cls = self.serializer_class_mapper.get(self.category)
return cls
def get_field_defaults(self, serializer):
field_defaults = {}
fields = serializer.get_fields()
for name, field in fields.items():
if isinstance(field, Serializer):
field_defaults[name] = self.get_field_defaults(field)
continue
field_defaults[name] = getattr(field, 'default', None)
return field_defaults
def get_encrypted_fields(self, serializer):
encrypted_fields = []
fields = serializer.get_fields()
for name, field in fields.items():
if isinstance(field, Serializer):
encrypted_fields += self.get_encrypted_fields(field)
continue
if not field.write_only:
continue
encrypted_fields.append(name)
return encrypted_fields
def get_object(self):
serializer = self.get_serializer_class()()
field_defaults = self.get_field_defaults(serializer)
qs = self.queryset.filter(user=self.user, category=self.category)
if not qs.exists():
return field_defaults
data = dict(qs.values_list('name', 'value'))
for k, v in data.items():
for d in field_defaults.values():
if k in d:
d[k] = v
break
return field_defaults
def perform_update(self, serializer):
user = self.user
category = self.category
model = self.queryset.model
encrypted_fields = self.get_encrypted_fields(serializer)
data = serializer.validated_data
for d in data.values():
for name, value in d.items():
kwargs = {'name': name, 'user': user}
defaults = {'category': category}
if name in encrypted_fields:
value = model.encrypt(value)
defaults['encrypted'] = True
defaults['value'] = value
defaults.update(kwargs)
model.objects.update_or_create(defaults, **kwargs)

17
apps/users/api/profile.py

@ -3,24 +3,23 @@ import uuid
from rest_framework import generics
from rest_framework.permissions import IsAuthenticated
from authentication.models import ConnectionToken
from common.permissions import IsValidUserOrConnectionToken
from common.utils import get_object_or_none
from orgs.utils import tmp_to_root_org
from authentication.models import ConnectionToken
from users.notifications import (
ResetPasswordMsg, ResetPasswordSuccessMsg, ResetSSHKeyMsg,
ResetPublicKeySuccessMsg,
)
from .mixins import UserQuerysetMixin
from .. import serializers
from ..models import User
from .mixins import UserQuerysetMixin
__all__ = [
'UserResetPasswordApi', 'UserResetPKApi',
'UserProfileApi', 'UserPasswordApi',
'UserSecretKeyApi', 'UserPublicKeyApi'
'UserPublicKeyApi'
]
@ -82,14 +81,6 @@ class UserPasswordApi(generics.RetrieveUpdateAPIView):
return resp
class UserSecretKeyApi(generics.RetrieveUpdateAPIView):
permission_classes = (IsAuthenticated,)
serializer_class = serializers.UserUpdateSecretKeySerializer
def get_object(self):
return self.request.user
class UserPublicKeyApi(generics.RetrieveUpdateAPIView):
permission_classes = (IsAuthenticated,)
serializer_class = serializers.UserUpdatePublicKeySerializer

34
apps/users/const.py

@ -17,3 +17,37 @@ class SystemOrOrgRole(TextChoices):
class PasswordStrategy(TextChoices):
email = 'email', _('Reset link will be generated and sent to the user')
custom = 'custom', _('Set password')
class RDPResolution(TextChoices):
AUTO = 'auto', _('AUTO')
RES_1024x768 = '1024x768', '1024x768'
RES_1366x768 = '1366x768', '1366x768'
RES_1600x900 = '1600x900', '1600x900'
RES_1920x1080 = '1920x1080', '1920x1080'
class RDPClientOption(TextChoices):
FULL_SCREEN = 'full_screen', _('Full screen')
MULTI_SCREEN = 'multi_screen', _('Multi screen')
DRIVES_REDIRECT = 'drives_redirect', _('Drives redirect')
class KeyboardLayout(TextChoices):
EN_US_QWERTY = 'en-us-qwerty', 'US English (Qwerty)'
EN_UK_QWERTY = 'en-gb-qwerty', 'UK English (Qwerty)'
JA_JP_QWERTY = 'ja-jp-qwerty', 'Japanese (Qwerty)'
FR_FR_AZERTY = 'fr-fr-azerty', 'French (Azerty)'
FR_CH_QWERTZ = 'fr-ch-qwertz', 'Swiss French (Qwertz)'
FR_BE_AZERTY = 'fr-be-azerty', 'Belgian French (Azerty)'
TR_TR_QWERTY = 'tr-tr-qwerty', 'Turkish-Q (Qwerty)'
class RemoteApplicationConnectionMethod(TextChoices):
WEB = 'web', _('Web')
CLIENT = 'client', _('Client')
class FileNameConflictResolution(TextChoices):
REPLACE = 'replace', _('Replace')
SUFFIX = 'suffix', _('Suffix')

77
apps/users/migrations/0043_remove_user_secret_key_preference.py

@ -0,0 +1,77 @@
# Generated by Django 4.1.10 on 2023-09-09 14:16
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
from common.db.utils import Encryptor
def migrate_secret_key(apps, *args):
user_model = apps.get_model('users', 'User')
preference_model = apps.get_model('users', 'Preference')
data = user_model.objects.filter(
secret_key__isnull=False
).values_list('id', 'secret_key')
objs = []
for user_id, secret_key in data:
secret_key = Encryptor(secret_key).encrypt()
objs.append(
preference_model(
name='secret_key', category='lina',
value=secret_key, encrypted=True, user_id=user_id
)
)
preference_model.objects.bulk_create(objs)
def migrate_graphical_resolution(apps, *args):
user_model = apps.get_model('users', 'User')
setting_model = apps.get_model('settings', 'Setting')
preference_model = apps.get_model('users', 'Preference')
s = setting_model.objects.filter(name='TERMINAL_GRAPHICAL_RESOLUTION').first()
if (s and s.value == 'Auto') or not s:
return
value = s.value
objs = []
for _id in user_model.objects.values_list('id', flat=True):
objs.append(
preference_model(
name='rdp_resolution', category='luna',
value=value, user_id=_id
)
)
preference_model.objects.bulk_create(objs)
class Migration(migrations.Migration):
dependencies = [
('users', '0042_auto_20230203_1201'),
]
operations = [
migrations.CreateModel(
name='Preference',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=128, verbose_name='Name')),
('category', models.CharField(max_length=128, verbose_name='Category')),
('value', models.TextField(blank=True, null=True, verbose_name='Value')),
('encrypted', models.BooleanField(default=False, verbose_name='Encrypted')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='preferences',
to=settings.AUTH_USER_MODEL, verbose_name='Users')),
],
options={
'verbose_name': 'Preference',
'db_table': 'users_preference',
'unique_together': {('name', 'user_id')},
},
),
migrations.RunPython(migrate_secret_key),
migrations.RunPython(migrate_graphical_resolution),
migrations.RemoveField(
model_name='user',
name='secret_key',
),
]

3
apps/users/models/__init__.py

@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-
#
from .user import *
from .group import *
from .preference import *
from .user import *
from .utils import *

39
apps/users/models/preference.py

@ -0,0 +1,39 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from common.db.utils import Encryptor
from common.utils import get_logger
logger = get_logger(__name__)
class Preference(models.Model):
name = models.CharField(max_length=128, verbose_name=_("Name"))
category = models.CharField(max_length=128, verbose_name=_('Category'))
value = models.TextField(verbose_name=_("Value"), null=True, blank=True)
encrypted = models.BooleanField(default=False, verbose_name=_('Encrypted'))
user = models.ForeignKey(
'users.User', verbose_name=_("Users"), related_name='preferences', on_delete=models.CASCADE
)
def __str__(self):
return f'{self.name}({self.user.username})'
@classmethod
def encrypt(cls, value):
return Encryptor(value).encrypt()
@classmethod
def decrypt(cls, value):
return Encryptor(value).decrypt()
@property
def decrypt_value(self):
if self.encrypted:
return self.decrypt(self.value)
return self.value
class Meta:
db_table = "users_preference"
verbose_name = _("Preference")
unique_together = [('name', 'user_id')]

10
apps/users/models/user.py

@ -819,9 +819,6 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, JSONFilterMixin, Abstract
public_key = fields.EncryptTextField(
blank=True, null=True, verbose_name=_('Public key')
)
secret_key = fields.EncryptCharField(
max_length=256, blank=True, null=True, verbose_name=_('Secret key')
)
comment = models.TextField(
blank=True, null=True, verbose_name=_('Comment')
)
@ -854,6 +851,13 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, JSONFilterMixin, Abstract
def __str__(self):
return '{0.name}({0.username})'.format(self)
@property
def secret_key(self):
instance = self.preferences.filter(name='secret_key').first()
if not instance:
return
return instance.decrypt_value
@property
def receive_backends(self):
return self.user_msg_subscription.receive_backends

5
apps/users/serializers/__init__.py

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
#
from .user import *
from .profile import *
from .group import *
from .preference import *
from .profile import *
from .realtion import *
from .user import *

3
apps/users/serializers/preference/__init__.py

@ -0,0 +1,3 @@
from .koko import *
from .lina import *
from .luna import *

15
apps/users/serializers/preference/koko.py

@ -0,0 +1,15 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from users.const import FileNameConflictResolution
class BasicSerializer(serializers.Serializer):
file_name_conflict_resolution = serializers.ChoiceField(
FileNameConflictResolution.choices, default=FileNameConflictResolution.REPLACE,
required=False, label=_('File name conflict resolution')
)
class KokoSerializer(serializers.Serializer):
basic = BasicSerializer(required=False, label=_('Basic'))

30
apps/users/serializers/preference/lina.py

@ -0,0 +1,30 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from common.serializers.fields import EncryptedField
class BasicSerializer(serializers.Serializer):
secret_key = EncryptedField(
required=False, max_length=1024,
write_only=True, allow_blank=True, label=_('Secret Key')
)
secret_key_again = EncryptedField(
required=False, max_length=1024,
write_only=True, allow_blank=True, label=_('Secret Key Again')
)
def validate(self, attrs):
secret_key = attrs.pop('secret_key', None)
secret_key_again = attrs.pop('secret_key_again', None)
if (secret_key or secret_key_again) and secret_key != secret_key_again:
msg = _('The newly set password is inconsistent')
raise serializers.ValidationError({'secret_key_again': msg})
elif secret_key and secret_key_again:
attrs['secret_key'] = secret_key
return attrs
class LinaSerializer(serializers.Serializer):
basic = BasicSerializer(required=False, label=_('Basic'))

62
apps/users/serializers/preference/luna.py

@ -0,0 +1,62 @@
import json
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from users.const import RDPResolution, KeyboardLayout, RDPClientOption, RemoteApplicationConnectionMethod
class MultipleChoiceField(serializers.MultipleChoiceField):
def to_representation(self, keys):
if isinstance(keys, str):
keys = json.loads(keys)
return keys
def to_internal_value(self, data):
data = super().to_internal_value(data)
return json.dumps(list(data))
class BasicSerializer(serializers.Serializer):
is_async_asset_tree = serializers.BooleanField(
required=False, default=True, label=_('Async loading of asset tree')
)
class GraphicsSerializer(serializers.Serializer):
rdp_resolution = serializers.ChoiceField(
RDPResolution.choices, default=RDPResolution.AUTO,
required=False, label=_('RDP resolution')
)
keyboard_layout = serializers.ChoiceField(
KeyboardLayout.choices, default=KeyboardLayout.EN_US_QWERTY,
required=False, label=_('Keyboard layout')
)
rdp_client_option = MultipleChoiceField(
choices=RDPClientOption.choices, default={RDPClientOption.FULL_SCREEN},
label=_('RDP client option'), required=False
)
remote_application_connection_method = serializers.ChoiceField(
RemoteApplicationConnectionMethod.choices, default=RemoteApplicationConnectionMethod.WEB,
required=False, label=_('Remote application connection method')
)
class CommandLineSerializer(serializers.Serializer):
character_terminal_font_size = serializers.IntegerField(
default=14, min_value=1, max_value=9999, required=False,
label=_('Character terminal font size'),
)
is_backspace_as_ctrl_h = serializers.BooleanField(
required=False, default=False, label=_('Backspace as Ctrl+H')
)
is_right_click_quickly_paste = serializers.BooleanField(
required=False, default=False, label=_('Right click quickly paste')
)
class LunaSerializer(serializers.Serializer):
basic = BasicSerializer(required=False, label=_('Basic'))
graphics = GraphicsSerializer(required=False, label=_('Graphics'))
command_line = CommandLineSerializer(required=False, label=_('Command line'))

24
apps/users/serializers/profile.py

@ -55,30 +55,6 @@ class UserUpdatePasswordSerializer(serializers.ModelSerializer):
return instance
class UserUpdateSecretKeySerializer(serializers.ModelSerializer):
new_secret_key = EncryptedField(required=True, max_length=128)
new_secret_key_again = EncryptedField(required=True, max_length=128)
has_secret_key = serializers.BooleanField(read_only=True, source='secret_key')
class Meta:
model = User
fields = ['has_secret_key', 'new_secret_key', 'new_secret_key_again']
def validate(self, values):
new_secret_key = values.get('new_secret_key', '')
new_secret_key_again = values.get('new_secret_key_again', '')
if new_secret_key != new_secret_key_again:
msg = _('The newly set password is inconsistent')
raise serializers.ValidationError({'new_secret_key_again': msg})
return values
def update(self, instance, validated_data):
new_secret_key = self.validated_data.get('new_secret_key')
instance.secret_key = new_secret_key
instance.save()
return instance
class UserUpdatePublicKeySerializer(serializers.ModelSerializer):
public_key_comment = serializers.CharField(
source='get_public_key_comment', required=False, read_only=True, max_length=128

2
apps/users/urls/api_urls.py

@ -22,9 +22,9 @@ router.register(r'connection-token', auth_api.ConnectionTokenViewSet, 'connectio
urlpatterns = [
path('profile/', api.UserProfileApi.as_view(), name='user-profile'),
path('profile/password/', api.UserPasswordApi.as_view(), name='user-password'),
path('profile/secret-key/', api.UserSecretKeyApi.as_view(), name='user-secret-key'),
path('profile/public-key/', api.UserPublicKeyApi.as_view(), name='user-public-key'),
path('profile/mfa/reset/', api.UserResetMFAApi.as_view(), name='my-mfa-reset'),
path('preference/', api.PreferenceApi.as_view(), name='preference'),
path('users/<uuid:pk>/mfa/reset/', api.UserResetMFAApi.as_view(), name='user-reset-mfa'),
path('users/<uuid:pk>/password/', api.UserChangePasswordApi.as_view(), name='change-user-password'),
path('users/<uuid:pk>/password/reset/', api.UserResetPasswordApi.as_view(), name='user-reset-password'),

Loading…
Cancel
Save