From 32ae77c42dbfa9638c83d580ef573ad4036324fd Mon Sep 17 00:00:00 2001 From: wangruidong <940853815@qq.com> Date: Tue, 13 Aug 2024 16:06:01 +0800 Subject: [PATCH] perf: add TERMINAL_SSH_KEY_LIMIT_COUNT conf --- apps/authentication/api/ssh_key.py | 6 +--- apps/authentication/serializers/ssh_key.py | 30 +++++++++++++--- apps/authentication/urls/view_urls.py | 1 + apps/i18n/core/ja/LC_MESSAGES/django.po | 38 +++++++++++++++----- apps/i18n/core/zh/LC_MESSAGES/django.po | 38 +++++++++++++++----- apps/i18n/core/zh_Hant/LC_MESSAGES/django.po | 38 +++++++++++++++----- apps/jumpserver/conf.py | 1 + apps/jumpserver/settings/custom.py | 3 +- apps/users/exceptions.py | 6 +++- apps/users/models/user/_auth.py | 10 ++++++ apps/users/views/profile/__init__.py | 1 + apps/users/views/profile/pubkey.py | 31 ++++++++++++++++ 12 files changed, 167 insertions(+), 36 deletions(-) create mode 100644 apps/users/views/profile/pubkey.py diff --git a/apps/authentication/api/ssh_key.py b/apps/authentication/api/ssh_key.py index e851e714e..2da19a5b8 100644 --- a/apps/authentication/api/ssh_key.py +++ b/apps/authentication/api/ssh_key.py @@ -1,8 +1,3 @@ -from django.utils import timezone -from rest_framework.response import Response -from rest_framework.decorators import action - -from rbac.permissions import RBACPermission from common.api import JMSModelViewSet from common.permissions import IsValidUser from ..serializers import SSHKeySerializer @@ -14,6 +9,7 @@ class SSHkeyViewSet(JMSModelViewSet): permission_classes = [IsValidUser] filterset_fields = ('name', 'is_active') search_fields = ('name',) + ordering = ('-date_last_used', '-date_created') def get_queryset(self): return self.request.user.ssh_keys.all() diff --git a/apps/authentication/serializers/ssh_key.py b/apps/authentication/serializers/ssh_key.py index 4d06a47c3..388607903 100644 --- a/apps/authentication/serializers/ssh_key.py +++ b/apps/authentication/serializers/ssh_key.py @@ -1,14 +1,22 @@ # -*- coding: utf-8 -*- # -from django.utils import timezone +from django.db.models import TextChoices from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from common.serializers.fields import ReadableHiddenField +from common.serializers.fields import ReadableHiddenField, LabeledChoiceField + from ..models import SSHKey from common.utils import validate_ssh_public_key +from users.exceptions import CreateSSHKeyExceedLimit -__all__ = ['SSHKeySerializer'] +__all__ = ['SSHKeySerializer', 'GenerateKeyType'] + + +class GenerateKeyType(TextChoices): + auto = 'auto', _('Automatically Generate Key Pair') + # 目前只支持sftp方式 + load = 'load', _('Import Existing Key Pair') class SSHKeySerializer(serializers.ModelSerializer): @@ -19,16 +27,22 @@ class SSHKeySerializer(serializers.ModelSerializer): public_key_hash_md5 = serializers.CharField( source='get_public_key_hash_md5', required=False, read_only=True, max_length=128 ) + generate_key_type = LabeledChoiceField( + choices=GenerateKeyType.choices, label=_('Create Type'), default=GenerateKeyType.auto.value, required=False, + help_text=_( + 'Please download the private key after creation. Each private key can only be downloaded once' + ) + ) class Meta: model = SSHKey fields_mini = ['name'] fields_small = fields_mini + [ - 'public_key', 'is_active', + 'public_key', 'is_active', 'comment' ] read_only_fields = [ 'id', 'user', 'public_key_comment', 'public_key_hash_md5', - 'date_last_used', 'date_created', 'date_updated' + 'date_last_used', 'date_created', 'date_updated', 'generate_key_type', ] fields = fields_small + read_only_fields @@ -42,3 +56,9 @@ class SSHKeySerializer(serializers.ModelSerializer): if not validate_ssh_public_key(value): raise serializers.ValidationError(_('Not a valid ssh public key')) return value + + def create(self, validated_data): + if not self.context["request"].user.can_create_ssh_key(): + raise CreateSSHKeyExceedLimit() + validated_data.pop('generate_key_type', None) + return super().create(validated_data) diff --git a/apps/authentication/urls/view_urls.py b/apps/authentication/urls/view_urls.py index 3df50260e..e1ffbea94 100644 --- a/apps/authentication/urls/view_urls.py +++ b/apps/authentication/urls/view_urls.py @@ -63,6 +63,7 @@ urlpatterns = [ # Profile path('profile/mfa/', users_view.MFASettingView.as_view(), name='user-mfa-setting'), + path('profile/pubkey/generate/', users_view.UserPublicKeyGenerateView.as_view(), name='user-pubkey-generate'), # OTP Setting path('profile/otp/enable/start/', users_view.UserOtpEnableStartView.as_view(), name='user-otp-enable-start'), diff --git a/apps/i18n/core/ja/LC_MESSAGES/django.po b/apps/i18n/core/ja/LC_MESSAGES/django.po index 251e2ec2d..c591ac979 100644 --- a/apps/i18n/core/ja/LC_MESSAGES/django.po +++ b/apps/i18n/core/ja/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-08-12 18:34+0800\n" +"POT-Creation-Date: 2024-08-13 16:47+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -1582,7 +1582,7 @@ msgid "Gather facts" msgstr "資産情報の収集" #: assets/const/base.py:32 audits/const.py:58 -#: terminal/serializers/applet_host.py:32 users/models/user/_auth.py:31 +#: terminal/serializers/applet_host.py:32 users/models/user/_auth.py:32 msgid "Disabled" msgstr "無効" @@ -2080,7 +2080,7 @@ msgstr "設定" #: assets/models/platform.py:38 audits/const.py:59 #: authentication/backends/passkey/models.py:11 settings/models.py:38 -#: terminal/serializers/applet_host.py:33 users/models/user/_auth.py:32 +#: terminal/serializers/applet_host.py:33 users/models/user/_auth.py:33 msgid "Enabled" msgstr "有効化" @@ -3119,7 +3119,7 @@ msgstr "MFAコードを入力してください" msgid "Please enter SMS code" msgstr "SMSコードを入力してください" -#: authentication/errors/failed.py:164 users/exceptions.py:15 +#: authentication/errors/failed.py:164 users/exceptions.py:14 msgid "Phone not set" msgstr "電話が設定されていない" @@ -3483,7 +3483,25 @@ msgstr "組織名" msgid "The {} cannot be empty" msgstr "{} 空にしてはならない" -#: authentication/serializers/ssh_key.py:43 users/forms/profile.py:161 +#: authentication/serializers/ssh_key.py:17 +msgid "Automatically Generate Key Pair" +msgstr "キーペアを自動作成" + +#: authentication/serializers/ssh_key.py:19 +msgid "Import Existing Key Pair" +msgstr "既存のキーペアをインポート" + +#: authentication/serializers/ssh_key.py:31 +msgid "Create Type" +msgstr "タイプを作成" + +#: authentication/serializers/ssh_key.py:33 +msgid "" +"Please download the private key after creation. Each private key can only be " +"downloaded once" +msgstr "作成完了後、秘密鍵をダウンロードしてください。各秘密鍵のダウンロードは一度きりです" + +#: authentication/serializers/ssh_key.py:57 users/forms/profile.py:161 #: users/serializers/profile.py:133 users/serializers/profile.py:160 msgid "Not a valid ssh public key" msgstr "有効なssh公開鍵ではありません" @@ -8445,14 +8463,18 @@ msgstr "置換" msgid "Suffix" msgstr "接尾辞を付ける" -#: users/exceptions.py:10 +#: users/exceptions.py:9 msgid "MFA not enabled" msgstr "MFAが有効化されていません" -#: users/exceptions.py:20 +#: users/exceptions.py:19 msgid "Unable to delete all users" msgstr "すべてのユーザーを削除できません" +#: users/exceptions.py:24 +msgid "Create failed. The number of SSH keys has reached the limit" +msgstr "作成に失敗しました。SSHキーの数が上限に達しました" + #: users/forms/profile.py:48 msgid "" "When enabled, you will enter the MFA binding process the next time you log " @@ -8578,7 +8600,7 @@ msgstr "ユーザーに一致できます" msgid "User password history" msgstr "ユーザーパスワード履歴" -#: users/models/user/_auth.py:33 +#: users/models/user/_auth.py:34 msgid "Force enabled" msgstr "強制有効" diff --git a/apps/i18n/core/zh/LC_MESSAGES/django.po b/apps/i18n/core/zh/LC_MESSAGES/django.po index 91adbab06..8b40a7007 100644 --- a/apps/i18n/core/zh/LC_MESSAGES/django.po +++ b/apps/i18n/core/zh/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-08-12 18:34+0800\n" +"POT-Creation-Date: 2024-08-13 16:47+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -1571,7 +1571,7 @@ msgid "Gather facts" msgstr "收集资产信息" #: assets/const/base.py:32 audits/const.py:58 -#: terminal/serializers/applet_host.py:32 users/models/user/_auth.py:31 +#: terminal/serializers/applet_host.py:32 users/models/user/_auth.py:32 msgid "Disabled" msgstr "禁用" @@ -2067,7 +2067,7 @@ msgstr "设置" #: assets/models/platform.py:38 audits/const.py:59 #: authentication/backends/passkey/models.py:11 settings/models.py:38 -#: terminal/serializers/applet_host.py:33 users/models/user/_auth.py:32 +#: terminal/serializers/applet_host.py:33 users/models/user/_auth.py:33 msgid "Enabled" msgstr "启用" @@ -3083,7 +3083,7 @@ msgstr "请输入 MFA 验证码" msgid "Please enter SMS code" msgstr "请输入短信验证码" -#: authentication/errors/failed.py:164 users/exceptions.py:15 +#: authentication/errors/failed.py:164 users/exceptions.py:14 msgid "Phone not set" msgstr "手机号没有设置" @@ -3443,7 +3443,25 @@ msgstr "组织名称" msgid "The {} cannot be empty" msgstr "{} 不能为空" -#: authentication/serializers/ssh_key.py:43 users/forms/profile.py:161 +#: authentication/serializers/ssh_key.py:17 +msgid "Automatically Generate Key Pair" +msgstr "自动创建密钥对" + +#: authentication/serializers/ssh_key.py:19 +msgid "Import Existing Key Pair" +msgstr "导入已有密钥对" + +#: authentication/serializers/ssh_key.py:31 +msgid "Create Type" +msgstr "创建类型" + +#: authentication/serializers/ssh_key.py:33 +msgid "" +"Please download the private key after creation. Each private key can only be " +"downloaded once" +msgstr "创建完成后请下载私钥,每个私钥只有一次下载机会" + +#: authentication/serializers/ssh_key.py:57 users/forms/profile.py:161 #: users/serializers/profile.py:133 users/serializers/profile.py:160 msgid "Not a valid ssh public key" msgstr "SSH密钥不合法" @@ -8293,14 +8311,18 @@ msgstr "替换" msgid "Suffix" msgstr "加后缀" -#: users/exceptions.py:10 +#: users/exceptions.py:9 msgid "MFA not enabled" msgstr "MFA 多因子认证没有开启" -#: users/exceptions.py:20 +#: users/exceptions.py:19 msgid "Unable to delete all users" msgstr "无法删除全部用户" +#: users/exceptions.py:24 +msgid "Create failed. The number of SSH keys has reached the limit" +msgstr "创建失败,SSH密钥数量已达到上限" + #: users/forms/profile.py:48 msgid "" "When enabled, you will enter the MFA binding process the next time you log " @@ -8426,7 +8448,7 @@ msgstr "可以匹配用户" msgid "User password history" msgstr "用户密码历史" -#: users/models/user/_auth.py:33 +#: users/models/user/_auth.py:34 msgid "Force enabled" msgstr "强制启用" diff --git a/apps/i18n/core/zh_Hant/LC_MESSAGES/django.po b/apps/i18n/core/zh_Hant/LC_MESSAGES/django.po index 58589d317..f085f8710 100644 --- a/apps/i18n/core/zh_Hant/LC_MESSAGES/django.po +++ b/apps/i18n/core/zh_Hant/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-08-12 18:34+0800\n" +"POT-Creation-Date: 2024-08-13 16:47+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -1573,7 +1573,7 @@ msgid "Gather facts" msgstr "收集資產資訊" #: assets/const/base.py:32 audits/const.py:58 -#: terminal/serializers/applet_host.py:32 users/models/user/_auth.py:31 +#: terminal/serializers/applet_host.py:32 users/models/user/_auth.py:32 msgid "Disabled" msgstr "禁用" @@ -2069,7 +2069,7 @@ msgstr "設置" #: assets/models/platform.py:38 audits/const.py:59 #: authentication/backends/passkey/models.py:11 settings/models.py:38 -#: terminal/serializers/applet_host.py:33 users/models/user/_auth.py:32 +#: terminal/serializers/applet_host.py:33 users/models/user/_auth.py:33 msgid "Enabled" msgstr "啟用" @@ -3085,7 +3085,7 @@ msgstr "請輸入 MFA 驗證碼" msgid "Please enter SMS code" msgstr "請輸入簡訊驗證碼" -#: authentication/errors/failed.py:164 users/exceptions.py:15 +#: authentication/errors/failed.py:164 users/exceptions.py:14 msgid "Phone not set" msgstr "手機號碼沒有設置" @@ -3445,7 +3445,25 @@ msgstr "組織名稱" msgid "The {} cannot be empty" msgstr "{} 不能為空" -#: authentication/serializers/ssh_key.py:43 users/forms/profile.py:161 +#: authentication/serializers/ssh_key.py:17 +msgid "Automatically Generate Key Pair" +msgstr "自動創建密鑰對" + +#: authentication/serializers/ssh_key.py:19 +msgid "Import Existing Key Pair" +msgstr "導入已有密鑰對" + +#: authentication/serializers/ssh_key.py:31 +msgid "Create Type" +msgstr "創建類型" + +#: authentication/serializers/ssh_key.py:33 +msgid "" +"Please download the private key after creation. Each private key can only be " +"downloaded once" +msgstr "創建完成後請下載私鑰,每個私鑰僅有一次下載機會" + +#: authentication/serializers/ssh_key.py:57 users/forms/profile.py:161 #: users/serializers/profile.py:133 users/serializers/profile.py:160 msgid "Not a valid ssh public key" msgstr "SSH金鑰不合法" @@ -8296,14 +8314,18 @@ msgstr "替換" msgid "Suffix" msgstr "加後綴" -#: users/exceptions.py:10 +#: users/exceptions.py:9 msgid "MFA not enabled" msgstr "MFA 多因子認證沒有開啟" -#: users/exceptions.py:20 +#: users/exceptions.py:19 msgid "Unable to delete all users" msgstr "無法刪除全部用戶" +#: users/exceptions.py:24 +msgid "Create failed. The number of SSH keys has reached the limit" +msgstr "創建失敗,SSH密鑰數量已達到上限" + #: users/forms/profile.py:48 msgid "" "When enabled, you will enter the MFA binding process the next time you log " @@ -8429,7 +8451,7 @@ msgstr "可以匹配用戶" msgid "User password history" msgstr "用戶密碼歷史" -#: users/models/user/_auth.py:33 +#: users/models/user/_auth.py:34 msgid "Force enabled" msgstr "強制啟用" diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index d44fb8bbe..2642c1da0 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -509,6 +509,7 @@ class Config(dict): # Terminal配置 'TERMINAL_PASSWORD_AUTH': True, 'TERMINAL_PUBLIC_KEY_AUTH': True, + 'TERMINAL_SSH_KEY_LIMIT_COUNT': 10, 'TERMINAL_HEARTBEAT_INTERVAL': 20, 'TERMINAL_ASSET_LIST_SORT_BY': 'name', 'TERMINAL_ASSET_LIST_PAGE_SIZE': 'auto', diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index 35e6f246c..821c209e1 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -83,6 +83,7 @@ CACHE_LOGIN_PASSWORD_TTL = CONFIG.CACHE_LOGIN_PASSWORD_TTL # Terminal other setting TERMINAL_PASSWORD_AUTH = CONFIG.TERMINAL_PASSWORD_AUTH TERMINAL_PUBLIC_KEY_AUTH = CONFIG.TERMINAL_PUBLIC_KEY_AUTH +TERMINAL_SSH_KEY_LIMIT_COUNT = CONFIG.TERMINAL_SSH_KEY_LIMIT_COUNT TERMINAL_HEARTBEAT_INTERVAL = CONFIG.TERMINAL_HEARTBEAT_INTERVAL TERMINAL_ASSET_LIST_SORT_BY = CONFIG.TERMINAL_ASSET_LIST_SORT_BY TERMINAL_ASSET_LIST_PAGE_SIZE = CONFIG.TERMINAL_ASSET_LIST_PAGE_SIZE @@ -239,4 +240,4 @@ ANSIBLE_RECEPTOR_TCP_LISTEN_ADDRESS = CONFIG.ANSIBLE_RECEPTOR_TCP_LISTEN_ADDRESS LOKI_LOG_ENABLED = CONFIG.LOKI_LOG_ENABLED LOKI_BASE_URL = CONFIG.LOKI_BASE_URL -TOOL_USER_ENABLED = CONFIG.TOOL_USER_ENABLED \ No newline at end of file +TOOL_USER_ENABLED = CONFIG.TOOL_USER_ENABLED diff --git a/apps/users/exceptions.py b/apps/users/exceptions.py index e062e367a..4d9f51b1c 100644 --- a/apps/users/exceptions.py +++ b/apps/users/exceptions.py @@ -1,6 +1,5 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import status - from common.exceptions import JMSException @@ -18,3 +17,8 @@ class PhoneNotSet(JMSException): class UnableToDeleteAllUsers(JMSException): default_code = 'unable_to_delete_all_users' default_detail = _('Unable to delete all users') + + +class CreateSSHKeyExceedLimit(JMSException): + default_code = 'create_ssh_key_exceed_limit' + default_detail = _('Create failed. The number of SSH keys has reached the limit') diff --git a/apps/users/models/user/_auth.py b/apps/users/models/user/_auth.py index 4b014b0e5..cd38dda51 100644 --- a/apps/users/models/user/_auth.py +++ b/apps/users/models/user/_auth.py @@ -18,6 +18,7 @@ from common.utils import ( lazyproperty, ) from users.signals import post_user_change_password +from users.exceptions import CreateSSHKeyExceedLimit logger = get_logger(__file__) @@ -133,6 +134,15 @@ class AuthMixin: post_user_change_password.send(self.__class__, user=self) super().set_password(raw_password) # noqa + def set_ssh_key(self, name, public_key, private_key): + if self.can_update_ssh_key(): + from authentication.models import SSHKey + SSHKey.objects.create(name=name, public_key=public_key, private_key=private_key, user=self) + post_user_change_password.send(self.__class__, user=self) + + def can_create_ssh_key(self): + return self.ssh_keys.count() < settings.TERMINAL_SSH_KEY_LIMIT_COUNT + def can_update_password(self): return self.is_local diff --git a/apps/users/views/profile/__init__.py b/apps/users/views/profile/__init__.py index 35e2c7948..5abff8d9f 100644 --- a/apps/users/views/profile/__init__.py +++ b/apps/users/views/profile/__init__.py @@ -4,3 +4,4 @@ from .password import * from .mfa import * from .otp import * from .reset import * +from .pubkey import * diff --git a/apps/users/views/profile/pubkey.py b/apps/users/views/profile/pubkey.py new file mode 100644 index 000000000..495b1c530 --- /dev/null +++ b/apps/users/views/profile/pubkey.py @@ -0,0 +1,31 @@ +# ~*~ coding: utf-8 ~*~ + +from django.http import HttpResponse +from django.views import View + +from common.utils import get_logger, ssh_key_gen +from common.permissions import IsValidUser +from common.views.mixins import PermissionsMixin +from users.exceptions import CreateSSHKeyExceedLimit + +__all__ = ['UserPublicKeyGenerateView'] + +logger = get_logger(__name__) + + +class UserPublicKeyGenerateView(PermissionsMixin, View): + permission_classes = [IsValidUser] + + def get(self, request, *args, **kwargs): + username = request.user.username + key_name = request.GET.get('name', '') + if not request.user.can_create_ssh_key(): + return HttpResponse( + CreateSSHKeyExceedLimit().default_detail, status=400 + ) + private, public = ssh_key_gen(username=username, hostname='jumpserver') + request.user.set_ssh_key(key_name, public, private) + response = HttpResponse(private, content_type='text/plain') + filename = "{0}-jumpserver.pem".format(username) + response['Content-Disposition'] = 'attachment; filename={}'.format(filename) + return response