From d363118911ba297439809f9bb3bc4d1e05c9bc28 Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Tue, 26 Jan 2021 17:54:12 +0800 Subject: [PATCH] =?UTF-8?q?perf(settings):=20=E4=BC=98=E5=8C=96settings?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=20(#5515)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * stash * perf: 优化 动态seting * perf(settings): 优化settings配置 * perf: 完成终端和安全setting * perf: 修改翻译 * perf: 去掉其他位置的DYNAMIC * perf: 还原回来原来的一些代码 * perf: 优化ldap * perf: 移除dynmic config * perf: 去掉debug消息 * perf: 优化 refresh 命名 Co-authored-by: ibuler --- apps/common/local.py | 35 +-- apps/common/signals_handlers.py | 18 -- apps/common/utils/connection.py | 27 ++ apps/jumpserver/conf.py | 96 ------- apps/jumpserver/const.py | 3 +- apps/jumpserver/settings/auth.py | 33 ++- apps/jumpserver/settings/base.py | 20 +- apps/jumpserver/settings/custom.py | 75 +++--- apps/jumpserver/settings/libs.py | 5 + apps/locale/zh/LC_MESSAGES/django.mo | Bin 64146 -> 69926 bytes apps/locale/zh/LC_MESSAGES/django.po | 371 ++++++++++++++++++++++---- apps/settings/api/__init__.py | 2 + apps/settings/api/common.py | 189 +++++++++++++ apps/settings/{api.py => api/ldap.py} | 117 +------- apps/settings/models.py | 71 +++-- apps/settings/serializers/settings.py | 229 ++++++++++------ apps/settings/signals_handler.py | 54 +++- apps/users/models/user.py | 3 +- 18 files changed, 858 insertions(+), 490 deletions(-) create mode 100644 apps/common/utils/connection.py create mode 100644 apps/settings/api/__init__.py create mode 100644 apps/settings/api/common.py rename apps/settings/{api.py => api/ldap.py} (58%) diff --git a/apps/common/local.py b/apps/common/local.py index e075d29ff..d0299b43e 100644 --- a/apps/common/local.py +++ b/apps/common/local.py @@ -1,40 +1,7 @@ -# -*- coding: utf-8 -*- -# -from jumpserver.const import DYNAMIC -from werkzeug.local import Local, LocalProxy +from werkzeug.local import Local thread_local = Local() def _find(attr): return getattr(thread_local, attr, None) - - -class _Settings: - pass - - -def get_dynamic_cfg_from_thread_local(): - KEY = 'dynamic_config' - - try: - cfg = getattr(thread_local, KEY) - except AttributeError: - cfg = _Settings() - setattr(thread_local, KEY, cfg) - - return cfg - - -class DynamicDefaultLocalProxy(LocalProxy): - def __getattr__(self, item): - try: - value = super().__getattr__(item) - except AttributeError: - value = getattr(DYNAMIC, item)() - setattr(self, item, value) - - return value - - -LOCAL_DYNAMIC_SETTINGS = DynamicDefaultLocalProxy(get_dynamic_cfg_from_thread_local) diff --git a/apps/common/signals_handlers.py b/apps/common/signals_handlers.py index d1cf1c7fa..0cb3a04ad 100644 --- a/apps/common/signals_handlers.py +++ b/apps/common/signals_handlers.py @@ -5,16 +5,12 @@ import os import logging from collections import defaultdict from django.conf import settings -from django.dispatch import receiver from django.core.signals import request_finished from django.db import connection -from django.conf import LazySettings -from django.db.utils import ProgrammingError, OperationalError from jumpserver.utils import get_current_request from .local import thread_local -from .signals import django_ready pattern = re.compile(r'FROM `(\w+)`') logger = logging.getLogger("jumpserver.common") @@ -74,17 +70,3 @@ if settings.DEBUG and DEBUG_DB: request_finished.connect(on_request_finished_logging_db_query) else: request_finished.connect(on_request_finished_release_local) - - -@receiver(django_ready) -def monkey_patch_settings(sender, **kwargs): - def monkey_patch_getattr(self, name): - val = getattr(self._wrapped, name) - if callable(val): - val = val() - return val - - try: - LazySettings.__getattr__ = monkey_patch_getattr - except (ProgrammingError, OperationalError): - pass diff --git a/apps/common/utils/connection.py b/apps/common/utils/connection.py new file mode 100644 index 000000000..9bdf39628 --- /dev/null +++ b/apps/common/utils/connection.py @@ -0,0 +1,27 @@ +import redis +from django.conf import settings + + +def get_redis_client(db): + rc = redis.StrictRedis( + host=settings.REDIS_HOST, + port=settings.REDIS_PORT, + password=settings.REDIS_PASSWORD, + db=db + ) + return rc + + +class RedisPubSub: + def __init__(self, ch, db=10): + self.ch = ch + self.redis = get_redis_client(db) + + def subscribe(self): + ps = self.redis.pubsub() + ps.subscribe(self.ch) + return ps + + def publish(self, data): + self.redis.publish(self.ch, data) + return True diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 602916cb6..9989103f8 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -426,98 +426,6 @@ class Config(dict): return self.get(item) -class DynamicConfig: - def __init__(self, static_config): - self.static_config = static_config - self.db_setting = None - - def __getitem__(self, item): - return self.dynamic(item) - - def __getattr__(self, item): - return self.dynamic(item) - - def dynamic(self, item): - return lambda: self.get(item) - - def LOGIN_URL(self): - return self.get('LOGIN_URL') - - def AUTHENTICATION_BACKENDS(self): - backends = [ - 'authentication.backends.pubkey.PublicKeyAuthBackend', - 'django.contrib.auth.backends.ModelBackend', - ] - if self.get('AUTH_LDAP'): - backends.insert(0, 'authentication.backends.ldap.LDAPAuthorizationBackend') - if self.static_config.get('AUTH_CAS'): - backends.insert(0, 'authentication.backends.cas.CASBackend') - if self.static_config.get('AUTH_OPENID'): - backends.insert(0, 'jms_oidc_rp.backends.OIDCAuthPasswordBackend') - backends.insert(0, 'jms_oidc_rp.backends.OIDCAuthCodeBackend') - if self.static_config.get('AUTH_RADIUS'): - backends.insert(0, 'authentication.backends.radius.RadiusBackend') - if self.static_config.get('AUTH_SSO'): - backends.insert(0, 'authentication.backends.api.SSOAuthentication') - return backends - - def XPACK_LICENSE_IS_VALID(self): - if not HAS_XPACK: - return False - try: - from xpack.plugins.license.models import License - return License.has_valid_license() - except: - return False - - def XPACK_INTERFACE_LOGIN_TITLE(self): - default_title = _('Welcome to the JumpServer open source fortress') - if not HAS_XPACK: - return default_title - try: - from xpack.plugins.interface.models import Interface - return Interface.get_login_title() - except: - return default_title - - def LOGO_URLS(self): - logo_urls = {'logo_logout': static('img/logo.png'), - 'logo_index': static('img/logo_text.png'), - 'login_image': static('img/login_image.png'), - 'favicon': static('img/facio.ico')} - if not HAS_XPACK: - return logo_urls - try: - from xpack.plugins.interface.models import Interface - obj = Interface.interface() - if obj: - if obj.logo_logout: - logo_urls.update({'logo_logout': obj.logo_logout.url}) - if obj.logo_index: - logo_urls.update({'logo_index': obj.logo_index.url}) - if obj.login_image: - logo_urls.update({'login_image': obj.login_image.url}) - if obj.favicon: - logo_urls.update({'favicon': obj.favicon.url}) - except: - pass - return logo_urls - - def get_from_db(self, item): - if self.db_setting is not None: - value = self.db_setting.get(item) - if value is not None: - return value - return None - - def get(self, item): - # 先从数据库中获取 - value = self.get_from_db(item) - if value is not None: - return value - return self.static_config.get(item) - - class ConfigManager: config_class = Config @@ -694,7 +602,3 @@ class ConfigManager: # 对config进行兼容处理 config.compatible() return config - - @classmethod - def get_dynamic_config(cls, config): - return DynamicConfig(config) diff --git a/apps/jumpserver/const.py b/apps/jumpserver/const.py index c2889870e..c4607808f 100644 --- a/apps/jumpserver/const.py +++ b/apps/jumpserver/const.py @@ -4,12 +4,11 @@ import os from .conf import ConfigManager -__all__ = ['BASE_DIR', 'PROJECT_DIR', 'VERSION', 'CONFIG', 'DYNAMIC'] +__all__ = ['BASE_DIR', 'PROJECT_DIR', 'VERSION', 'CONFIG'] BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_DIR = os.path.dirname(BASE_DIR) VERSION = '2.0.0' CONFIG = ConfigManager.load_user_config() -DYNAMIC = ConfigManager.get_dynamic_config(CONFIG) diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index 4430aae2f..b0190e8a6 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -3,21 +3,21 @@ import os import ldap -from ..const import CONFIG, DYNAMIC, PROJECT_DIR +from ..const import CONFIG, PROJECT_DIR # OTP settings OTP_ISSUER_NAME = CONFIG.OTP_ISSUER_NAME OTP_VALID_WINDOW = CONFIG.OTP_VALID_WINDOW # Auth LDAP settings -AUTH_LDAP = DYNAMIC.AUTH_LDAP -AUTH_LDAP_SERVER_URI = DYNAMIC.AUTH_LDAP_SERVER_URI -AUTH_LDAP_BIND_DN = DYNAMIC.AUTH_LDAP_BIND_DN -AUTH_LDAP_BIND_PASSWORD = DYNAMIC.AUTH_LDAP_BIND_PASSWORD -AUTH_LDAP_SEARCH_OU = DYNAMIC.AUTH_LDAP_SEARCH_OU -AUTH_LDAP_SEARCH_FILTER = DYNAMIC.AUTH_LDAP_SEARCH_FILTER -AUTH_LDAP_START_TLS = DYNAMIC.AUTH_LDAP_START_TLS -AUTH_LDAP_USER_ATTR_MAP = DYNAMIC.AUTH_LDAP_USER_ATTR_MAP +AUTH_LDAP = CONFIG.AUTH_LDAP +AUTH_LDAP_SERVER_URI = CONFIG.AUTH_LDAP_SERVER_URI +AUTH_LDAP_BIND_DN = CONFIG.AUTH_LDAP_BIND_DN +AUTH_LDAP_BIND_PASSWORD = CONFIG.AUTH_LDAP_BIND_PASSWORD +AUTH_LDAP_SEARCH_OU = CONFIG.AUTH_LDAP_SEARCH_OU +AUTH_LDAP_SEARCH_FILTER = CONFIG.AUTH_LDAP_SEARCH_FILTER +AUTH_LDAP_START_TLS = CONFIG.AUTH_LDAP_START_TLS +AUTH_LDAP_USER_ATTR_MAP = CONFIG.AUTH_LDAP_USER_ATTR_MAP AUTH_LDAP_USER_QUERY_FIELD = 'username' AUTH_LDAP_GLOBAL_OPTIONS = { ldap.OPT_X_TLS_REQUIRE_CERT: ldap.OPT_X_TLS_NEVER, @@ -105,4 +105,17 @@ TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION LOGIN_CONFIRM_ENABLE = CONFIG.LOGIN_CONFIRM_ENABLE OTP_IN_RADIUS = CONFIG.OTP_IN_RADIUS -AUTHENTICATION_BACKENDS = DYNAMIC.AUTHENTICATION_BACKENDS +AUTHENTICATION_BACKENDS = [ + 'authentication.backends.pubkey.PublicKeyAuthBackend', + 'django.contrib.auth.backends.ModelBackend', +] + +if AUTH_CAS: + AUTHENTICATION_BACKENDS.insert(0, 'authentication.backends.cas.CASBackend') +if AUTH_OPENID: + AUTHENTICATION_BACKENDS.insert(0, 'jms_oidc_rp.backends.OIDCAuthPasswordBackend') + AUTHENTICATION_BACKENDS.insert(0, 'jms_oidc_rp.backends.OIDCAuthCodeBackend') +if AUTH_RADIUS: + AUTHENTICATION_BACKENDS.insert(0, 'authentication.backends.radius.RadiusBackend') +if AUTH_SSO: + AUTHENTICATION_BACKENDS.insert(0, 'authentication.backends.api.SSOAuthentication') diff --git a/apps/jumpserver/settings/base.py b/apps/jumpserver/settings/base.py index 2a6291751..bfb956bdc 100644 --- a/apps/jumpserver/settings/base.py +++ b/apps/jumpserver/settings/base.py @@ -3,7 +3,7 @@ import os from django.urls import reverse_lazy from .. import const -from ..const import CONFIG, DYNAMIC +from ..const import CONFIG # Build paths inside the project like this: os.path.join(BASE_DIR, ...) VERSION = const.VERSION @@ -23,7 +23,7 @@ BOOTSTRAP_TOKEN = CONFIG.BOOTSTRAP_TOKEN DEBUG = CONFIG.DEBUG # Absolute url for some case, for example email link -SITE_URL = DYNAMIC.SITE_URL +SITE_URL = CONFIG.SITE_URL # LOG LEVEL LOG_LEVEL = CONFIG.LOG_LEVEL @@ -216,14 +216,14 @@ MEDIA_ROOT = os.path.join(PROJECT_DIR, 'data', 'media').replace('\\', '/') + '/' FIXTURE_DIRS = [os.path.join(BASE_DIR, 'fixtures'), ] # Email config -EMAIL_HOST = DYNAMIC.EMAIL_HOST -EMAIL_PORT = DYNAMIC.EMAIL_PORT -EMAIL_HOST_USER = DYNAMIC.EMAIL_HOST_USER -EMAIL_HOST_PASSWORD = DYNAMIC.EMAIL_HOST_PASSWORD -EMAIL_FROM = DYNAMIC.EMAIL_FROM -EMAIL_RECIPIENT = DYNAMIC.EMAIL_RECIPIENT -EMAIL_USE_SSL = DYNAMIC.EMAIL_USE_SSL -EMAIL_USE_TLS = DYNAMIC.EMAIL_USE_TLS +EMAIL_HOST = CONFIG.EMAIL_HOST +EMAIL_PORT = CONFIG.EMAIL_PORT +EMAIL_HOST_USER = CONFIG.EMAIL_HOST_USER +EMAIL_HOST_PASSWORD = CONFIG.EMAIL_HOST_PASSWORD +EMAIL_FROM = CONFIG.EMAIL_FROM +EMAIL_RECIPIENT = CONFIG.EMAIL_RECIPIENT +EMAIL_USE_SSL = CONFIG.EMAIL_USE_SSL +EMAIL_USE_TLS = CONFIG.EMAIL_USE_TLS # Custom User Auth model diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index b1f2e1d41..95c1fce16 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -from ..const import CONFIG, DYNAMIC +from ..const import CONFIG # Storage settings COMMAND_STORAGE = { @@ -11,7 +11,7 @@ DEFAULT_TERMINAL_COMMAND_STORAGE = { "TYPE": "server", }, } -TERMINAL_COMMAND_STORAGE = DYNAMIC.TERMINAL_COMMAND_STORAGE or {} +TERMINAL_COMMAND_STORAGE = CONFIG.TERMINAL_COMMAND_STORAGE or {} # Server 类型的录像存储 SERVER_REPLAY_STORAGE = CONFIG.SERVER_REPLAY_STORAGE @@ -28,20 +28,20 @@ DEFAULT_TERMINAL_REPLAY_STORAGE = { "TYPE": "server", }, } -TERMINAL_REPLAY_STORAGE = DYNAMIC.TERMINAL_REPLAY_STORAGE +TERMINAL_REPLAY_STORAGE = CONFIG.TERMINAL_REPLAY_STORAGE # Security settings -SECURITY_MFA_AUTH = DYNAMIC.SECURITY_MFA_AUTH -SECURITY_COMMAND_EXECUTION = DYNAMIC.SECURITY_COMMAND_EXECUTION -SECURITY_LOGIN_LIMIT_COUNT = DYNAMIC.SECURITY_LOGIN_LIMIT_COUNT -SECURITY_LOGIN_LIMIT_TIME = DYNAMIC.SECURITY_LOGIN_LIMIT_TIME # Unit: minute -SECURITY_MAX_IDLE_TIME = DYNAMIC.SECURITY_MAX_IDLE_TIME # Unit: minute -SECURITY_PASSWORD_EXPIRATION_TIME = DYNAMIC.SECURITY_PASSWORD_EXPIRATION_TIME # Unit: day -SECURITY_PASSWORD_MIN_LENGTH = DYNAMIC.SECURITY_PASSWORD_MIN_LENGTH # Unit: bit -SECURITY_PASSWORD_UPPER_CASE = DYNAMIC.SECURITY_PASSWORD_UPPER_CASE -SECURITY_PASSWORD_LOWER_CASE = DYNAMIC.SECURITY_PASSWORD_LOWER_CASE -SECURITY_PASSWORD_NUMBER = DYNAMIC.SECURITY_PASSWORD_NUMBER -SECURITY_PASSWORD_SPECIAL_CHAR = DYNAMIC.SECURITY_PASSWORD_SPECIAL_CHAR +SECURITY_MFA_AUTH = CONFIG.SECURITY_MFA_AUTH +SECURITY_COMMAND_EXECUTION = CONFIG.SECURITY_COMMAND_EXECUTION +SECURITY_LOGIN_LIMIT_COUNT = CONFIG.SECURITY_LOGIN_LIMIT_COUNT +SECURITY_LOGIN_LIMIT_TIME = CONFIG.SECURITY_LOGIN_LIMIT_TIME # Unit: minute +SECURITY_MAX_IDLE_TIME = CONFIG.SECURITY_MAX_IDLE_TIME # Unit: minute +SECURITY_PASSWORD_EXPIRATION_TIME = CONFIG.SECURITY_PASSWORD_EXPIRATION_TIME # Unit: day +SECURITY_PASSWORD_MIN_LENGTH = CONFIG.SECURITY_PASSWORD_MIN_LENGTH # Unit: bit +SECURITY_PASSWORD_UPPER_CASE = CONFIG.SECURITY_PASSWORD_UPPER_CASE +SECURITY_PASSWORD_LOWER_CASE = CONFIG.SECURITY_PASSWORD_LOWER_CASE +SECURITY_PASSWORD_NUMBER = CONFIG.SECURITY_PASSWORD_NUMBER +SECURITY_PASSWORD_SPECIAL_CHAR = CONFIG.SECURITY_PASSWORD_SPECIAL_CHAR SECURITY_PASSWORD_RULES = [ 'SECURITY_PASSWORD_MIN_LENGTH', 'SECURITY_PASSWORD_UPPER_CASE', @@ -51,24 +51,24 @@ SECURITY_PASSWORD_RULES = [ ] SECURITY_MFA_VERIFY_TTL = CONFIG.SECURITY_MFA_VERIFY_TTL SECURITY_VIEW_AUTH_NEED_MFA = CONFIG.SECURITY_VIEW_AUTH_NEED_MFA -SECURITY_SERVICE_ACCOUNT_REGISTRATION = DYNAMIC.SECURITY_SERVICE_ACCOUNT_REGISTRATION +SECURITY_SERVICE_ACCOUNT_REGISTRATION = CONFIG.SECURITY_SERVICE_ACCOUNT_REGISTRATION SECURITY_LOGIN_CAPTCHA_ENABLED = CONFIG.SECURITY_LOGIN_CAPTCHA_ENABLED SECURITY_LOGIN_CHALLENGE_ENABLED = CONFIG.SECURITY_LOGIN_CHALLENGE_ENABLED SECURITY_DATA_CRYPTO_ALGO = CONFIG.SECURITY_DATA_CRYPTO_ALGO -SECURITY_INSECURE_COMMAND = DYNAMIC.SECURITY_INSECURE_COMMAND +SECURITY_INSECURE_COMMAND = CONFIG.SECURITY_INSECURE_COMMAND SECURITY_INSECURE_COMMAND_LEVEL = CONFIG.SECURITY_INSECURE_COMMAND_LEVEL -SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER = DYNAMIC.SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER +SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER = CONFIG.SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER # Terminal other setting -TERMINAL_PASSWORD_AUTH = DYNAMIC.TERMINAL_PASSWORD_AUTH -TERMINAL_PUBLIC_KEY_AUTH = DYNAMIC.TERMINAL_PUBLIC_KEY_AUTH -TERMINAL_HEARTBEAT_INTERVAL = DYNAMIC.TERMINAL_HEARTBEAT_INTERVAL -TERMINAL_ASSET_LIST_SORT_BY = DYNAMIC.TERMINAL_ASSET_LIST_SORT_BY -TERMINAL_ASSET_LIST_PAGE_SIZE = DYNAMIC.TERMINAL_ASSET_LIST_PAGE_SIZE -TERMINAL_SESSION_KEEP_DURATION = DYNAMIC.TERMINAL_SESSION_KEEP_DURATION -TERMINAL_HOST_KEY = DYNAMIC.TERMINAL_HOST_KEY -TERMINAL_HEADER_TITLE = DYNAMIC.TERMINAL_HEADER_TITLE -TERMINAL_TELNET_REGEX = DYNAMIC.TERMINAL_TELNET_REGEX +TERMINAL_PASSWORD_AUTH = CONFIG.TERMINAL_PASSWORD_AUTH +TERMINAL_PUBLIC_KEY_AUTH = CONFIG.TERMINAL_PUBLIC_KEY_AUTH +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 +TERMINAL_SESSION_KEEP_DURATION = CONFIG.TERMINAL_SESSION_KEEP_DURATION +TERMINAL_HOST_KEY = CONFIG.TERMINAL_HOST_KEY +TERMINAL_HEADER_TITLE = CONFIG.TERMINAL_HEADER_TITLE +TERMINAL_TELNET_REGEX = CONFIG.TERMINAL_TELNET_REGEX # User or user group permission cache time, default 3600 seconds ASSETS_PERM_CACHE_ENABLE = CONFIG.ASSETS_PERM_CACHE_ENABLE @@ -90,32 +90,25 @@ PERIOD_TASK_ENABLED = CONFIG.PERIOD_TASK_ENABLED USER_LOGIN_SINGLE_MACHINE_ENABLED = CONFIG.USER_LOGIN_SINGLE_MACHINE_ENABLED # Email custom content -EMAIL_SUBJECT_PREFIX = DYNAMIC.EMAIL_SUBJECT_PREFIX -EMAIL_SUFFIX = DYNAMIC.EMAIL_SUFFIX -EMAIL_CUSTOM_USER_CREATED_SUBJECT = DYNAMIC.EMAIL_CUSTOM_USER_CREATED_SUBJECT -EMAIL_CUSTOM_USER_CREATED_HONORIFIC = DYNAMIC.EMAIL_CUSTOM_USER_CREATED_HONORIFIC -EMAIL_CUSTOM_USER_CREATED_BODY = DYNAMIC.EMAIL_CUSTOM_USER_CREATED_BODY -EMAIL_CUSTOM_USER_CREATED_SIGNATURE = DYNAMIC.EMAIL_CUSTOM_USER_CREATED_SIGNATURE +EMAIL_SUBJECT_PREFIX = CONFIG.EMAIL_SUBJECT_PREFIX +EMAIL_SUFFIX = CONFIG.EMAIL_SUFFIX +EMAIL_CUSTOM_USER_CREATED_SUBJECT = CONFIG.EMAIL_CUSTOM_USER_CREATED_SUBJECT +EMAIL_CUSTOM_USER_CREATED_HONORIFIC = CONFIG.EMAIL_CUSTOM_USER_CREATED_HONORIFIC +EMAIL_CUSTOM_USER_CREATED_BODY = CONFIG.EMAIL_CUSTOM_USER_CREATED_BODY +EMAIL_CUSTOM_USER_CREATED_SIGNATURE = CONFIG.EMAIL_CUSTOM_USER_CREATED_SIGNATURE DISPLAY_PER_PAGE = CONFIG.DISPLAY_PER_PAGE DEFAULT_EXPIRED_YEARS = 70 -USER_GUIDE_URL = DYNAMIC.USER_GUIDE_URL +USER_GUIDE_URL = CONFIG.USER_GUIDE_URL HTTP_LISTEN_PORT = CONFIG.HTTP_LISTEN_PORT WS_LISTEN_PORT = CONFIG.WS_LISTEN_PORT -LOGIN_LOG_KEEP_DAYS = DYNAMIC.LOGIN_LOG_KEEP_DAYS +LOGIN_LOG_KEEP_DAYS = CONFIG.LOGIN_LOG_KEEP_DAYS TASK_LOG_KEEP_DAYS = CONFIG.TASK_LOG_KEEP_DAYS ORG_CHANGE_TO_URL = CONFIG.ORG_CHANGE_TO_URL WINDOWS_SKIP_ALL_MANUAL_PASSWORD = CONFIG.WINDOWS_SKIP_ALL_MANUAL_PASSWORD AUTH_EXPIRED_SECONDS = 60 * 5 -# XPACK -XPACK_LICENSE_IS_VALID = DYNAMIC.XPACK_LICENSE_IS_VALID - -XPACK_INTERFACE_LOGIN_TITLE = DYNAMIC.XPACK_INTERFACE_LOGIN_TITLE - -LOGO_URLS = DYNAMIC.LOGO_URLS - CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED = CONFIG.CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED DATETIME_DISPLAY_FORMAT = '%Y-%m-%d %H:%M:%S' diff --git a/apps/jumpserver/settings/libs.py b/apps/jumpserver/settings/libs.py index aac0c4d50..49b3d3b2d 100644 --- a/apps/jumpserver/settings/libs.py +++ b/apps/jumpserver/settings/libs.py @@ -127,3 +127,8 @@ CELERY_WORKER_REDIRECT_STDOUTS_LEVEL = "INFO" CELERY_TASK_SOFT_TIME_LIMIT = 3600 ANSIBLE_LOG_DIR = os.path.join(PROJECT_DIR, 'data', 'ansible') + +# +REDIS_HOST = CONFIG.REDIS_HOST +REDIS_PORT = CONFIG.REDIS_PORT +REDIS_PASSWORD = CONFIG.REDIS_PASSWORD diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index e00cb3d92aa8ae1771b51e17d0a4c3a74d6838e3..ea2503cb993c1688fe3ba25e7386495b773ed7b6 100644 GIT binary patch delta 25617 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\n" "Language-Team: JumpServer team\n" @@ -36,10 +36,10 @@ msgstr "自定义" #: assets/models/base.py:234 assets/models/cluster.py:18 #: assets/models/cmd_filter.py:21 assets/models/domain.py:21 #: assets/models/group.py:20 assets/models/label.py:18 ops/mixin.py:24 -#: orgs/models.py:23 perms/models/base.py:48 settings/models.py:27 +#: orgs/models.py:23 perms/models/base.py:48 settings/models.py:29 #: terminal/models/storage.py:15 terminal/models/storage.py:55 #: terminal/models/task.py:16 terminal/models/terminal.py:131 -#: users/forms/profile.py:20 users/models/group.py:15 users/models/user.py:518 +#: users/forms/profile.py:20 users/models/group.py:15 users/models/user.py:517 #: users/templates/users/_select_user_modal.html:13 #: users/templates/users/user_asset_permission.html:37 #: users/templates/users/user_asset_permission.html:154 @@ -95,10 +95,10 @@ msgstr "" #: assets/models/cmd_filter.py:57 assets/models/domain.py:22 #: assets/models/domain.py:56 assets/models/group.py:23 #: assets/models/label.py:23 ops/models/adhoc.py:37 orgs/models.py:26 -#: perms/models/base.py:56 settings/models.py:32 terminal/models/storage.py:21 +#: perms/models/base.py:56 settings/models.py:34 terminal/models/storage.py:21 #: terminal/models/storage.py:61 terminal/models/terminal.py:145 #: tickets/models/ticket.py:73 users/models/group.py:16 -#: users/models/user.py:551 users/templates/users/user_detail.html:115 +#: users/models/user.py:550 users/templates/users/user_detail.html:115 #: users/templates/users/user_granted_database_app.html:38 #: users/templates/users/user_granted_remote_app.html:37 #: users/templates/users/user_group_detail.html:62 @@ -164,7 +164,7 @@ msgstr "目标URL" #: assets/models/base.py:235 assets/models/gathered_user.py:15 #: audits/models.py:99 authentication/forms.py:11 #: authentication/templates/authentication/login.html:101 -#: ops/models/adhoc.py:148 users/forms/profile.py:19 users/models/user.py:516 +#: ops/models/adhoc.py:148 users/forms/profile.py:19 users/models/user.py:515 #: users/templates/users/_select_user_modal.html:14 #: users/templates/users/user_detail.html:53 #: users/templates/users/user_list.html:15 @@ -181,7 +181,8 @@ msgstr "用户名" #: assets/models/base.py:236 assets/serializers/asset_user.py:71 #: audits/signals_handler.py:42 authentication/forms.py:13 #: authentication/templates/authentication/login.html:109 -#: users/forms/user.py:22 users/forms/user.py:193 +#: settings/serializers/settings.py:84 users/forms/user.py:22 +#: users/forms/user.py:193 #: users/templates/users/user_otp_check_password.html:13 #: users/templates/users/user_password_update.html:43 #: users/templates/users/user_password_verify.html:18 @@ -204,7 +205,7 @@ msgstr "目标URL" #: applications/serializers/attrs/application_type/mysql_workbench.py:18 #: assets/models/asset.py:190 assets/models/domain.py:52 -#: assets/serializers/asset_user.py:46 settings/serializers/settings.py:52 +#: assets/serializers/asset_user.py:46 settings/serializers/settings.py:103 #: users/templates/users/_granted_assets.html:26 #: users/templates/users/user_asset_permission.html:156 msgid "IP" @@ -260,7 +261,7 @@ msgid "Platform" msgstr "系统平台" #: assets/models/asset.py:191 assets/serializers/asset_user.py:45 -#: assets/serializers/gathered_user.py:20 settings/serializers/settings.py:51 +#: assets/serializers/gathered_user.py:20 settings/serializers/settings.py:102 #: users/templates/users/_granted_assets.html:25 #: users/templates/users/user_asset_permission.html:157 msgid "Hostname" @@ -368,7 +369,7 @@ msgstr "标签管理" #: assets/models/cluster.py:28 assets/models/cmd_filter.py:26 #: assets/models/cmd_filter.py:60 assets/models/group.py:21 #: common/db/models.py:67 common/mixins/models.py:49 orgs/models.py:24 -#: orgs/models.py:427 perms/models/base.py:54 users/models/user.py:559 +#: orgs/models.py:427 perms/models/base.py:54 users/models/user.py:558 #: users/serializers/group.py:35 users/templates/users/user_detail.html:97 #: xpack/plugins/change_auth_plan/models.py:81 xpack/plugins/cloud/models.py:58 #: xpack/plugins/cloud/models.py:156 xpack/plugins/gathered_user/models.py:30 @@ -430,7 +431,7 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:537 +#: assets/models/cluster.py:22 users/models/user.py:536 #: users/templates/users/user_detail.html:62 msgid "Phone" msgstr "手机" @@ -456,7 +457,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:678 +#: users/models/user.py:677 msgid "System" msgstr "系统" @@ -560,7 +561,7 @@ msgstr "默认资产组" #: templates/index.html:78 terminal/backends/command/models.py:18 #: terminal/backends/command/serializers.py:12 terminal/models/session.py:37 #: tickets/models/comment.py:17 users/forms/group.py:15 -#: users/models/user.py:159 users/models/user.py:666 +#: users/models/user.py:158 users/models/user.py:665 #: users/serializers/group.py:20 #: users/templates/users/user_asset_permission.html:38 #: users/templates/users/user_asset_permission.html:64 @@ -574,7 +575,7 @@ msgstr "默认资产组" msgid "User" msgstr "用户" -#: assets/models/label.py:19 assets/models/node.py:413 settings/models.py:28 +#: assets/models/label.py:19 assets/models/node.py:413 settings/models.py:30 msgid "Value" msgstr "值" @@ -741,14 +742,14 @@ msgid "Backend" msgstr "后端" #: assets/serializers/asset_user.py:75 users/forms/profile.py:148 -#: users/models/user.py:548 users/templates/users/user_password_update.html:48 +#: users/models/user.py:547 users/templates/users/user_password_update.html:48 #: users/templates/users/user_profile.html:69 #: users/templates/users/user_profile_update.html:46 #: users/templates/users/user_pubkey_update.html:46 msgid "Public key" msgstr "SSH公钥" -#: assets/serializers/asset_user.py:79 users/models/user.py:545 +#: assets/serializers/asset_user.py:79 users/models/user.py:544 msgid "Private key" msgstr "ssh私钥" @@ -1045,7 +1046,7 @@ msgstr "修改者" msgid "Disabled" msgstr "禁用" -#: audits/models.py:90 settings/models.py:31 +#: audits/models.py:90 settings/models.py:33 #: users/templates/users/user_detail.html:82 msgid "Enabled" msgstr "启用" @@ -1079,7 +1080,7 @@ msgstr "用户代理" #: audits/models.py:104 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 #: authentication/templates/authentication/login_otp.html:6 -#: users/forms/profile.py:52 users/models/user.py:540 +#: users/forms/profile.py:52 users/models/user.py:539 #: users/serializers/user.py:232 users/templates/users/user_detail.html:77 #: users/templates/users/user_profile.html:87 msgid "MFA" @@ -1354,7 +1355,7 @@ msgid "Show" msgstr "显示" #: authentication/templates/authentication/_access_key_modal.html:66 -#: users/models/user.py:444 users/serializers/user.py:229 +#: users/models/user.py:443 users/serializers/user.py:229 #: users/templates/users/user_profile.html:94 #: users/templates/users/user_profile.html:163 #: users/templates/users/user_profile.html:166 @@ -1363,7 +1364,7 @@ msgid "Disable" msgstr "禁用" #: authentication/templates/authentication/_access_key_modal.html:67 -#: users/models/user.py:445 users/serializers/user.py:230 +#: users/models/user.py:444 users/serializers/user.py:230 #: users/templates/users/user_profile.html:92 #: users/templates/users/user_profile.html:170 msgid "Enable" @@ -1600,11 +1601,6 @@ msgstr "字段必须唯一" msgid "Should not contains special characters" msgstr "不能包含特殊字符" -#: jumpserver/conf.py:474 xpack/plugins/interface/api.py:18 -#: xpack/plugins/interface/models.py:36 -msgid "Welcome to the JumpServer open source fortress" -msgstr "欢迎使用JumpServer开源堡垒机" - #: jumpserver/views/celery_flower.py:23 msgid "

Flow service unavailable, check it

" msgstr "" @@ -1813,7 +1809,7 @@ msgstr "组织管理员" msgid "Organization auditor" msgstr "组织审计员" -#: orgs/models.py:424 users/forms/user.py:27 users/models/user.py:528 +#: orgs/models.py:424 users/forms/user.py:27 users/models/user.py:527 #: users/templates/users/_select_user_modal.html:15 #: users/templates/users/user_detail.html:73 #: users/templates/users/user_list.html:16 @@ -1837,7 +1833,7 @@ msgstr "管理员正在修改授权,请稍等" msgid "The authorization cannot be revoked for the time being" msgstr "该授权暂时不能撤销" -#: perms/models/application_permission.py:27 users/models/user.py:160 +#: perms/models/application_permission.py:27 users/models/user.py:159 msgid "Application" msgstr "应用程序" @@ -1845,7 +1841,7 @@ msgstr "应用程序" msgid "Application permission" msgstr "应用管理" -#: perms/models/asset_permission.py:34 settings/serializers/settings.py:56 +#: perms/models/asset_permission.py:34 settings/serializers/settings.py:107 msgid "All" msgstr "全部" @@ -1887,7 +1883,7 @@ msgid "Asset permission" msgstr "资产授权" #: perms/models/base.py:50 templates/_nav.html:21 users/forms/user.py:168 -#: users/models/group.py:31 users/models/user.py:524 +#: users/models/group.py:31 users/models/user.py:523 #: users/templates/users/_select_user_modal.html:16 #: users/templates/users/user_asset_permission.html:39 #: users/templates/users/user_asset_permission.html:67 @@ -1905,7 +1901,7 @@ msgstr "用户组" #: tickets/serializers/ticket/meta/ticket_type/apply_application.py:77 #: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:43 #: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:81 -#: users/models/user.py:556 users/templates/users/user_detail.html:93 +#: users/models/user.py:555 users/templates/users/user_detail.html:93 #: users/templates/users/user_profile.html:120 msgid "Date expired" msgstr "失效日期" @@ -1944,27 +1940,310 @@ msgstr "收藏夹" msgid "Please wait while your data is being initialized" msgstr "数据正在初始化,请稍等" -#: settings/api.py:34 +#: settings/api/common.py:24 msgid "Test mail sent to {}, please check" msgstr "邮件已经发送{}, 请检查" -#: settings/api.py:244 +#: settings/api/common.py:110 xpack/plugins/interface/api.py:18 +#: xpack/plugins/interface/models.py:36 +msgid "Welcome to the JumpServer open source fortress" +msgstr "欢迎使用JumpServer开源堡垒机" + +#: settings/api/ldap.py:189 msgid "Get ldap users is None" msgstr "获取 LDAP 用户为 None" -#: settings/api.py:251 +#: settings/api/ldap.py:196 msgid "Imported {} users successfully" msgstr "导入 {} 个用户成功" -#: settings/models.py:96 users/templates/users/reset_password.html:29 +#: settings/models.py:123 users/templates/users/reset_password.html:29 #: users/templates/users/user_profile.html:20 msgid "Setting" msgstr "设置" -#: settings/serializers/settings.py:57 +#: settings/serializers/settings.py:15 +msgid "Site url" +msgstr "当前站点URL" + +#: settings/serializers/settings.py:16 +msgid "eg: http://demo.jumpserver.org:8080" +msgstr "如: http://demo.jumpserver.org:8080" + +#: settings/serializers/settings.py:19 +msgid "User guide url" +msgstr "用户向导URL" + +#: settings/serializers/settings.py:20 +msgid "User first login update profile done redirect to it" +msgstr "用户第一次登录,修改profile后重定向到地址, 可以是 wiki 或 其他说明文档" + +#: settings/serializers/settings.py:27 +msgid "SMTP host" +msgstr "SMTP 主机" + +#: settings/serializers/settings.py:28 +msgid "SMTP port" +msgstr "SMTP 端口" + +#: settings/serializers/settings.py:29 +msgid "SMTP account" +msgstr "SMTP 账号" + +#: settings/serializers/settings.py:31 +msgid "SMTP password" +msgstr "SMTP 密码" + +#: settings/serializers/settings.py:32 +msgid "Tips: Some provider use token except password" +msgstr "提示:一些邮件提供商需要输入的是授权码" + +#: settings/serializers/settings.py:35 +msgid "Send user" +msgstr "发件人" + +#: settings/serializers/settings.py:36 +msgid "Tips: Send mail account, default SMTP account as the send account" +msgstr "提示:发送邮件账号,默认使用 SMTP 账号作为发送账号" + +#: settings/serializers/settings.py:39 +msgid "Test recipient" +msgstr "测试收件人" + +#: settings/serializers/settings.py:40 +msgid "Tips: Used only as a test mail recipient" +msgstr "提示:仅用来作为测试邮件收件人" + +#: settings/serializers/settings.py:43 +msgid "Use SSL" +msgstr "使用 SSL" + +#: settings/serializers/settings.py:44 +msgid "If SMTP port is 465, may be select" +msgstr "如果SMTP端口是465,通常需要启用 SSL" + +#: settings/serializers/settings.py:47 +msgid "Use TLS" +msgstr "使用 TLS" + +#: settings/serializers/settings.py:48 +msgid "If SMTP port is 587, may be select" +msgstr "如果SMTP端口是587,通常需要启用 TLS" + +#: settings/serializers/settings.py:51 +msgid "Subject prefix" +msgstr "主题前缀" + +#: settings/serializers/settings.py:58 +msgid "Create user email subject" +msgstr "邮件主题" + +#: settings/serializers/settings.py:59 +msgid "" +"Tips: When creating a user, send the subject of the email (eg:Create account " +"successfully)" +msgstr "提示: 创建用户时,发送设置密码邮件的主题 (例如: 创建用户成功)" + +#: settings/serializers/settings.py:63 +msgid "Create user honorific" +msgstr "邮件的敬语" + +#: settings/serializers/settings.py:64 +msgid "Tips: When creating a user, send the honorific of the email (eg:Hello)" +msgstr "提示: 创建用户时,发送设置密码邮件的敬语 (例如: 您好)" + +#: settings/serializers/settings.py:68 +msgid "Create user email content" +msgstr "邮件的内容" + +#: settings/serializers/settings.py:69 +msgid "Tips:When creating a user, send the content of the email" +msgstr "提示: 创建用户时,发送设置密码邮件的内容" + +#: settings/serializers/settings.py:72 +msgid "Signature" +msgstr "署名" + +#: settings/serializers/settings.py:73 +msgid "Tips: Email signature (eg:jumpserver)" +msgstr "邮件署名 (如:jumpserver)" + +#: settings/serializers/settings.py:81 +msgid "LDAP server" +msgstr "LDAP 地址" + +#: settings/serializers/settings.py:81 +msgid "eg: ldap://localhost:389" +msgstr "" + +#: settings/serializers/settings.py:83 +msgid "Bind DN" +msgstr "绑定 DN" + +#: settings/serializers/settings.py:86 +msgid "User OU" +msgstr "用户 OU" + +#: settings/serializers/settings.py:87 +msgid "Use | split multi OUs" +msgstr "多个 OU 使用 | 分割" + +#: settings/serializers/settings.py:90 +msgid "User search filter" +msgstr "用户过滤器" + +#: settings/serializers/settings.py:91 +#, python-format +msgid "Choice may be (cn|uid|sAMAccountName)=%(user)s)" +msgstr "可能的选项是(cn或uid或sAMAccountName=%(user)s)" + +#: settings/serializers/settings.py:94 +msgid "User attr map" +msgstr "用户属性映射" + +#: settings/serializers/settings.py:95 +msgid "" +"User attr map present how to map LDAP user attr to jumpserver, username,name," +"email is jumpserver attr" +msgstr "" +"用户属性映射代表怎样将LDAP中用户属性映射到jumpserver用户上,username, name," +"email 是jumpserver的用户需要属性" + +#: settings/serializers/settings.py:97 +msgid "Enable LDAP auth" +msgstr "启用 LDAP 认证" + +#: settings/serializers/settings.py:108 msgid "Auto" msgstr "自动" +#: settings/serializers/settings.py:114 +msgid "Password auth" +msgstr "密码认证" + +#: settings/serializers/settings.py:115 +msgid "Public key auth" +msgstr "密钥认证" + +#: settings/serializers/settings.py:116 +msgid "List sort by" +msgstr "资产列表排序" + +#: settings/serializers/settings.py:117 +msgid "List page size" +msgstr "资产列表每页数量" + +#: settings/serializers/settings.py:119 +msgid "Session keep duration" +msgstr "会话日志保存时间" + +#: settings/serializers/settings.py:120 +msgid "" +"Units: days, Session, record, command will be delete if more than duration, " +"only in database" +msgstr "" +"单位:天。 会话、录像、命令记录超过该时长将会被删除(仅影响数据库存储, oss等不" +"受影响)" + +#: settings/serializers/settings.py:122 +msgid "Telnet login regex" +msgstr "Telnet 成功正则表达式" + +#: settings/serializers/settings.py:127 +msgid "Global MFA auth" +msgstr "全局启用 MFA 认证" + +#: settings/serializers/settings.py:128 +msgid "All user enable MFA" +msgstr "强制每个启用多因子认证" + +#: settings/serializers/settings.py:131 +msgid "Batch command execution" +msgstr "批量命令执行" + +#: settings/serializers/settings.py:132 +msgid "Allow user run batch command or not using ansible" +msgstr "是否允许用户使用 ansible 执行批量命令" + +#: settings/serializers/settings.py:135 +msgid "Enable terminal register" +msgstr "终端注册" + +#: settings/serializers/settings.py:136 +msgid "" +"Allow terminal register, after all terminal setup, you should disable this " +"for security" +msgstr "是否允许终端注册,当所有终端启动后,为了安全应该关闭" + +#: settings/serializers/settings.py:140 +msgid "Limit the number of login failures" +msgstr "限制登录失败次数" + +#: settings/serializers/settings.py:144 +msgid "Block logon interval" +msgstr "禁止登录时间间隔" + +#: settings/serializers/settings.py:145 +msgid "" +"Tip: (unit/minute) if the user has failed to log in for a limited number of " +"times, no login is allowed during this time interval." +msgstr "" +"提示:(单位:分)当用户登录失败次数达到限制后,那么在此时间间隔内禁止登录" + +#: settings/serializers/settings.py:149 +msgid "Connection max idle time" +msgstr "连接最大空闲时间" + +#: settings/serializers/settings.py:150 +msgid "If idle time more than it, disconnect connection Unit: minute" +msgstr "提示:如果超过该配置没有操作,连接会被断开 (单位:分)" + +#: settings/serializers/settings.py:154 +msgid "User password expiration" +msgstr "用户密码过期时间" + +#: settings/serializers/settings.py:155 +msgid "" +"Tip: (unit: day) If the user does not update the password during the time, " +"the user password will expire failure;The password expiration reminder mail " +"will be automatic sent to the user by system within 5 days (daily) before " +"the password expires" +msgstr "" +"提示:(单位:天)如果用户在此期间没有更新密码,用户密码将过期失效; 密码过期" +"提醒邮件将在密码过期前5天内由系统(每天)自动发送给用户" + +#: settings/serializers/settings.py:159 +msgid "Password minimum length" +msgstr "密码最小长度" + +#: settings/serializers/settings.py:162 +msgid "Must contain capital" +msgstr "必须包含大写字符" + +#: settings/serializers/settings.py:164 +msgid "Must contain lowercase" +msgstr "必须包含小写字符" + +#: settings/serializers/settings.py:165 +msgid "Must contain numeric" +msgstr "必须包含数字" + +#: settings/serializers/settings.py:166 +msgid "Must contain special" +msgstr "必须包含特殊字符" + +#: settings/serializers/settings.py:167 +msgid "Insecure command alert" +msgstr "危险命令告警" + +#: settings/serializers/settings.py:169 +msgid "Email recipient" +msgstr "邮件收件人" + +#: settings/serializers/settings.py:170 +msgid "Multiple user using , split" +msgstr "多个用户,使用 , 分割" + #: settings/utils/ldap.py:411 msgid "Host or port is disconnected: {}" msgstr "主机或端口不可连接: {}" @@ -3215,7 +3494,7 @@ msgstr "确认密码" msgid "Password does not match" msgstr "密码不一致" -#: users/forms/profile.py:89 users/models/user.py:520 +#: users/forms/profile.py:89 users/models/user.py:519 #: users/templates/users/user_detail.html:57 #: users/templates/users/user_profile.html:59 msgid "Email" @@ -3256,7 +3535,7 @@ msgstr "不能和原来的密钥相同" msgid "Not a valid ssh public key" msgstr "SSH密钥不合法" -#: users/forms/user.py:31 users/models/user.py:563 +#: users/forms/user.py:31 users/models/user.py:562 #: users/templates/users/user_detail.html:89 #: users/templates/users/user_list.html:18 #: users/templates/users/user_profile.html:102 @@ -3290,39 +3569,39 @@ msgstr "设置密码" msgid "Password strategy" msgstr "密码策略" -#: users/models/user.py:157 +#: users/models/user.py:156 msgid "System administrator" msgstr "系统管理员" -#: users/models/user.py:158 +#: users/models/user.py:157 msgid "System auditor" msgstr "系统审计员" -#: users/models/user.py:446 users/templates/users/user_profile.html:90 +#: users/models/user.py:445 users/templates/users/user_profile.html:90 msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:508 +#: users/models/user.py:507 msgid "Local" msgstr "数据库" -#: users/models/user.py:531 +#: users/models/user.py:530 msgid "Avatar" msgstr "头像" -#: users/models/user.py:534 users/templates/users/user_detail.html:68 +#: users/models/user.py:533 users/templates/users/user_detail.html:68 msgid "Wechat" msgstr "微信" -#: users/models/user.py:567 +#: users/models/user.py:566 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:674 +#: users/models/user.py:673 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:677 +#: users/models/user.py:676 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" diff --git a/apps/settings/api/__init__.py b/apps/settings/api/__init__.py new file mode 100644 index 000000000..151617be5 --- /dev/null +++ b/apps/settings/api/__init__.py @@ -0,0 +1,2 @@ +from .common import * +from .ldap import * diff --git a/apps/settings/api/common.py b/apps/settings/api/common.py new file mode 100644 index 000000000..ba7945ec7 --- /dev/null +++ b/apps/settings/api/common.py @@ -0,0 +1,189 @@ +# -*- coding: utf-8 -*- +# + +from smtplib import SMTPSenderRefused +from rest_framework import generics +from rest_framework.views import Response, APIView +from rest_framework.permissions import AllowAny +from django.conf import settings +from django.core.mail import send_mail, get_connection +from django.utils.translation import ugettext_lazy as _ +from django.templatetags.static import static + +from common.permissions import IsSuperUser +from common.utils import get_logger +from .. import serializers +from ..models import Setting + +logger = get_logger(__file__) + + +class MailTestingAPI(APIView): + permission_classes = (IsSuperUser,) + serializer_class = serializers.MailTestSerializer + success_message = _("Test mail sent to {}, please check") + + def post(self, request): + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + + email_host = serializer.validated_data['EMAIL_HOST'] + email_port = serializer.validated_data['EMAIL_PORT'] + email_host_user = serializer.validated_data["EMAIL_HOST_USER"] + email_host_password = serializer.validated_data['EMAIL_HOST_PASSWORD'] + email_from = serializer.validated_data["EMAIL_FROM"] + email_recipient = serializer.validated_data["EMAIL_RECIPIENT"] + email_use_ssl = serializer.validated_data['EMAIL_USE_SSL'] + email_use_tls = serializer.validated_data['EMAIL_USE_TLS'] + + # 设置 settings 的值,会导致动态配置在当前进程失效 + # for k, v in serializer.validated_data.items(): + # if k.startswith('EMAIL'): + # setattr(settings, k, v) + try: + subject = "Test" + message = "Test smtp setting" + email_from = email_from or email_host_user + email_recipient = email_recipient or email_from + connection = get_connection( + host=email_host, port=email_port, + username=email_host_user, password=email_host_password, + use_tls=email_use_tls, use_ssl=email_use_ssl, + ) + send_mail( + subject, message, email_from, [email_recipient], + connection=connection + ) + except SMTPSenderRefused as e: + error = e.smtp_error + if isinstance(error, bytes): + for coding in ('gbk', 'utf8'): + try: + error = error.decode(coding) + except UnicodeDecodeError: + continue + else: + break + return Response({"error": str(error)}, status=400) + except Exception as e: + logger.error(e) + return Response({"error": str(e)}, status=400) + return Response({"msg": self.success_message.format(email_recipient)}) + + +class PublicSettingApi(generics.RetrieveAPIView): + permission_classes = (AllowAny,) + serializer_class = serializers.PublicSettingSerializer + + @staticmethod + def get_logo_urls(): + logo_urls = { + 'logo_logout': static('img/logo.png'), + 'logo_index': static('img/logo_text.png'), + 'login_image': static('img/login_image.png'), + 'favicon': static('img/facio.ico') + } + if not settings.XPACK_ENABLED: + return logo_urls + from xpack.plugins.interface.models import Interface + obj = Interface.interface() + if not obj: + return logo_urls + for attr in ['logo_logout', 'logo_index', 'login_image', 'favicon']: + if getattr(obj, attr, '') and getattr(obj, attr).url: + logo_urls.update({attr: getattr(obj, attr).url}) + return logo_urls + + @staticmethod + def get_xpack_license_is_valid(): + if not settings.XPACK_ENABLED: + return False + try: + from xpack.plugins.license.models import License + return License.has_valid_license() + except Exception as e: + logger.error(e) + return False + + @staticmethod + def get_login_title(): + default_title = _('Welcome to the JumpServer open source fortress') + if not settings.XPACK_ENABLED: + return default_title + from xpack.plugins.interface.models import Interface + return Interface.get_login_title() + + def get_object(self): + instance = { + "data": { + "WINDOWS_SKIP_ALL_MANUAL_PASSWORD": settings.WINDOWS_SKIP_ALL_MANUAL_PASSWORD, + "SECURITY_MAX_IDLE_TIME": settings.SECURITY_MAX_IDLE_TIME, + "XPACK_ENABLED": settings.XPACK_ENABLED, + "LOGIN_CONFIRM_ENABLE": settings.LOGIN_CONFIRM_ENABLE, + "SECURITY_VIEW_AUTH_NEED_MFA": settings.SECURITY_VIEW_AUTH_NEED_MFA, + "SECURITY_MFA_VERIFY_TTL": settings.SECURITY_MFA_VERIFY_TTL, + "SECURITY_COMMAND_EXECUTION": settings.SECURITY_COMMAND_EXECUTION, + "SECURITY_PASSWORD_EXPIRATION_TIME": settings.SECURITY_PASSWORD_EXPIRATION_TIME, + "XPACK_LICENSE_IS_VALID": self.get_xpack_license_is_valid(), + "LOGIN_TITLE": self.get_login_title(), + "LOGO_URLS": self.get_logo_urls(), + "TICKETS_ENABLED": settings.TICKETS_ENABLED, + "PASSWORD_RULE": { + 'SECURITY_PASSWORD_MIN_LENGTH': settings.SECURITY_PASSWORD_MIN_LENGTH, + 'SECURITY_PASSWORD_UPPER_CASE': settings.SECURITY_PASSWORD_UPPER_CASE, + 'SECURITY_PASSWORD_LOWER_CASE': settings.SECURITY_PASSWORD_LOWER_CASE, + 'SECURITY_PASSWORD_NUMBER': settings.SECURITY_PASSWORD_NUMBER, + 'SECURITY_PASSWORD_SPECIAL_CHAR': settings.SECURITY_PASSWORD_SPECIAL_CHAR, + } + } + } + return instance + + +class SettingsApi(generics.RetrieveUpdateAPIView): + permission_classes = (IsSuperUser,) + serializer_class_mapper = { + 'all': serializers.SettingsSerializer, + 'basic': serializers.BasicSettingSerializer, + 'terminal': serializers.TerminalSettingSerializer, + 'security': serializers.SecuritySettingSerializer, + 'ldap': serializers.LDAPSettingSerializer, + 'email': serializers.EmailSettingSerializer, + 'email_content': serializers.EmailContentSettingSerializer, + } + + def get_serializer_class(self): + category = self.request.query_params.get('category', serializers.BasicSettingSerializer) + return self.serializer_class_mapper.get(category, serializers.BasicSettingSerializer) + + def get_fields(self): + serializer = self.get_serializer_class()() + fields = serializer.get_fields() + return fields + + def get_object(self): + items = self.get_fields().keys() + return {item: getattr(settings, item) for item in items} + + def parse_serializer_data(self, serializer): + data = [] + fields = self.get_fields() + encrypted_items = [name for name, field in fields.items() if field.write_only] + category = self.request.query_params.get('category', '') + for name, value in serializer.validated_data.items(): + encrypted = name in encrypted_items + data.append({ + 'name': name, 'value': value, + 'encrypted': encrypted, 'category': category + }) + return data + + def perform_update(self, serializer): + settings_items = self.parse_serializer_data(serializer) + serializer_data = getattr(serializer, 'data', {}) + for item in settings_items: + changed, setting = Setting.update_or_create(**item) + if not changed: + continue + serializer_data[setting.name] = setting.cleaned_value + setattr(serializer, '_data', serializer_data) diff --git a/apps/settings/api.py b/apps/settings/api/ldap.py similarity index 58% rename from apps/settings/api.py rename to apps/settings/api/ldap.py index 1fba03e6a..0c982d1b1 100644 --- a/apps/settings/api.py +++ b/apps/settings/api/ldap.py @@ -10,16 +10,15 @@ from rest_framework.views import Response, APIView from django.conf import settings from django.core.mail import send_mail, get_connection from django.utils.translation import ugettext_lazy as _ -from rest_framework import serializers -from .utils import ( +from ..utils import ( LDAPServerUtil, LDAPCacheUtil, LDAPImportUtil, LDAPSyncUtil, LDAP_USE_CACHE_FLAGS, LDAPTestUtil, ObjectDict ) -from .tasks import sync_ldap_user +from ..tasks import sync_ldap_user from common.permissions import IsOrgAdmin, IsSuperUser from common.utils import get_logger -from .serializers import ( +from ..serializers import ( MailTestSerializer, LDAPTestConfigSerializer, LDAPUserSerializer, PublicSettingSerializer, LDAPTestLoginSerializer, SettingsSerializer ) @@ -28,60 +27,6 @@ from users.models import User logger = get_logger(__file__) -class MailTestingAPI(APIView): - permission_classes = (IsSuperUser,) - serializer_class = MailTestSerializer - success_message = _("Test mail sent to {}, please check") - - def post(self, request): - serializer = self.serializer_class(data=request.data) - if serializer.is_valid(): - email_host = serializer.validated_data['EMAIL_HOST'] - email_port = serializer.validated_data['EMAIL_PORT'] - email_host_user = serializer.validated_data["EMAIL_HOST_USER"] - email_host_password = serializer.validated_data['EMAIL_HOST_PASSWORD'] - email_from = serializer.validated_data["EMAIL_FROM"] - email_recipient = serializer.validated_data["EMAIL_RECIPIENT"] - email_use_ssl = serializer.validated_data['EMAIL_USE_SSL'] - email_use_tls = serializer.validated_data['EMAIL_USE_TLS'] - - # 设置 settings 的值,会导致动态配置在当前进程失效 - # for k, v in serializer.validated_data.items(): - # if k.startswith('EMAIL'): - # setattr(settings, k, v) - try: - subject = "Test" - message = "Test smtp setting" - email_from = email_from or email_host_user - email_recipient = email_recipient or email_from - connection = get_connection( - host=email_host, port=email_port, - username=email_host_user, password=email_host_password, - use_tls=email_use_tls, use_ssl=email_use_ssl, - ) - send_mail( - subject, message, email_from, [email_recipient], - connection=connection - ) - except SMTPSenderRefused as e: - resp = e.smtp_error - if isinstance(resp, bytes): - for coding in ('gbk', 'utf8'): - try: - resp = resp.decode(coding) - except UnicodeDecodeError: - continue - else: - break - return Response({"error": str(resp)}, status=400) - except Exception as e: - print(e) - return Response({"error": str(e)}, status=400) - return Response({"msg": self.success_message.format(email_recipient)}) - else: - return Response({"error": str(serializer.errors)}, status=400) - - class LDAPTestingConfigAPI(APIView): permission_classes = (IsSuperUser,) serializer_class = LDAPTestConfigSerializer @@ -262,59 +207,3 @@ class LDAPCacheRefreshAPI(generics.RetrieveAPIView): return Response(data={'msg': str(e)}, status=400) return Response(data={'msg': 'success'}) - -class PublicSettingApi(generics.RetrieveAPIView): - permission_classes = () - serializer_class = PublicSettingSerializer - - def get_object(self): - instance = { - "data": { - "WINDOWS_SKIP_ALL_MANUAL_PASSWORD": settings.WINDOWS_SKIP_ALL_MANUAL_PASSWORD, - "SECURITY_MAX_IDLE_TIME": settings.SECURITY_MAX_IDLE_TIME, - "XPACK_ENABLED": settings.XPACK_ENABLED, - "XPACK_LICENSE_IS_VALID": settings.XPACK_LICENSE_IS_VALID, - "LOGIN_CONFIRM_ENABLE": settings.LOGIN_CONFIRM_ENABLE, - "SECURITY_VIEW_AUTH_NEED_MFA": settings.SECURITY_VIEW_AUTH_NEED_MFA, - "SECURITY_MFA_VERIFY_TTL": settings.SECURITY_MFA_VERIFY_TTL, - "SECURITY_COMMAND_EXECUTION": settings.SECURITY_COMMAND_EXECUTION, - "LOGIN_TITLE": settings.XPACK_INTERFACE_LOGIN_TITLE, - "SECURITY_PASSWORD_EXPIRATION_TIME": settings.SECURITY_PASSWORD_EXPIRATION_TIME, - "LOGO_URLS": settings.LOGO_URLS, - "TICKETS_ENABLED": settings.TICKETS_ENABLED, - "PASSWORD_RULE": { - 'SECURITY_PASSWORD_MIN_LENGTH': settings.SECURITY_PASSWORD_MIN_LENGTH, - 'SECURITY_PASSWORD_UPPER_CASE': settings.SECURITY_PASSWORD_UPPER_CASE, - 'SECURITY_PASSWORD_LOWER_CASE': settings.SECURITY_PASSWORD_LOWER_CASE, - 'SECURITY_PASSWORD_NUMBER': settings.SECURITY_PASSWORD_NUMBER, - 'SECURITY_PASSWORD_SPECIAL_CHAR': settings.SECURITY_PASSWORD_SPECIAL_CHAR, - } - } - } - return instance - - -class SettingsApi(generics.RetrieveUpdateAPIView): - permission_classes = (IsSuperUser,) - serializer_class = SettingsSerializer - - def get_object(self): - instance = {category: self._get_setting_fields_obj(list(category_serializer.get_fields())) - for category, category_serializer in self.serializer_class().get_fields().items() - if isinstance(category_serializer, serializers.Serializer)} - return ObjectDict(instance) - - def perform_update(self, serializer): - serializer.save() - - def _get_setting_fields_obj(self, category_fields): - if isinstance(category_fields, Iterable): - fields_data = {field_name: getattr(settings, field_name) - for field_name in category_fields} - return ObjectDict(fields_data) - - if isinstance(category_fields, str): - fields_data = {category_fields: getattr(settings, category_fields)} - return ObjectDict(fields_data) - - return ObjectDict() diff --git a/apps/settings/models.py b/apps/settings/models.py index 30732a00c..5617b010d 100644 --- a/apps/settings/models.py +++ b/apps/settings/models.py @@ -3,9 +3,11 @@ import json from django.db import models from django.db.utils import ProgrammingError, OperationalError from django.utils.translation import ugettext_lazy as _ -from django.core.cache import cache +from django.conf import settings -from common.utils import signer +from common.utils import signer, get_logger + +logger = get_logger(__name__) class SettingQuerySet(models.QuerySet): @@ -37,24 +39,6 @@ class Setting(models.Model): def __str__(self): return self.name - @classmethod - def get(cls, item): - cached = cls.get_from_cache(item) - if cached is not None: - return cached - instances = cls.objects.filter(name=item) - if len(instances) == 1: - s = instances[0] - s.refresh_setting() - return s.cleaned_value - return None - - @classmethod - def get_from_cache(cls, item): - key = cls.cache_key_prefix + item - cached = cache.get(key) - return cached - @property def cleaned_value(self): try: @@ -87,9 +71,52 @@ class Setting(models.Model): except (ProgrammingError, OperationalError): pass + @classmethod + def refresh_item(cls, name): + item = cls.objects.filter(name=name).first() + if not item: + return + item.refresh_setting() + def refresh_setting(self): - key = self.cache_key_prefix + self.name - cache.set(key, self.cleaned_value, None) + logger.debug(f"Refresh setting: {self.name}") + if hasattr(self.__class__, f'refresh_{self.name}'): + getattr(self.__class__, f'refresh_{self.name}')() + else: + setattr(settings, self.name, self.cleaned_value) + + @classmethod + def refresh_AUTH_LDAP(cls): + setting = cls.objects.filter(name='AUTH_LDAP').first() + if not setting: + return + ldap_backend = 'authentication.backends.ldap.LDAPAuthorizationBackend' + backends = settings.AUTHENTICATION_BACKENDS + has = ldap_backend in backends + if setting.cleaned_value and not has: + settings.AUTHENTICATION_BACKENDS.insert(0, ldap_backend) + + if not setting.cleaned_value and has: + index = backends.index(ldap_backend) + backends.pop(index) + settings.AUTH_LDAP = setting.cleaned_value + + @classmethod + def update_or_create(cls, name='', value='', encrypted=False, category=''): + """ + 不能使用 Model 提供的,update_or_create 因为这里有 encrypted 和 cleaned_value + :return: (changed, instance) + """ + setting = cls.objects.filter(name=name).first() + changed = False + if not setting: + setting = Setting(name=name, encrypted=encrypted, category=category) + if setting.cleaned_value != value: + setting.encrypted = encrypted + setting.cleaned_value = value + setting.save() + changed = True + return changed, setting class Meta: db_table = "settings_setting" diff --git a/apps/settings/serializers/settings.py b/apps/settings/serializers/settings.py index 208c86078..982d78ff4 100644 --- a/apps/settings/serializers/settings.py +++ b/apps/settings/serializers/settings.py @@ -1,49 +1,100 @@ # coding: utf-8 -from django.db import transaction from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from ..models import Setting -__all__ = ['SettingsSerializer'] +__all__ = [ + 'BasicSettingSerializer', 'EmailSettingSerializer', 'EmailContentSettingSerializer', + 'LDAPSettingSerializer', 'TerminalSettingSerializer', 'SecuritySettingSerializer', + 'SettingsSerializer' +] class BasicSettingSerializer(serializers.Serializer): - SITE_URL = serializers.URLField(required=True) - USER_GUIDE_URL = serializers.URLField(required=False, allow_blank=True, ) - EMAIL_SUBJECT_PREFIX = serializers.CharField(max_length=1024, required=True) + SITE_URL = serializers.URLField( + required=True, label=_("Site url"), + help_text=_('eg: http://demo.jumpserver.org:8080') + ) + USER_GUIDE_URL = serializers.URLField( + required=False, allow_blank=True, label=_("User guide url"), + help_text=_('User first login update profile done redirect to it') + ) class EmailSettingSerializer(serializers.Serializer): - encrypt_fields = ["EMAIL_HOST_PASSWORD", ] + # encrypt_fields 现在使用 write_only 来判断了 - EMAIL_HOST = serializers.CharField(max_length=1024, required=True) - EMAIL_PORT = serializers.CharField(max_length=5, required=True) - EMAIL_HOST_USER = serializers.CharField(max_length=128, required=True) - EMAIL_HOST_PASSWORD = serializers.CharField(max_length=1024, write_only=True, required=False, ) - EMAIL_FROM = serializers.CharField(max_length=128, allow_blank=True, required=False) - EMAIL_RECIPIENT = serializers.CharField(max_length=128, allow_blank=True, required=False) - EMAIL_USE_SSL = serializers.BooleanField(required=False) - EMAIL_USE_TLS = serializers.BooleanField(required=False) + EMAIL_HOST = serializers.CharField(max_length=1024, required=True, label=_("SMTP host")) + EMAIL_PORT = serializers.CharField(max_length=5, required=True, label=_("SMTP port")) + EMAIL_HOST_USER = serializers.CharField(max_length=128, required=True, label=_("SMTP account")) + EMAIL_HOST_PASSWORD = serializers.CharField( + max_length=1024, write_only=True, required=False, label=_("SMTP password"), + help_text=_("Tips: Some provider use token except password") + ) + EMAIL_FROM = serializers.CharField( + max_length=128, allow_blank=True, required=False, label=_('Send user'), + help_text=_('Tips: Send mail account, default SMTP account as the send account') + ) + EMAIL_RECIPIENT = serializers.CharField( + max_length=128, allow_blank=True, required=False, label=_('Test recipient'), + help_text=_('Tips: Used only as a test mail recipient') + ) + EMAIL_USE_SSL = serializers.BooleanField( + required=False, label=_('Use SSL'), + help_text=_('If SMTP port is 465, may be select') + ) + EMAIL_USE_TLS = serializers.BooleanField( + required=False, label=_("Use TLS"), + help_text=_('If SMTP port is 587, may be select') + ) + EMAIL_SUBJECT_PREFIX = serializers.CharField( + max_length=1024, required=True, label=_('Subject prefix') + ) class EmailContentSettingSerializer(serializers.Serializer): - EMAIL_CUSTOM_USER_CREATED_SUBJECT = serializers.CharField(max_length=1024, allow_blank=True, required=False, ) - EMAIL_CUSTOM_USER_CREATED_HONORIFIC = serializers.CharField(max_length=1024, allow_blank=True, required=False, ) - EMAIL_CUSTOM_USER_CREATED_BODY = serializers.CharField(max_length=4096, allow_blank=True, required=False) - EMAIL_CUSTOM_USER_CREATED_SIGNATURE = serializers.CharField(max_length=512, allow_blank=True, required=False) + EMAIL_CUSTOM_USER_CREATED_SUBJECT = serializers.CharField( + max_length=1024, allow_blank=True, required=False, + label=_('Create user email subject'), + help_text=_('Tips: When creating a user, send the subject of the email (eg:Create account successfully)') + ) + EMAIL_CUSTOM_USER_CREATED_HONORIFIC = serializers.CharField( + max_length=1024, allow_blank=True, required=False, + label=_('Create user honorific'), + help_text=_('Tips: When creating a user, send the honorific of the email (eg:Hello)') + ) + EMAIL_CUSTOM_USER_CREATED_BODY = serializers.CharField( + max_length=4096, allow_blank=True, required=False, + label=_('Create user email content'), + help_text=_('Tips:When creating a user, send the content of the email') + ) + EMAIL_CUSTOM_USER_CREATED_SIGNATURE = serializers.CharField( + max_length=512, allow_blank=True, required=False, label=_('Signature'), + help_text=_('Tips: Email signature (eg:jumpserver)') + ) -class LdapSettingSerializer(serializers.Serializer): - encrypt_fields = ["AUTH_LDAP_BIND_PASSWORD", ] +class LDAPSettingSerializer(serializers.Serializer): + # encrypt_fields 现在使用 write_only 来判断了 - AUTH_LDAP_SERVER_URI = serializers.CharField(required=True) - AUTH_LDAP_BIND_DN = serializers.CharField(required=False) - AUTH_LDAP_BIND_PASSWORD = serializers.CharField(max_length=1024, write_only=True, required=False) - AUTH_LDAP_SEARCH_OU = serializers.CharField(max_length=1024, allow_blank=True, required=False) - AUTH_LDAP_SEARCH_FILTER = serializers.CharField(max_length=1024, required=True) - AUTH_LDAP_USER_ATTR_MAP = serializers.DictField(required=True) - AUTH_LDAP = serializers.BooleanField(required=False) + AUTH_LDAP_SERVER_URI = serializers.CharField( + required=True, max_length=1024, label=_('LDAP server'), help_text=_('eg: ldap://localhost:389') + ) + AUTH_LDAP_BIND_DN = serializers.CharField(required=False, max_length=1024, label=_('Bind DN')) + AUTH_LDAP_BIND_PASSWORD = serializers.CharField(max_length=1024, write_only=True, required=False, label=_('Password')) + AUTH_LDAP_SEARCH_OU = serializers.CharField( + max_length=1024, allow_blank=True, required=False, label=_('User OU'), + help_text=_('Use | split multi OUs') + ) + AUTH_LDAP_SEARCH_FILTER = serializers.CharField( + max_length=1024, required=True, label=_('User search filter'), + help_text=_('Choice may be (cn|uid|sAMAccountName)=%(user)s)') + ) + AUTH_LDAP_USER_ATTR_MAP = serializers.DictField( + required=True, label=_('User attr map'), + help_text=_('User attr map present how to map LDAP user attr to jumpserver, username,name,email is jumpserver attr') + ) + AUTH_LDAP = serializers.BooleanField(required=False, label=_('Enable LDAP auth')) class TerminalSettingSerializer(serializers.Serializer): @@ -60,67 +111,75 @@ class TerminalSettingSerializer(serializers.Serializer): ('25', '25'), ('50', '50'), ) - TERMINAL_PASSWORD_AUTH = serializers.BooleanField(required=False) - TERMINAL_PUBLIC_KEY_AUTH = serializers.BooleanField(required=False) - TERMINAL_HEARTBEAT_INTERVAL = serializers.IntegerField(min_value=5, max_value=99999, required=False) - TERMINAL_ASSET_LIST_SORT_BY = serializers.ChoiceField(SORT_BY_CHOICES, required=False) - TERMINAL_ASSET_LIST_PAGE_SIZE = serializers.ChoiceField(PAGE_SIZE_CHOICES, required=False) - TERMINAL_SESSION_KEEP_DURATION = serializers.IntegerField(min_value=1, max_value=99999, required=True) - TERMINAL_TELNET_REGEX = serializers.CharField(allow_blank=True, required=False) + TERMINAL_PASSWORD_AUTH = serializers.BooleanField(required=False, label=_('Password auth')) + TERMINAL_PUBLIC_KEY_AUTH = serializers.BooleanField(required=False, label=_('Public key auth')) + TERMINAL_ASSET_LIST_SORT_BY = serializers.ChoiceField(SORT_BY_CHOICES, required=False, label=_('List sort by')) + TERMINAL_ASSET_LIST_PAGE_SIZE = serializers.ChoiceField(PAGE_SIZE_CHOICES, required=False, label=_('List page size')) + TERMINAL_SESSION_KEEP_DURATION = serializers.IntegerField( + min_value=1, max_value=99999, required=True, label=_('Session keep duration'), + help_text=_('Units: days, Session, record, command will be delete if more than duration, only in database') + ) + TERMINAL_TELNET_REGEX = serializers.CharField(allow_blank=True, max_length=1024, required=False, label=_('Telnet login regex')) class SecuritySettingSerializer(serializers.Serializer): - SECURITY_MFA_AUTH = serializers.BooleanField(required=False) - SECURITY_COMMAND_EXECUTION = serializers.BooleanField(required=False) - SECURITY_SERVICE_ACCOUNT_REGISTRATION = serializers.BooleanField(required=True) - SECURITY_LOGIN_LIMIT_COUNT = serializers.IntegerField(min_value=3, max_value=99999, required=True) - SECURITY_LOGIN_LIMIT_TIME = serializers.IntegerField(min_value=5, max_value=99999, required=True) - SECURITY_MAX_IDLE_TIME = serializers.IntegerField(min_value=1, max_value=99999, required=False) - SECURITY_PASSWORD_EXPIRATION_TIME = serializers.IntegerField(min_value=1, max_value=99999, required=True) - SECURITY_PASSWORD_MIN_LENGTH = serializers.IntegerField(min_value=6, max_value=30, required=True) - SECURITY_PASSWORD_UPPER_CASE = serializers.BooleanField(required=False) - SECURITY_PASSWORD_LOWER_CASE = serializers.BooleanField(required=False) - SECURITY_PASSWORD_NUMBER = serializers.BooleanField(required=False) - SECURITY_PASSWORD_SPECIAL_CHAR = serializers.BooleanField(required=False) - SECURITY_INSECURE_COMMAND = serializers.BooleanField(required=False) - SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER = serializers.CharField(max_length=8192, required=False, allow_blank=True) + SECURITY_MFA_AUTH = serializers.BooleanField( + required=False, label=_("Global MFA auth"), + help_text=_('All user enable MFA') + ) + SECURITY_COMMAND_EXECUTION = serializers.BooleanField( + required=False, label=_('Batch command execution'), + help_text=_('Allow user run batch command or not using ansible') + ) + SECURITY_SERVICE_ACCOUNT_REGISTRATION = serializers.BooleanField( + required=True, label=_('Enable terminal register'), + help_text=_("Allow terminal register, after all terminal setup, you should disable this for security") + ) + SECURITY_LOGIN_LIMIT_COUNT = serializers.IntegerField( + min_value=3, max_value=99999, + label=_('Limit the number of login failures') + ) + SECURITY_LOGIN_LIMIT_TIME = serializers.IntegerField( + min_value=5, max_value=99999, required=True, + label=_('Block logon interval'), + help_text=_('Tip: (unit/minute) if the user has failed to log in for a limited number of times, no login is allowed during this time interval.') + ) + SECURITY_MAX_IDLE_TIME = serializers.IntegerField( + min_value=1, max_value=99999, required=False, + label=_('Connection max idle time'), + help_text=_('If idle time more than it, disconnect connection Unit: minute') + ) + SECURITY_PASSWORD_EXPIRATION_TIME = serializers.IntegerField( + min_value=1, max_value=99999, required=True, + label=_('User password expiration'), + help_text=_('Tip: (unit: day) If the user does not update the password during the time, the user password will expire failure;The password expiration reminder mail will be automatic sent to the user by system within 5 days (daily) before the password expires') + ) + SECURITY_PASSWORD_MIN_LENGTH = serializers.IntegerField( + min_value=6, max_value=30, required=True, + label=_('Password minimum length') + ) + SECURITY_PASSWORD_UPPER_CASE = serializers.BooleanField( + required=False, label=_('Must contain capital') + ) + SECURITY_PASSWORD_LOWER_CASE = serializers.BooleanField(required=False, label=_('Must contain lowercase')) + SECURITY_PASSWORD_NUMBER = serializers.BooleanField(required=False, label=_('Must contain numeric')) + SECURITY_PASSWORD_SPECIAL_CHAR = serializers.BooleanField(required=False, label=_('Must contain special')) + SECURITY_INSECURE_COMMAND = serializers.BooleanField(required=False, label=_('Insecure command alert')) + SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER = serializers.CharField( + max_length=8192, required=False, allow_blank=True, label=_('Email recipient'), + help_text=_('Multiple user using , split') + ) -class SettingsSerializer(serializers.Serializer): - basic = BasicSettingSerializer(required=False) - email = EmailSettingSerializer(required=False) - email_content = EmailContentSettingSerializer(required=False) - ldap = LdapSettingSerializer(required=False) - terminal = TerminalSettingSerializer(required=False) - security = SecuritySettingSerializer(required=False) +class SettingsSerializer( + BasicSettingSerializer, + EmailSettingSerializer, + EmailContentSettingSerializer, + LDAPSettingSerializer, + TerminalSettingSerializer, + SecuritySettingSerializer +): - encrypt_fields = ["EMAIL_HOST_PASSWORD", "AUTH_LDAP_BIND_PASSWORD"] + # encrypt_fields 现在使用 write_only 来判断了 + pass - def create(self, validated_data): - pass - - def update(self, instance, validated_data): - for category, category_data in validated_data.items(): - if not category_data: - continue - self.update_validated_settings(category_data) - for field_name, field_value in category_data.items(): - setattr(getattr(instance, category), field_name, field_value) - - return instance - - def update_validated_settings(self, validated_data, category='default'): - if not validated_data: - return - with transaction.atomic(): - for field_name, field_value in validated_data.items(): - try: - setting = Setting.objects.get(name=field_name) - except Setting.DoesNotExist: - setting = Setting() - encrypted = True if field_name in self.encrypt_fields else False - setting.name = field_name - setting.category = category - setting.encrypted = encrypted - setting.cleaned_value = field_value - setting.save() diff --git a/apps/settings/signals_handler.py b/apps/settings/signals_handler.py index 94c4569bd..9625df3f6 100644 --- a/apps/settings/signals_handler.py +++ b/apps/settings/signals_handler.py @@ -1,28 +1,44 @@ # -*- coding: utf-8 -*- # import json +import threading from django.dispatch import receiver from django.db.models.signals import post_save, pre_save +from django.utils.functional import LazyObject from jumpserver.utils import current_request +from common.decorator import on_transaction_commit from common.utils import get_logger, ssh_key_gen +from common.utils.connection import RedisPubSub from common.signals import django_ready from .models import Setting logger = get_logger(__file__) -@receiver(post_save, sender=Setting, dispatch_uid="my_unique_identifier") +def get_settings_pub_sub(): + return RedisPubSub('settings') + + +class SettingSubPub(LazyObject): + def _setup(self): + self._wrapped = get_settings_pub_sub() + + +setting_pub_sub = SettingSubPub() + + +@receiver(post_save, sender=Setting) +@on_transaction_commit def refresh_settings_on_changed(sender, instance=None, **kwargs): if instance: - instance.refresh_setting() + setting_pub_sub.publish(instance.name) @receiver(django_ready) def on_django_ready_add_db_config(sender, **kwargs): - from django.conf import settings - settings.DYNAMIC.db_setting = Setting + Setting.refresh_all_settings() @receiver(django_ready) @@ -41,9 +57,27 @@ def auto_generate_terminal_host_key(sender, **kwargs): def on_create_set_created_by(sender, instance=None, **kwargs): if getattr(instance, '_ignore_auto_created_by', False) is True: return - if hasattr(instance, 'created_by') and not instance.created_by: - if current_request and current_request.user.is_authenticated: - user_name = current_request.user.name - if isinstance(user_name, str): - user_name = user_name[:30] - instance.created_by = user_name + if not hasattr(instance, 'created_by') or instance.created_by: + return + if current_request and current_request.user.is_authenticated: + user_name = current_request.user.name + if isinstance(user_name, str): + user_name = user_name[:30] + instance.created_by = user_name + + +@receiver(django_ready) +def subscribe_settings_change(sender, **kwargs): + logger.debug("Start subscribe setting change") + + def keep_subscribe(): + sub = setting_pub_sub.subscribe() + for msg in sub.listen(): + if msg["type"] != "message": + continue + item = msg['data'].decode() + logger.debug("Found setting change: {}".format(str(item))) + Setting.refresh_item(item) + t = threading.Thread(target=keep_subscribe) + t.daemon = True + t.start() diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 36e5eb330..1d8590ed0 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -16,7 +16,6 @@ from django.utils.translation import ugettext_lazy as _ from django.utils import timezone from django.shortcuts import reverse -from common.local import LOCAL_DYNAMIC_SETTINGS from orgs.utils import current_org from orgs.models import OrganizationMember, Organization from common.utils import date_expired_default, get_logger, lazyproperty @@ -452,7 +451,7 @@ class MFAMixin: @property def mfa_force_enabled(self): - if LOCAL_DYNAMIC_SETTINGS.SECURITY_MFA_AUTH: + if settings.SECURITY_MFA_AUTH: return True return self.mfa_level == 2