diff --git a/apps/common/api.py b/apps/common/api.py index 51416fa3b..d0512ebca 100644 --- a/apps/common/api.py +++ b/apps/common/api.py @@ -168,13 +168,16 @@ class DjangoSettingsAPI(APIView): return Response("Not in debug mode") data = {} - for k, v in settings.__dict__.items(): - if k and k.isupper(): - try: - json.dumps(v) - data[k] = v - except (json.JSONDecodeError, TypeError): - data[k] = str(v) + for i in [settings, getattr(settings, '_wrapped')]: + if not i: + continue + for k, v in i.__dict__.items(): + if k and k.isupper(): + try: + json.dumps(v) + data[k] = v + except (json.JSONDecodeError, TypeError): + data[k] = str(v) return Response(data) diff --git a/apps/common/forms.py b/apps/common/forms.py index d164699fe..34337159f 100644 --- a/apps/common/forms.py +++ b/apps/common/forms.py @@ -5,7 +5,7 @@ from django import forms from django.utils.translation import ugettext_lazy as _ from django.db import transaction -from .models import Setting, common_settings +from .models import Setting, settings from .fields import FormDictField, FormEncryptCharField, \ FormEncryptMixin @@ -14,7 +14,7 @@ class BaseForm(forms.Form): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) for name, field in self.fields.items(): - value = getattr(common_settings, name) + value = getattr(settings, name, None) # django_value = getattr(settings, name) if hasattr(settings, name) else None if value is None: # and django_value is None: @@ -43,7 +43,7 @@ class BaseForm(forms.Form): field = self.fields[name] if isinstance(field.widget, forms.PasswordInput) and not value: continue - if value == getattr(common_settings, name): + if value == getattr(settings, name): continue encrypted = True if isinstance(field, FormEncryptMixin) else False @@ -69,7 +69,6 @@ class BasicSettingForm(BaseForm): ) EMAIL_SUBJECT_PREFIX = forms.CharField( max_length=1024, label=_("Email Subject Prefix"), - initial="[Jumpserver] " ) @@ -97,21 +96,21 @@ class EmailSettingForm(BaseForm): class LDAPSettingForm(BaseForm): AUTH_LDAP_SERVER_URI = forms.CharField( - label=_("LDAP server"), initial='ldap://localhost:389' + label=_("LDAP server"), ) AUTH_LDAP_BIND_DN = forms.CharField( - label=_("Bind DN"), initial='cn=admin,dc=jumpserver,dc=org' + label=_("Bind DN"), ) AUTH_LDAP_BIND_PASSWORD = FormEncryptCharField( - label=_("Password"), initial='', + label=_("Password"), widget=forms.PasswordInput, required=False ) AUTH_LDAP_SEARCH_OU = forms.CharField( - label=_("User OU"), initial='ou=tech,dc=jumpserver,dc=org', + label=_("User OU"), help_text=_("Use | split User OUs") ) AUTH_LDAP_SEARCH_FILTER = forms.CharField( - label=_("User search filter"), initial='(cn=%(user)s)', + label=_("User search filter"), help_text=_("Choice may be (cn|uid|sAMAccountName)=%(user)s)") ) AUTH_LDAP_USER_ATTR_MAP = FormDictField( @@ -119,14 +118,14 @@ class LDAPSettingForm(BaseForm): help_text=_( "User attr map present how to map LDAP user attr to jumpserver, " "username,name,email is jumpserver attr" - ) + ), ) # AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU # AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER AUTH_LDAP_START_TLS = forms.BooleanField( - label=_("Use SSL"), initial=False, required=False + label=_("Use SSL"), required=False ) - AUTH_LDAP = forms.BooleanField(label=_("Enable LDAP auth"), initial=False, required=False) + AUTH_LDAP = forms.BooleanField(label=_("Enable LDAP auth"), required=False) class TerminalSettingForm(BaseForm): diff --git a/apps/common/models.py b/apps/common/models.py index a1ae9b9ad..a678d32dd 100644 --- a/apps/common/models.py +++ b/apps/common/models.py @@ -2,6 +2,7 @@ import json import ldap from django.db import models +from django.core.cache import cache from django.db.utils import ProgrammingError, OperationalError from django.utils.translation import ugettext_lazy as _ from django.conf import settings @@ -40,15 +41,7 @@ class Setting(models.Model): return self.name def __getattr__(self, item): - default = getattr(settings, item, None) - try: - instances = self.__class__.objects.filter(name=item) - except Exception: - return default - if len(instances) == 1: - return instances[0].cleaned_value - else: - return default + return cache.get(item) @property def cleaned_value(self): @@ -106,22 +99,15 @@ class Setting(models.Model): def refresh_setting(self): setattr(settings, self.name, self.cleaned_value) - if self.name == "AUTH_LDAP": if self.cleaned_value and settings.AUTH_LDAP_BACKEND not in settings.AUTHENTICATION_BACKENDS: - settings.AUTHENTICATION_BACKENDS.insert(0, settings.AUTH_LDAP_BACKEND) + old_setting = settings.AUTHENTICATION_BACKENDS + old_setting.insert(0, settings.AUTH_LDAP_BACKEND) + settings.AUTHENTICATION_BACKENDS = old_setting elif not self.cleaned_value and settings.AUTH_LDAP_BACKEND in settings.AUTHENTICATION_BACKENDS: - settings.AUTHENTICATION_BACKENDS.remove(settings.AUTH_LDAP_BACKEND) - - if self.name == "AUTH_LDAP_SEARCH_FILTER": - settings.AUTH_LDAP_USER_SEARCH_UNION = [ - LDAPSearch(USER_SEARCH, ldap.SCOPE_SUBTREE, settings.AUTH_LDAP_SEARCH_FILTER) - for USER_SEARCH in str(settings.AUTH_LDAP_SEARCH_OU).split("|") - ] - settings.AUTH_LDAP_USER_SEARCH = LDAPSearchUnion(*settings.AUTH_LDAP_USER_SEARCH_UNION) + old_setting = settings.AUTHENTICATION_BACKENDS + old_setting.remove(settings.AUTH_LDAP_BACKEND) + settings.AUTHENTICATION_BACKENDS = old_setting class Meta: db_table = "settings" - - -common_settings = Setting() diff --git a/apps/common/signals.py b/apps/common/signals.py index 6edf140e2..de8a84139 100644 --- a/apps/common/signals.py +++ b/apps/common/signals.py @@ -4,4 +4,3 @@ from django.dispatch import Signal django_ready = Signal() -ldap_auth_enable = Signal(providing_args=["enabled"]) diff --git a/apps/common/signals_handler.py b/apps/common/signals_handler.py index a46dcfd0b..92d4bce97 100644 --- a/apps/common/signals_handler.py +++ b/apps/common/signals_handler.py @@ -2,13 +2,14 @@ # from django.dispatch import receiver from django.db.models.signals import post_save, pre_save -from django.conf import settings +from django.conf import LazySettings, empty from django.db.utils import ProgrammingError, OperationalError +from django.core.cache import cache from jumpserver.utils import current_request from .models import Setting from .utils import get_logger -from .signals import django_ready, ldap_auth_enable +from .signals import django_ready logger = get_logger(__file__) @@ -25,25 +26,43 @@ def refresh_settings_on_changed(sender, instance=None, **kwargs): def refresh_all_settings_on_django_ready(sender, **kwargs): logger.debug("Receive django ready signal") logger.debug(" - fresh all settings") + CACHE_KEY_PREFIX = '_SETTING_' + + def monkey_patch_getattr(self, name): + key = CACHE_KEY_PREFIX + name + cached = cache.get(key) + if cached is not None: + return cached + if self._wrapped is empty: + self._setup(name) + val = getattr(self._wrapped, name) + # self.__dict__[name] = val # Never set it + return val + + def monkey_patch_setattr(self, name, value): + key = CACHE_KEY_PREFIX + name + cache.set(key, value, None) + if name == '_wrapped': + self.__dict__.clear() + else: + self.__dict__.pop(name, None) + super(LazySettings, self).__setattr__(name, value) + + def monkey_patch_delattr(self, name): + super(LazySettings, self).__delattr__(name) + self.__dict__.pop(name, None) + key = CACHE_KEY_PREFIX + name + cache.delete(key) + try: + LazySettings.__getattr__ = monkey_patch_getattr + LazySettings.__setattr__ = monkey_patch_setattr + LazySettings.__delattr__ = monkey_patch_delattr Setting.refresh_all_settings() except (ProgrammingError, OperationalError): pass -@receiver(ldap_auth_enable, dispatch_uid="my_unique_identifier") -def ldap_auth_on_changed(sender, enabled=True, **kwargs): - if enabled: - logger.debug("Enable LDAP auth") - if settings.AUTH_LDAP_BACKEND not in settings.AUTHENTICATION_BACKENDS: - settings.AUTHENTICATION_BACKENDS.insert(0, settings.AUTH_LDAP_BACKEND) - - else: - logger.debug("Disable LDAP auth") - if settings.AUTH_LDAP_BACKEND in settings.AUTHENTICATION_BACKENDS: - settings.AUTHENTICATION_BACKENDS.remove(settings.AUTH_LDAP_BACKEND) - - @receiver(pre_save, dispatch_uid="my_unique_identifier") def on_create_set_created_by(sender, instance=None, **kwargs): if hasattr(instance, 'created_by') and not instance.created_by: diff --git a/apps/common/tasks.py b/apps/common/tasks.py index 00420bc8b..5aefd28ee 100644 --- a/apps/common/tasks.py +++ b/apps/common/tasks.py @@ -3,7 +3,6 @@ from django.conf import settings from celery import shared_task from .utils import get_logger from .models import Setting -from common.models import common_settings logger = get_logger(__file__) @@ -23,13 +22,9 @@ def send_mail_async(*args, **kwargs): Example: send_mail_sync.delay(subject, message, recipient_list, fail_silently=False, html_message=None) """ - configs = Setting.objects.filter(name__startswith='EMAIL') - for config in configs: - setattr(settings, config.name, config.cleaned_value) - if len(args) == 3: args = list(args) - args[0] = common_settings.EMAIL_SUBJECT_PREFIX + args[0] + args[0] = settings.EMAIL_SUBJECT_PREFIX + args[0] args.insert(2, settings.EMAIL_HOST_USER) args = tuple(args) diff --git a/apps/common/urls/api_urls.py b/apps/common/urls/api_urls.py index 3c86a3a2a..a4ae8f9f1 100644 --- a/apps/common/urls/api_urls.py +++ b/apps/common/urls/api_urls.py @@ -13,5 +13,5 @@ urlpatterns = [ path('terminal/replay-storage/delete/', api.ReplayStorageDeleteAPI.as_view(), name='replay-storage-delete'), path('terminal/command-storage/create/', api.CommandStorageCreateAPI.as_view(), name='command-storage-create'), path('terminal/command-storage/delete/', api.CommandStorageDeleteAPI.as_view(), name='command-storage-delete'), - # path('django-settings/', api.DjangoSettingsAPI.as_view(), name='django-settings'), + path('django-settings/', api.DjangoSettingsAPI.as_view(), name='django-settings'), ] diff --git a/apps/common/utils.py b/apps/common/utils.py index 74baec06b..13985d300 100644 --- a/apps/common/utils.py +++ b/apps/common/utils.py @@ -37,8 +37,7 @@ def reverse(view_name, urlconf=None, args=None, kwargs=None, kwargs=kwargs, current_app=current_app) if external: - from common.models import common_settings - site_url = common_settings.SITE_URL + site_url = settings.SITE_URL url = site_url.strip('/') + url return url @@ -390,17 +389,15 @@ def get_request_ip(request): def get_command_storage_setting(): - from common.models import common_settings - default = settings.TERMINAL_COMMAND_STORAGE - value = common_settings.TERMINAL_COMMAND_STORAGE + default = settings.DEFAULT_TERMINAL_COMMAND_STORAGE + value = settings.TERMINAL_COMMAND_STORAGE value.update(default) return value def get_replay_storage_setting(): - from common.models import common_settings - default = settings.TERMINAL_REPLAY_STORAGE - value = common_settings.TERMINAL_REPLAY_STORAGE + default = settings.DEFAULT_TERMINAL_REPLAY_STORAGE + value = settings.TERMINAL_REPLAY_STORAGE value.update(default) return value diff --git a/apps/common/views.py b/apps/common/views.py index 80f4c3b2b..524b6cbd4 100644 --- a/apps/common/views.py +++ b/apps/common/views.py @@ -6,7 +6,6 @@ from django.utils.translation import ugettext as _ from .forms import EmailSettingForm, LDAPSettingForm, BasicSettingForm, \ TerminalSettingForm, SecuritySettingForm from common.permissions import SuperUserRequiredMixin -from .signals import ldap_auth_enable from . import utils @@ -79,8 +78,6 @@ class LDAPSettingView(SuperUserRequiredMixin, TemplateView): form = self.form_class(request.POST) if form.is_valid(): form.save() - if "AUTH_LDAP" in form.cleaned_data: - ldap_auth_enable.send(sender=self.__class__, enabled=form.cleaned_data["AUTH_LDAP"]) msg = _("Update setting successfully, please restart program") messages.success(request, msg) return redirect('settings:ldap-setting') diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py new file mode 100644 index 000000000..aceb3c841 --- /dev/null +++ b/apps/jumpserver/conf.py @@ -0,0 +1,331 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +import os +import sys +import types +import errno +import json +import yaml +from importlib import import_module + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +PROJECT_DIR = os.path.dirname(BASE_DIR) + + +def import_string(dotted_path): + try: + module_path, class_name = dotted_path.rsplit('.', 1) + except ValueError as err: + raise ImportError("%s doesn't look like a module path" % dotted_path) from err + + module = import_module(module_path) + + try: + return getattr(module, class_name) + except AttributeError as err: + raise ImportError('Module "%s" does not define a "%s" attribute/class' % ( + module_path, class_name) + ) from err + + +class Config(dict): + """Works exactly like a dict but provides ways to fill it from files + or special dictionaries. There are two common patterns to populate the + config. + + Either you can fill the config from a config file:: + + app.config.from_pyfile('yourconfig.cfg') + + Or alternatively you can define the configuration options in the + module that calls :meth:`from_object` or provide an import path to + a module that should be loaded. It is also possible to tell it to + use the same module and with that provide the configuration values + just before the call:: + + DEBUG = True + SECRET_KEY = 'development key' + app.config.from_object(__name__) + + In both cases (loading from any Python file or loading from modules), + only uppercase keys are added to the config. This makes it possible to use + lowercase values in the config file for temporary values that are not added + to the config or to define the config keys in the same file that implements + the application. + + Probably the most interesting way to load configurations is from an + environment variable pointing to a file:: + + app.config.from_envvar('YOURAPPLICATION_SETTINGS') + + In this case before launching the application you have to set this + environment variable to the file you want to use. On Linux and OS X + use the export statement:: + + export YOURAPPLICATION_SETTINGS='/path/to/config/file' + + On windows use `set` instead. + + :param root_path: path to which files are read relative from. When the + config object is created by the application, this is + the application's :attr:`~flask.Flask.root_path`. + :param defaults: an optional dictionary of default values + """ + + def __init__(self, root_path=None, defaults=None): + self.defaults = defaults or {} + self.root_path = root_path + super().__init__({}) + + def from_envvar(self, variable_name, silent=False): + """Loads a configuration from an environment variable pointing to + a configuration file. This is basically just a shortcut with nicer + error messages for this line of code:: + + app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS']) + + :param variable_name: name of the environment variable + :param silent: set to ``True`` if you want silent failure for missing + files. + :return: bool. ``True`` if able to load config, ``False`` otherwise. + """ + rv = os.environ.get(variable_name) + if not rv: + if silent: + return False + raise RuntimeError('The environment variable %r is not set ' + 'and as such configuration could not be ' + 'loaded. Set this variable and make it ' + 'point to a configuration file' % + variable_name) + return self.from_pyfile(rv, silent=silent) + + def from_pyfile(self, filename, silent=False): + """Updates the values in the config from a Python file. This function + behaves as if the file was imported as module with the + :meth:`from_object` function. + + :param filename: the filename of the config. This can either be an + absolute filename or a filename relative to the + root path. + :param silent: set to ``True`` if you want silent failure for missing + files. + + .. versionadded:: 0.7 + `silent` parameter. + """ + if self.root_path: + filename = os.path.join(self.root_path, filename) + d = types.ModuleType('config') + d.__file__ = filename + try: + with open(filename, mode='rb') as config_file: + exec(compile(config_file.read(), filename, 'exec'), d.__dict__) + except IOError as e: + if silent and e.errno in (errno.ENOENT, errno.EISDIR): + return False + e.strerror = 'Unable to load configuration file (%s)' % e.strerror + raise + self.from_object(d) + return True + + def from_object(self, obj): + """Updates the values from the given object. An object can be of one + of the following two types: + + - a string: in this case the object with that name will be imported + - an actual object reference: that object is used directly + + Objects are usually either modules or classes. :meth:`from_object` + loads only the uppercase attributes of the module/class. A ``dict`` + object will not work with :meth:`from_object` because the keys of a + ``dict`` are not attributes of the ``dict`` class. + + Example of module-based configuration:: + + app.config.from_object('yourapplication.default_config') + from yourapplication import default_config + app.config.from_object(default_config) + + You should not use this function to load the actual configuration but + rather configuration defaults. The actual config should be loaded + with :meth:`from_pyfile` and ideally from a location not within the + package because the package might be installed system wide. + + See :ref:`config-dev-prod` for an example of class-based configuration + using :meth:`from_object`. + + :param obj: an import name or object + """ + if isinstance(obj, str): + obj = import_string(obj) + for key in dir(obj): + if key.isupper(): + self[key] = getattr(obj, key) + + def from_json(self, filename, silent=False): + """Updates the values in the config from a JSON file. This function + behaves as if the JSON object was a dictionary and passed to the + :meth:`from_mapping` function. + + :param filename: the filename of the JSON file. This can either be an + absolute filename or a filename relative to the + root path. + :param silent: set to ``True`` if you want silent failure for missing + files. + + .. versionadded:: 0.11 + """ + if self.root_path: + filename = os.path.join(self.root_path, filename) + try: + with open(filename) as json_file: + obj = json.loads(json_file.read()) + except IOError as e: + if silent and e.errno in (errno.ENOENT, errno.EISDIR): + return False + e.strerror = 'Unable to load configuration file (%s)' % e.strerror + raise + return self.from_mapping(obj) + + def from_yaml(self, filename, silent=False): + if self.root_path: + filename = os.path.join(self.root_path, filename) + try: + with open(filename) as json_file: + obj = yaml.load(json_file) + except IOError as e: + if silent and e.errno in (errno.ENOENT, errno.EISDIR): + return False + e.strerror = 'Unable to load configuration file (%s)' % e.strerror + raise + return self.from_mapping(obj) + + def from_mapping(self, *mapping, **kwargs): + """Updates the config like :meth:`update` ignoring items with non-upper + keys. + + .. versionadded:: 0.11 + """ + mappings = [] + if len(mapping) == 1: + if hasattr(mapping[0], 'items'): + mappings.append(mapping[0].items()) + else: + mappings.append(mapping[0]) + elif len(mapping) > 1: + raise TypeError( + 'expected at most 1 positional argument, got %d' % len(mapping) + ) + mappings.append(kwargs.items()) + for mapping in mappings: + for (key, value) in mapping: + if key.isupper(): + self[key] = value + return True + + def get_namespace(self, namespace, lowercase=True, trim_namespace=True): + """Returns a dictionary containing a subset of configuration options + that match the specified namespace/prefix. Example usage:: + + app.config['IMAGE_STORE_TYPE'] = 'fs' + app.config['IMAGE_STORE_PATH'] = '/var/app/images' + app.config['IMAGE_STORE_BASE_URL'] = 'http://img.website.com' + image_store_config = app.config.get_namespace('IMAGE_STORE_') + + The resulting dictionary `image_store_config` would look like:: + + { + 'types': 'fs', + 'path': '/var/app/images', + 'base_url': 'http://img.website.com' + } + + This is often useful when configuration options map directly to + keyword arguments in functions or class constructors. + + :param namespace: a configuration namespace + :param lowercase: a flag indicating if the keys of the resulting + dictionary should be lowercase + :param trim_namespace: a flag indicating if the keys of the resulting + dictionary should not include the namespace + + .. versionadded:: 0.11 + """ + rv = {} + for k, v in self.items(): + if not k.startswith(namespace): + continue + if trim_namespace: + key = k[len(namespace):] + else: + key = k + if lowercase: + key = key.lower() + rv[key] = v + return rv + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self)) + + def __getitem__(self, item): + try: + value = super().__getitem__(item) + except KeyError: + value = None + if value is not None: + return value + value = os.environ.get(item, None) + if value is not None: + return value + return self.defaults.get(item) + + def __getattr__(self, item): + return self.__getitem__(item) + + +defaults = { + 'SECRET_KEY': '2vym+ky!997d5kkcc64mnz06y1mmui3lut#(^wd=%s_qj$1%x', + 'BOOTSTRAP_TOKEN': 'PleaseChangeMe', + 'DEBUG': True, + 'SITE_URL': 'http://localhost', + 'LOG_LEVEL': 'DEBUG', + 'LOG_DIR': os.path.join(PROJECT_DIR, 'logs'), + 'DB_ENGINE': 'mysql', + 'DB_NAME': 'jumpserver', + 'DB_HOST': '127.0.0.1', + 'DB_PORT': 3306, + 'DB_USER': 'root', + 'DB_PASSWORD': '', + 'REDIS_HOST': '127.0.0.1', + 'REDIS_PORT': 6379, + 'REDIS_PASSWORD': '', + 'REDIS_DB_CELERY_BROKER': 3, + 'REDIS_DB_CACHE': 4, + 'CAPTCHA_TEST_MODE': None, + 'TOKEN_EXPIRATION': 3600, + 'DISPLAY_PER_PAGE': 25, + 'DEFAULT_EXPIRED_YEARS': 70, + 'SESSION_COOKIE_DOMAIN': None, + 'CSRF_COOKIE_DOMAIN': None, + 'SESSION_COOKIE_AGE': 3600 * 24, + 'AUTH_OPENID': False, +} + + +def load_user_config(): + sys.path.insert(0, PROJECT_DIR) + config = Config(PROJECT_DIR, defaults) + try: + from config import config as c + config.from_object(c) + except ImportError: + msg = """ + + Error: No config file found. + + You can run `cp config_example.py config.py`, and edit it. + """ + raise ImportError(msg) + return config diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index 19419b769..7433435e1 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -17,24 +17,12 @@ import ldap from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion from django.urls import reverse_lazy +from .conf import load_user_config + # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_DIR = os.path.dirname(BASE_DIR) - -sys.path.append(PROJECT_DIR) - -# Import project config setting -try: - from config import config as CONFIG -except ImportError: - msg = """ - - Error: No config file found. - - You can run `cp config_example.py config.py`, and edit it. - """ - raise ImportError(msg) - # CONFIG = type('_', (), {'__getattr__': lambda arg1, arg2: None})() +CONFIG = load_user_config() # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ @@ -43,15 +31,15 @@ except ImportError: SECRET_KEY = CONFIG.SECRET_KEY # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = CONFIG.DEBUG or False +DEBUG = CONFIG.DEBUG # Absolute url for some case, for example email link -SITE_URL = CONFIG.SITE_URL or 'http://localhost' +SITE_URL = CONFIG.SITE_URL # LOG LEVEL -LOG_LEVEL = 'DEBUG' if DEBUG else CONFIG.LOG_LEVEL or 'WARNING' +LOG_LEVEL = CONFIG.LOG_LEVEL -ALLOWED_HOSTS = CONFIG.ALLOWED_HOSTS or [] +ALLOWED_HOSTS = ['*'] # Application definition @@ -152,9 +140,9 @@ TEMPLATES = [ LOGIN_REDIRECT_URL = reverse_lazy('index') LOGIN_URL = reverse_lazy('users:login') -SESSION_COOKIE_DOMAIN = CONFIG.SESSION_COOKIE_DOMAIN or None -CSRF_COOKIE_DOMAIN = CONFIG.CSRF_COOKIE_DOMAIN or None -SESSION_COOKIE_AGE = CONFIG.SESSION_COOKIE_AGE or 3600 * 24 +SESSION_COOKIE_DOMAIN = CONFIG.SESSION_COOKIE_DOMAIN +CSRF_COOKIE_DOMAIN = CONFIG.CSRF_COOKIE_DOMAIN +SESSION_COOKIE_AGE = CONFIG.SESSION_COOKIE_AGE MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' # Database @@ -317,13 +305,13 @@ MEDIA_ROOT = os.path.join(PROJECT_DIR, 'data', 'media').replace('\\', '/') + '/' FIXTURE_DIRS = [os.path.join(BASE_DIR, 'fixtures'), ] # Email config -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_USE_SSL = CONFIG.EMAIL_USE_SSL -EMAIL_USE_TLS = CONFIG.EMAIL_USE_TLS -EMAIL_SUBJECT_PREFIX = CONFIG.EMAIL_SUBJECT_PREFIX +EMAIL_HOST = 'smtp.jumpserver.org' +EMAIL_PORT = 25 +EMAIL_HOST_USER = 'noreply@jumpserver.org' +EMAIL_HOST_PASSWORD = '' +EMAIL_USE_SSL = False +EMAIL_USE_TLS = False +EMAIL_SUBJECT_PREFIX = '[Jumpserver] ' REST_FRAMEWORK = { # Use Django's standard `django.contrib.auth` permissions, @@ -363,23 +351,23 @@ FILE_UPLOAD_PERMISSIONS = 0o644 FILE_UPLOAD_DIRECTORY_PERMISSIONS = 0o755 # Auth LDAP settings -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_SEARCH_UNION = [ +AUTH_LDAP = False +AUTH_LDAP_SERVER_URI = 'ldap://localhost:389' +AUTH_LDAP_BIND_DN = 'cn=admin,dc=jumpserver,dc=org' +AUTH_LDAP_BIND_PASSWORD = '' +AUTH_LDAP_SEARCH_OU = 'ou=tech,dc=jumpserver,dc=org' +AUTH_LDAP_SEARCH_FILTER = '(cn=%(user)s)' +AUTH_LDAP_START_TLS = False +AUTH_LDAP_USER_ATTR_MAP = {"username": "cn", "name": "sn", "email": "mail"} +AUTH_LDAP_USER_SEARCH_UNION = lambda: [ LDAPSearch(USER_SEARCH, ldap.SCOPE_SUBTREE, AUTH_LDAP_SEARCH_FILTER) for USER_SEARCH in str(AUTH_LDAP_SEARCH_OU).split("|") ] -AUTH_LDAP_USER_SEARCH = LDAPSearchUnion(*AUTH_LDAP_USER_SEARCH_UNION) +AUTH_LDAP_USER_SEARCH = lambda: LDAPSearchUnion(*AUTH_LDAP_USER_SEARCH_UNION()) AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER AUTH_LDAP_GROUP_SEARCH = LDAPSearch( - AUTH_LDAP_GROUP_SEARCH_OU, ldap.SCOPE_SUBTREE, AUTH_LDAP_GROUP_SEARCH_FILTER + AUTH_LDAP_GROUP_SEARCH_OU, ldap.SCOPE_SUBTREE, AUTH_LDAP_GROUP_SEARCH_FILTER ) AUTH_LDAP_CONNECTION_OPTIONS = { ldap.OPT_TIMEOUT: 5 @@ -414,7 +402,7 @@ CELERY_BROKER_URL = 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % { 'password': CONFIG.REDIS_PASSWORD if CONFIG.REDIS_PASSWORD else '', 'host': CONFIG.REDIS_HOST or '127.0.0.1', 'port': CONFIG.REDIS_PORT or 6379, - 'db':CONFIG.REDIS_DB_CELERY_BROKER or 3, + 'db': CONFIG.REDIS_DB_CELERY_BROKER or 3, } CELERY_TASK_SERIALIZER = 'pickle' CELERY_RESULT_SERIALIZER = 'pickle' @@ -436,10 +424,10 @@ CACHES = { 'default': { 'BACKEND': 'redis_cache.RedisCache', 'LOCATION': 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % { - 'password': CONFIG.REDIS_PASSWORD if CONFIG.REDIS_PASSWORD else '', - 'host': CONFIG.REDIS_HOST or '127.0.0.1', - 'port': CONFIG.REDIS_PORT or 6379, - 'db': CONFIG.REDIS_DB_CACHE or 4, + 'password': CONFIG.REDIS_PASSWORD, + 'host': CONFIG.REDIS_HOST, + 'port': CONFIG.REDIS_PORT, + 'db': CONFIG.REDIS_DB_CACHE, } } } @@ -454,27 +442,44 @@ COMMAND_STORAGE = { 'ENGINE': 'terminal.backends.command.db', } -TERMINAL_COMMAND_STORAGE = { +DEFAULT_TERMINAL_COMMAND_STORAGE = { "default": { "TYPE": "server", }, +} + +TERMINAL_COMMAND_STORAGE = { # 'ali-es': { # 'TYPE': 'elasticsearch', # 'HOSTS': ['http://elastic:changeme@localhost:9200'], # }, } -TERMINAL_REPLAY_STORAGE = { +DEFAULT_TERMINAL_REPLAY_STORAGE = { "default": { "TYPE": "server", }, } +TERMINAL_REPLAY_STORAGE = { +} -SECURITY_PASSWORD_MIN_LENGTH = 6 +SECURITY_MFA_AUTH = False SECURITY_LOGIN_LIMIT_COUNT = 7 SECURITY_LOGIN_LIMIT_TIME = 30 # Unit: minute SECURITY_MAX_IDLE_TIME = 30 # Unit: minute +SECURITY_PASSWORD_MIN_LENGTH = 6 +SECURITY_PASSWORD_UPPER_CASE = False +SECURITY_PASSWORD_LOWER_CASE = False +SECURITY_PASSWORD_NUMBER = False +SECURITY_PASSWORD_SPECIAL_CHAR = False +SECURITY_PASSWORD_RULES = [ + 'SECURITY_PASSWORD_MIN_LENGTH', + 'SECURITY_PASSWORD_UPPER_CASE', + 'SECURITY_PASSWORD_LOWER_CASE', + 'SECURITY_PASSWORD_NUMBER', + 'SECURITY_PASSWORD_SPECIAL_CHAR' +] # Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html BOOTSTRAP3 = { @@ -486,8 +491,8 @@ BOOTSTRAP3 = { 'success_css_class': '', } -TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION or 3600 -DISPLAY_PER_PAGE = CONFIG.DISPLAY_PER_PAGE or 25 +TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION +DISPLAY_PER_PAGE = CONFIG.DISPLAY_PER_PAGE DEFAULT_EXPIRED_YEARS = 70 USER_GUIDE_URL = "" diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index bb3a01026..0f6f072a5 100644 Binary files a/apps/locale/zh/LC_MESSAGES/django.mo and b/apps/locale/zh/LC_MESSAGES/django.mo differ diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 361e05589..c5ec299d2 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Jumpserver 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-11-08 19:18+0800\n" +"POT-Creation-Date: 2018-11-21 19:06+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: Jumpserver team\n" @@ -126,7 +126,7 @@ msgstr "端口" #: perms/templates/perms/asset_permission_create_update.html:40 #: perms/templates/perms/asset_permission_list.html:56 #: perms/templates/perms/asset_permission_list.html:148 -#: terminal/backends/command/models.py:13 terminal/models.py:133 +#: terminal/backends/command/models.py:13 terminal/models.py:134 #: terminal/templates/terminal/command_list.html:40 #: terminal/templates/terminal/command_list.html:73 #: terminal/templates/terminal/session_list.html:41 @@ -154,7 +154,7 @@ msgstr "不能包含特殊字符" #: assets/templates/assets/domain_list.html:25 #: assets/templates/assets/label_list.html:14 #: assets/templates/assets/system_user_detail.html:58 -#: assets/templates/assets/system_user_list.html:29 common/models.py:30 +#: assets/templates/assets/system_user_list.html:29 common/models.py:31 #: common/templates/common/command_storage_create.html:41 #: common/templates/common/replay_storage_create.html:44 #: common/templates/common/terminal_setting.html:80 @@ -164,9 +164,9 @@ msgstr "不能包含特殊字符" #: perms/templates/perms/asset_permission_detail.html:62 #: perms/templates/perms/asset_permission_list.html:53 #: perms/templates/perms/asset_permission_user.html:54 terminal/models.py:18 -#: terminal/models.py:160 terminal/templates/terminal/terminal_detail.html:43 +#: terminal/models.py:161 terminal/templates/terminal/terminal_detail.html:43 #: terminal/templates/terminal/terminal_list.html:29 users/models/group.py:14 -#: users/models/user.py:51 users/templates/users/_select_user_modal.html:13 +#: users/models/user.py:52 users/templates/users/_select_user_modal.html:13 #: users/templates/users/user_detail.html:63 #: users/templates/users/user_group_detail.html:55 #: users/templates/users/user_group_list.html:12 @@ -191,7 +191,7 @@ msgstr "名称" #: assets/templates/assets/system_user_list.html:30 #: audits/templates/audits/login_log_list.html:49 #: perms/templates/perms/asset_permission_user.html:55 users/forms.py:15 -#: users/forms.py:33 users/models/authentication.py:72 users/models/user.py:49 +#: users/forms.py:33 users/models/authentication.py:72 users/models/user.py:50 #: users/templates/users/_select_user_modal.html:14 #: users/templates/users/login.html:62 #: users/templates/users/user_detail.html:67 @@ -204,7 +204,7 @@ msgstr "用户名" msgid "Password or private key passphrase" msgstr "密码或密钥密码" -#: assets/forms/user.py:26 assets/models/base.py:24 common/forms.py:107 +#: assets/forms/user.py:26 assets/models/base.py:24 common/forms.py:105 #: users/forms.py:17 users/forms.py:35 users/forms.py:47 #: users/templates/users/login.html:65 #: users/templates/users/reset_password.html:53 @@ -216,7 +216,7 @@ msgstr "密码或密钥密码" msgid "Password" msgstr "密码" -#: assets/forms/user.py:29 users/models/user.py:78 +#: assets/forms/user.py:29 users/models/user.py:79 msgid "Private key" msgstr "ssh私钥" @@ -265,7 +265,7 @@ msgstr "如果选择手动登录模式,用户名和密码可以不填写" #: assets/templates/assets/system_user_asset.html:51 #: assets/templates/assets/user_asset_list.html:46 #: assets/templates/assets/user_asset_list.html:162 -#: audits/templates/audits/login_log_list.html:52 common/forms.py:136 +#: audits/templates/audits/login_log_list.html:52 common/forms.py:134 #: perms/templates/perms/asset_permission_asset.html:55 #: users/templates/users/user_granted_asset.html:45 #: users/templates/users/user_group_granted_asset.html:45 @@ -278,7 +278,7 @@ msgstr "IP" #: assets/templates/assets/asset_list.html:92 #: assets/templates/assets/system_user_asset.html:50 #: assets/templates/assets/user_asset_list.html:45 -#: assets/templates/assets/user_asset_list.html:161 common/forms.py:135 +#: assets/templates/assets/user_asset_list.html:161 common/forms.py:133 #: perms/templates/perms/asset_permission_asset.html:54 #: users/templates/users/user_granted_asset.html:44 #: users/templates/users/user_group_granted_asset.html:44 @@ -388,7 +388,7 @@ msgstr "标签管理" #: assets/templates/assets/system_user_detail.html:100 #: ops/templates/ops/adhoc_detail.html:86 orgs/models.py:15 perms/models.py:37 #: perms/models.py:84 perms/templates/perms/asset_permission_detail.html:98 -#: users/models/user.py:92 users/templates/users/user_detail.html:111 +#: users/models/user.py:93 users/templates/users/user_detail.html:111 #: xpack/plugins/cloud/models.py:46 xpack/plugins/cloud/models.py:140 msgid "Created by" msgstr "创建者" @@ -426,11 +426,11 @@ msgstr "创建日期" #: assets/templates/assets/domain_list.html:28 #: assets/templates/assets/system_user_detail.html:104 #: assets/templates/assets/system_user_list.html:37 -#: assets/templates/assets/user_asset_list.html:170 common/models.py:35 +#: assets/templates/assets/user_asset_list.html:170 common/models.py:36 #: ops/models/adhoc.py:43 orgs/models.py:17 perms/models.py:39 #: perms/models.py:86 perms/templates/perms/asset_permission_detail.html:102 #: terminal/models.py:28 terminal/templates/terminal/terminal_detail.html:63 -#: users/models/group.py:15 users/models/user.py:84 +#: users/models/group.py:15 users/models/user.py:85 #: users/templates/users/user_detail.html:123 #: users/templates/users/user_group_detail.html:67 #: users/templates/users/user_group_list.html:14 @@ -461,7 +461,7 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:70 +#: assets/models/cluster.py:22 users/models/user.py:71 #: users/templates/users/user_detail.html:76 msgid "Phone" msgstr "手机" @@ -487,7 +487,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:363 +#: users/models/user.py:364 msgid "System" msgstr "系统" @@ -515,7 +515,7 @@ msgstr "BGP全网通" msgid "Regex" msgstr "正则表达式" -#: assets/models/cmd_filter.py:35 terminal/models.py:139 +#: assets/models/cmd_filter.py:35 terminal/models.py:140 #: terminal/templates/terminal/command_list.html:55 #: terminal/templates/terminal/command_list.html:71 #: terminal/templates/terminal/session_detail.html:48 @@ -616,14 +616,14 @@ msgstr "默认资产组" #: perms/templates/perms/asset_permission_create_update.html:36 #: perms/templates/perms/asset_permission_list.html:54 #: perms/templates/perms/asset_permission_list.html:142 templates/index.html:87 -#: terminal/backends/command/models.py:12 terminal/models.py:132 +#: terminal/backends/command/models.py:12 terminal/models.py:133 #: terminal/templates/terminal/command_list.html:32 #: terminal/templates/terminal/command_list.html:72 #: terminal/templates/terminal/session_list.html:33 #: terminal/templates/terminal/session_list.html:71 users/forms.py:312 -#: users/models/user.py:33 users/models/user.py:351 +#: users/models/user.py:32 users/models/user.py:352 #: users/templates/users/user_group_detail.html:78 -#: users/templates/users/user_group_list.html:13 users/views/user.py:385 +#: users/templates/users/user_group_list.html:13 users/views/user.py:384 #: xpack/plugins/orgs/forms.py:26 #: xpack/plugins/orgs/templates/orgs/org_detail.html:113 #: xpack/plugins/orgs/templates/orgs/org_list.html:14 @@ -631,7 +631,7 @@ msgid "User" msgstr "用户" #: assets/models/label.py:19 assets/models/node.py:20 -#: assets/templates/assets/label_list.html:15 common/models.py:31 +#: assets/templates/assets/label_list.html:15 common/models.py:32 msgid "Value" msgstr "值" @@ -699,7 +699,7 @@ msgstr "登录模式" #: perms/templates/perms/asset_permission_detail.html:140 #: perms/templates/perms/asset_permission_list.html:58 #: perms/templates/perms/asset_permission_list.html:154 templates/_nav.html:25 -#: terminal/backends/command/models.py:14 terminal/models.py:134 +#: terminal/backends/command/models.py:14 terminal/models.py:135 #: terminal/templates/terminal/command_list.html:48 #: terminal/templates/terminal/command_list.html:74 #: terminal/templates/terminal/session_list.html:49 @@ -1060,7 +1060,7 @@ msgstr "选择节点" #: users/templates/users/user_detail.html:476 #: users/templates/users/user_group_create_update.html:32 #: users/templates/users/user_group_list.html:88 -#: users/templates/users/user_list.html:201 +#: users/templates/users/user_list.html:205 #: users/templates/users/user_profile.html:232 #: xpack/plugins/cloud/templates/cloud/account_create_update.html:34 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_create.html:36 @@ -1277,7 +1277,7 @@ msgstr "重命名失败,不能更改root节点的名称" #: users/templates/users/user_detail.html:402 #: users/templates/users/user_detail.html:470 #: users/templates/users/user_group_list.html:82 -#: users/templates/users/user_list.html:195 +#: users/templates/users/user_list.html:199 msgid "Are you sure?" msgstr "你确认吗?" @@ -1293,7 +1293,7 @@ msgstr "删除选择资产" #: users/templates/users/user_detail.html:474 #: users/templates/users/user_group_create_update.html:31 #: users/templates/users/user_group_list.html:86 -#: users/templates/users/user_list.html:199 +#: users/templates/users/user_list.html:203 #: xpack/plugins/orgs/templates/orgs/org_create_update.html:32 msgid "Cancel" msgstr "取消" @@ -1624,7 +1624,7 @@ msgstr "系统用户集群资产" #: audits/templates/audits/ftp_log_list.html:73 #: audits/templates/audits/operate_log_list.html:70 #: audits/templates/audits/password_change_log_list.html:52 -#: terminal/models.py:136 terminal/templates/terminal/session_list.html:74 +#: terminal/models.py:137 terminal/templates/terminal/session_list.html:74 #: terminal/templates/terminal/terminal_detail.html:47 msgid "Remote addr" msgstr "远端地址" @@ -1665,7 +1665,7 @@ msgstr "修改者" #: ops/templates/ops/adhoc_history.html:52 #: ops/templates/ops/adhoc_history_detail.html:61 #: ops/templates/ops/task_history.html:58 perms/models.py:35 -#: perms/templates/perms/asset_permission_detail.html:86 terminal/models.py:143 +#: perms/templates/perms/asset_permission_detail.html:86 terminal/models.py:144 #: terminal/templates/terminal/session_list.html:78 msgid "Date start" msgstr "开始日期" @@ -1707,7 +1707,7 @@ msgid "City" msgstr "城市" #: audits/templates/audits/login_log_list.html:54 users/forms.py:169 -#: users/models/authentication.py:77 users/models/user.py:73 +#: users/models/authentication.py:77 users/models/user.py:74 #: users/templates/users/first_login.html:45 msgid "MFA" msgstr "MFA" @@ -1758,9 +1758,9 @@ msgstr "改密日志" #: audits/views.py:187 templates/_nav.html:10 users/views/group.py:28 #: users/views/group.py:44 users/views/group.py:60 users/views/group.py:76 -#: users/views/group.py:92 users/views/login.py:334 users/views/user.py:68 -#: users/views/user.py:83 users/views/user.py:111 users/views/user.py:193 -#: users/views/user.py:354 users/views/user.py:404 users/views/user.py:439 +#: users/views/group.py:92 users/views/login.py:332 users/views/user.py:68 +#: users/views/user.py:83 users/views/user.py:111 users/views/user.py:192 +#: users/views/user.py:353 users/views/user.py:403 users/views/user.py:437 msgid "Users" msgstr "用户管理" @@ -1819,88 +1819,88 @@ msgstr "不是字符类型" msgid "Encrypt field using Secret Key" msgstr "" -#: common/forms.py:64 +#: common/forms.py:63 msgid "Current SITE URL" msgstr "当前站点URL" -#: common/forms.py:68 +#: common/forms.py:67 msgid "User Guide URL" msgstr "用户向导URL" -#: common/forms.py:69 +#: common/forms.py:68 msgid "User first login update profile done redirect to it" msgstr "用户第一次登录,修改profile后重定向到地址" -#: common/forms.py:72 +#: common/forms.py:71 msgid "Email Subject Prefix" msgstr "Email主题前缀" -#: common/forms.py:79 +#: common/forms.py:77 msgid "SMTP host" msgstr "SMTP主机" -#: common/forms.py:81 +#: common/forms.py:79 msgid "SMTP port" msgstr "SMTP端口" -#: common/forms.py:83 +#: common/forms.py:81 msgid "SMTP user" msgstr "SMTP账号" -#: common/forms.py:86 +#: common/forms.py:84 msgid "SMTP password" msgstr "SMTP密码" -#: common/forms.py:87 +#: common/forms.py:85 msgid "Some provider use token except password" msgstr "一些邮件提供商需要输入的是Token" -#: common/forms.py:90 common/forms.py:128 +#: common/forms.py:88 common/forms.py:126 msgid "Use SSL" msgstr "使用SSL" -#: common/forms.py:91 +#: common/forms.py:89 msgid "If SMTP port is 465, may be select" msgstr "如果SMTP端口是465,通常需要启用SSL" -#: common/forms.py:94 +#: common/forms.py:92 msgid "Use TLS" msgstr "使用TLS" -#: common/forms.py:95 +#: common/forms.py:93 msgid "If SMTP port is 587, may be select" msgstr "如果SMTP端口是587,通常需要启用TLS" -#: common/forms.py:101 +#: common/forms.py:99 msgid "LDAP server" msgstr "LDAP地址" -#: common/forms.py:104 +#: common/forms.py:102 msgid "Bind DN" msgstr "绑定DN" -#: common/forms.py:111 +#: common/forms.py:109 msgid "User OU" msgstr "用户OU" -#: common/forms.py:112 +#: common/forms.py:110 msgid "Use | split User OUs" msgstr "使用|分隔各OU" -#: common/forms.py:115 +#: common/forms.py:113 msgid "User search filter" msgstr "用户过滤器" -#: common/forms.py:116 +#: common/forms.py:114 #, python-format msgid "Choice may be (cn|uid|sAMAccountName)=%(user)s)" msgstr "可能的选项是(cn或uid或sAMAccountName=%(user)s)" -#: common/forms.py:119 +#: common/forms.py:117 msgid "User attr map" msgstr "LDAP属性映射" -#: common/forms.py:121 +#: common/forms.py:119 msgid "" "User attr map present how to map LDAP user attr to jumpserver, username,name," "email is jumpserver attr" @@ -1908,103 +1908,103 @@ msgstr "" "用户属性映射代表怎样将LDAP中用户属性映射到jumpserver用户上,username, name," "email 是jumpserver的属性" -#: common/forms.py:130 +#: common/forms.py:128 msgid "Enable LDAP auth" msgstr "启用LDAP认证" -#: common/forms.py:139 +#: common/forms.py:137 msgid "Password auth" msgstr "密码认证" -#: common/forms.py:142 +#: common/forms.py:140 msgid "Public key auth" msgstr "密钥认证" -#: common/forms.py:145 +#: common/forms.py:143 msgid "Heartbeat interval" msgstr "心跳间隔" -#: common/forms.py:145 ops/models/adhoc.py:38 +#: common/forms.py:143 ops/models/adhoc.py:38 msgid "Units: seconds" msgstr "单位: 秒" -#: common/forms.py:148 +#: common/forms.py:146 msgid "List sort by" msgstr "资产列表排序" -#: common/forms.py:160 +#: common/forms.py:158 msgid "MFA Secondary certification" msgstr "MFA 二次认证" -#: common/forms.py:162 +#: common/forms.py:160 msgid "" "After opening, the user login must use MFA secondary authentication (valid " "for all users, including administrators)" msgstr "开启后,用户登录必须使用MFA二次认证(对所有用户有效,包括管理员)" -#: common/forms.py:169 +#: common/forms.py:167 msgid "Limit the number of login failures" msgstr "限制登录失败次数" -#: common/forms.py:174 +#: common/forms.py:172 msgid "No logon interval" msgstr "禁止登录时间间隔" -#: common/forms.py:176 +#: common/forms.py:174 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 "" "提示: (单位: 分钟) 当用户登录失败次数达到限制后,那么在此时间间隔内禁止登录." -#: common/forms.py:182 +#: common/forms.py:180 msgid "Connection max idle time" msgstr "SSH最大空闲时间" -#: common/forms.py:184 +#: common/forms.py:182 msgid "" "If idle time more than it, disconnect connection(only ssh now) Unit: minute" msgstr "提示: (单位: 分钟) 如果超过该配置没有操作,连接会被断开(仅ssh) " -#: common/forms.py:190 +#: common/forms.py:188 msgid "Password minimum length" msgstr "密码最小长度 " -#: common/forms.py:196 +#: common/forms.py:194 msgid "Must contain capital letters" msgstr "必须包含大写字母" -#: common/forms.py:198 +#: common/forms.py:196 msgid "" "After opening, the user password changes and resets must contain uppercase " "letters" msgstr "开启后,用户密码修改、重置必须包含大写字母" -#: common/forms.py:204 +#: common/forms.py:202 msgid "Must contain lowercase letters" msgstr "必须包含小写字母" -#: common/forms.py:205 +#: common/forms.py:203 msgid "" "After opening, the user password changes and resets must contain lowercase " "letters" msgstr "开启后,用户密码修改、重置必须包含小写字母" -#: common/forms.py:211 +#: common/forms.py:209 msgid "Must contain numeric characters" msgstr "必须包含数字字符" -#: common/forms.py:212 +#: common/forms.py:210 msgid "" "After opening, the user password changes and resets must contain numeric " "characters" msgstr "开启后,用户密码修改、重置必须包含数字字符" -#: common/forms.py:218 +#: common/forms.py:216 msgid "Must contain special characters" msgstr "必须包含特殊字符" -#: common/forms.py:219 +#: common/forms.py:217 msgid "" "After opening, the user password changes and resets must contain special " "characters" @@ -2018,7 +2018,7 @@ msgstr "" msgid "discard time" msgstr "" -#: common/models.py:34 users/models/authentication.py:51 +#: common/models.py:35 users/models/authentication.py:51 #: users/templates/users/user_detail.html:96 msgid "Enabled" msgstr "启用" @@ -2028,7 +2028,7 @@ msgstr "启用" #: common/templates/common/ldap_setting.html:15 #: common/templates/common/security_setting.html:15 #: common/templates/common/terminal_setting.html:16 -#: common/templates/common/terminal_setting.html:46 common/views.py:22 +#: common/templates/common/terminal_setting.html:46 common/views.py:19 msgid "Basic setting" msgstr "基本设置" @@ -2036,7 +2036,7 @@ msgstr "基本设置" #: common/templates/common/email_setting.html:18 #: common/templates/common/ldap_setting.html:18 #: common/templates/common/security_setting.html:18 -#: common/templates/common/terminal_setting.html:20 common/views.py:48 +#: common/templates/common/terminal_setting.html:20 common/views.py:45 msgid "Email setting" msgstr "邮件设置" @@ -2044,7 +2044,7 @@ msgstr "邮件设置" #: common/templates/common/email_setting.html:21 #: common/templates/common/ldap_setting.html:21 #: common/templates/common/security_setting.html:21 -#: common/templates/common/terminal_setting.html:24 common/views.py:74 +#: common/templates/common/terminal_setting.html:24 common/views.py:71 msgid "LDAP setting" msgstr "LDAP设置" @@ -2052,7 +2052,7 @@ msgstr "LDAP设置" #: common/templates/common/email_setting.html:24 #: common/templates/common/ldap_setting.html:24 #: common/templates/common/security_setting.html:24 -#: common/templates/common/terminal_setting.html:28 common/views.py:105 +#: common/templates/common/terminal_setting.html:28 common/views.py:100 msgid "Terminal setting" msgstr "终端设置" @@ -2060,7 +2060,7 @@ msgstr "终端设置" #: common/templates/common/email_setting.html:27 #: common/templates/common/ldap_setting.html:27 #: common/templates/common/security_setting.html:27 -#: common/templates/common/terminal_setting.html:31 common/views.py:157 +#: common/templates/common/terminal_setting.html:31 common/views.py:152 msgid "Security setting" msgstr "安全设置" @@ -2168,22 +2168,22 @@ msgstr "您确定删除吗?" msgid "Special char not allowed" msgstr "不能包含特殊字符" -#: common/views.py:21 common/views.py:47 common/views.py:73 common/views.py:104 -#: common/views.py:131 common/views.py:143 common/views.py:156 +#: common/views.py:18 common/views.py:44 common/views.py:70 common/views.py:99 +#: common/views.py:126 common/views.py:138 common/views.py:151 #: templates/_nav.html:116 msgid "Settings" msgstr "系统设置" -#: common/views.py:32 common/views.py:58 common/views.py:86 common/views.py:117 -#: common/views.py:167 +#: common/views.py:29 common/views.py:55 common/views.py:81 common/views.py:112 +#: common/views.py:162 msgid "Update setting successfully, please restart program" msgstr "更新设置成功, 请手动重启程序" -#: common/views.py:132 +#: common/views.py:127 msgid "Create replay storage" msgstr "创建录像存储" -#: common/views.py:144 +#: common/views.py:139 msgid "Create command storage" msgstr "创建命令存储" @@ -2445,7 +2445,7 @@ msgstr "组织管理" #: perms/forms.py:31 perms/models.py:30 perms/models.py:80 #: perms/templates/perms/asset_permission_list.html:55 #: perms/templates/perms/asset_permission_list.html:145 templates/_nav.html:14 -#: users/forms.py:282 users/models/group.py:26 users/models/user.py:57 +#: users/forms.py:282 users/models/group.py:26 users/models/user.py:58 #: users/templates/users/_select_user_modal.html:16 #: users/templates/users/user_detail.html:207 #: users/templates/users/user_list.html:26 @@ -2463,7 +2463,7 @@ msgstr "资产和节点至少选一个" #: perms/models.py:36 perms/models.py:83 #: perms/templates/perms/asset_permission_detail.html:90 -#: users/models/user.py:89 users/templates/users/user_detail.html:107 +#: users/models/user.py:90 users/templates/users/user_detail.html:107 #: users/templates/users/user_profile.html:112 msgid "Date expired" msgstr "失效日期" @@ -2594,7 +2594,7 @@ msgstr "商业支持" #: users/templates/users/user_profile.html:17 #: users/templates/users/user_profile_update.html:37 #: users/templates/users/user_profile_update.html:57 -#: users/templates/users/user_pubkey_update.html:37 users/views/user.py:367 +#: users/templates/users/user_pubkey_update.html:37 users/views/user.py:366 msgid "Profile" msgstr "个人信息" @@ -2906,43 +2906,43 @@ msgstr "SSH端口" msgid "HTTP Port" msgstr "HTTP端口" -#: terminal/models.py:104 +#: terminal/models.py:105 msgid "Session Online" msgstr "在线会话" -#: terminal/models.py:105 +#: terminal/models.py:106 msgid "CPU Usage" msgstr "CPU使用" -#: terminal/models.py:106 +#: terminal/models.py:107 msgid "Memory Used" msgstr "内存使用" -#: terminal/models.py:107 +#: terminal/models.py:108 msgid "Connections" msgstr "连接数" -#: terminal/models.py:108 +#: terminal/models.py:109 msgid "Threads" msgstr "线程数" -#: terminal/models.py:109 +#: terminal/models.py:110 msgid "Boot Time" msgstr "运行时间" -#: terminal/models.py:138 terminal/templates/terminal/session_list.html:104 +#: terminal/models.py:139 terminal/templates/terminal/session_list.html:104 msgid "Replay" msgstr "回放" -#: terminal/models.py:142 +#: terminal/models.py:143 msgid "Date last active" msgstr "最后活跃日期" -#: terminal/models.py:144 +#: terminal/models.py:145 msgid "Date end" msgstr "结束日期" -#: terminal/models.py:161 +#: terminal/models.py:162 msgid "Args" msgstr "参数" @@ -3092,11 +3092,11 @@ msgstr "登录频繁, 稍后重试" msgid "Please carry seed value and conduct MFA secondary certification" msgstr "请携带seed值, 进行MFA二次认证" -#: users/api/auth.py:196 +#: users/api/auth.py:195 msgid "Please verify the user name and password first" msgstr "请先进行用户名和密码验证" -#: users/api/auth.py:208 +#: users/api/auth.py:207 msgid "MFA certification failed" msgstr "MFA认证失败" @@ -3159,7 +3159,7 @@ msgstr "" msgid "MFA code" msgstr "MFA 验证码" -#: users/forms.py:52 users/models/user.py:61 +#: users/forms.py:52 users/models/user.py:62 #: users/templates/users/_select_user_modal.html:15 #: users/templates/users/user_detail.html:87 #: users/templates/users/user_list.html:25 @@ -3247,7 +3247,7 @@ msgstr "自动配置并下载SSH密钥" msgid "Paste your id_rsa.pub here." msgstr "复制你的公钥到这里" -#: users/forms.py:258 users/models/user.py:81 +#: users/forms.py:258 users/models/user.py:82 #: users/templates/users/first_login.html:42 #: users/templates/users/user_password_update.html:46 #: users/templates/users/user_profile.html:68 @@ -3310,49 +3310,49 @@ msgstr "Agent" msgid "Date login" msgstr "登录日期" -#: users/models/user.py:32 users/models/user.py:359 +#: users/models/user.py:31 users/models/user.py:360 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:34 +#: users/models/user.py:33 msgid "Application" msgstr "应用程序" -#: users/models/user.py:37 users/templates/users/user_profile.html:92 +#: users/models/user.py:36 users/templates/users/user_profile.html:92 #: users/templates/users/user_profile.html:163 #: users/templates/users/user_profile.html:166 msgid "Disable" msgstr "禁用" -#: users/models/user.py:38 users/templates/users/user_profile.html:90 +#: users/models/user.py:37 users/templates/users/user_profile.html:90 #: users/templates/users/user_profile.html:170 msgid "Enable" msgstr "启用" -#: users/models/user.py:39 users/templates/users/user_profile.html:88 +#: users/models/user.py:38 users/templates/users/user_profile.html:88 msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:53 users/templates/users/user_detail.html:71 +#: users/models/user.py:54 users/templates/users/user_detail.html:71 #: users/templates/users/user_profile.html:59 msgid "Email" msgstr "邮件" -#: users/models/user.py:64 +#: users/models/user.py:65 msgid "Avatar" msgstr "头像" -#: users/models/user.py:67 users/templates/users/user_detail.html:82 +#: users/models/user.py:68 users/templates/users/user_detail.html:82 msgid "Wechat" msgstr "微信" -#: users/models/user.py:96 users/templates/users/user_detail.html:103 +#: users/models/user.py:97 users/templates/users/user_detail.html:103 #: users/templates/users/user_list.html:27 #: users/templates/users/user_profile.html:100 msgid "Source" msgstr "用户来源" -#: users/models/user.py:362 +#: users/models/user.py:363 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" @@ -3636,7 +3636,7 @@ msgid "Reset link will be generated and sent to the user. " msgstr "生成重置密码连接,通过邮件发送给用户" #: users/templates/users/user_detail.html:19 -#: users/templates/users/user_granted_asset.html:18 users/views/user.py:194 +#: users/templates/users/user_granted_asset.html:18 users/views/user.py:193 msgid "User detail" msgstr "用户详情" @@ -3764,20 +3764,20 @@ msgstr "用户组删除" msgid "UserGroup Deleting failed." msgstr "用户组删除失败" -#: users/templates/users/user_list.html:196 +#: users/templates/users/user_list.html:200 msgid "This will delete the selected users !!!" msgstr "删除选中用户 !!!" -#: users/templates/users/user_list.html:205 +#: users/templates/users/user_list.html:209 msgid "User Deleted." msgstr "已被删除" -#: users/templates/users/user_list.html:206 -#: users/templates/users/user_list.html:211 +#: users/templates/users/user_list.html:210 +#: users/templates/users/user_list.html:215 msgid "User Delete" msgstr "删除" -#: users/templates/users/user_list.html:210 +#: users/templates/users/user_list.html:214 msgid "User Deleting failed." msgstr "用户删除失败" @@ -3826,8 +3826,8 @@ msgstr "安装完成后点击下一步进入绑定页面(如已安装,直接 msgid "Administrator Settings force MFA login" msgstr "管理员设置强制使用MFA登录" -#: users/templates/users/user_profile.html:116 users/views/user.py:230 -#: users/views/user.py:284 +#: users/templates/users/user_profile.html:116 users/views/user.py:229 +#: users/views/user.py:283 msgid "User groups" msgstr "用户组" @@ -4003,22 +4003,18 @@ msgstr "" "
\n" " " -#: users/utils.py:162 +#: users/utils.py:148 msgid "User not exist" msgstr "用户不存在" -#: users/utils.py:164 +#: users/utils.py:150 msgid "Disabled or expired" msgstr "禁用或失效" -#: users/utils.py:177 +#: users/utils.py:163 msgid "Password or SSH public key invalid" msgstr "密码或密钥不合法" -#: users/utils.py:300 users/utils.py:310 -msgid "Bit" -msgstr " 位" - #: users/views/group.py:29 msgid "User group list" msgstr "用户组列表" @@ -4031,11 +4027,11 @@ msgstr "更新用户组" msgid "User group granted asset" msgstr "用户组授权资产" -#: users/views/login.py:70 +#: users/views/login.py:69 msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: users/views/login.py:180 users/views/user.py:526 users/views/user.py:551 +#: users/views/login.py:179 users/views/user.py:524 users/views/user.py:549 msgid "MFA code invalid, or ntp sync server time" msgstr "MFA验证码不正确,或者服务器端时间不对" @@ -4068,67 +4064,67 @@ msgstr "重置密码成功" msgid "Reset password success, return to login page" msgstr "重置密码成功,返回到登录页面" -#: users/views/login.py:277 users/views/login.py:290 +#: users/views/login.py:272 users/views/login.py:288 msgid "Token invalid or expired" msgstr "Token错误或失效" -#: users/views/login.py:286 +#: users/views/login.py:284 msgid "Password not same" msgstr "密码不一致" -#: users/views/login.py:296 users/views/user.py:127 users/views/user.py:422 +#: users/views/login.py:294 users/views/user.py:126 users/views/user.py:420 msgid "* Your password does not meet the requirements" msgstr "* 您的密码不符合要求" -#: users/views/login.py:334 +#: users/views/login.py:332 msgid "First login" msgstr "首次登陆" -#: users/views/user.py:144 +#: users/views/user.py:143 msgid "Bulk update user success" msgstr "批量更新用户成功" -#: users/views/user.py:174 +#: users/views/user.py:173 msgid "Bulk update user" msgstr "批量更新用户" -#: users/views/user.py:259 +#: users/views/user.py:258 msgid "Invalid file." msgstr "文件不合法" -#: users/views/user.py:355 +#: users/views/user.py:354 msgid "User granted assets" msgstr "用户授权资产" -#: users/views/user.py:386 +#: users/views/user.py:385 msgid "Profile setting" msgstr "个人信息设置" -#: users/views/user.py:405 +#: users/views/user.py:404 msgid "Password update" msgstr "密码更新" -#: users/views/user.py:440 +#: users/views/user.py:438 msgid "Public key update" msgstr "密钥更新" -#: users/views/user.py:481 +#: users/views/user.py:479 msgid "Password invalid" msgstr "用户名或密码无效" -#: users/views/user.py:581 +#: users/views/user.py:579 msgid "MFA enable success" msgstr "MFA 绑定成功" -#: users/views/user.py:582 +#: users/views/user.py:580 msgid "MFA enable success, return login page" msgstr "MFA 绑定成功,返回到登录页面" -#: users/views/user.py:584 +#: users/views/user.py:582 msgid "MFA disable success" msgstr "MFA 解绑成功" -#: users/views/user.py:585 +#: users/views/user.py:583 msgid "MFA disable success, return login page" msgstr "MFA 解绑成功,返回登录页面" @@ -4439,6 +4435,9 @@ msgstr "创建组织" msgid "Update org" msgstr "更新组织" +#~ msgid "Bit" +#~ msgstr " 位" + #, fuzzy #~| msgid "Delete succeed" #~ msgid "Delete success" diff --git a/apps/locale/zh/LC_MESSAGES/djangojs.mo b/apps/locale/zh/LC_MESSAGES/djangojs.mo index 35d79b28e..189ddbc8e 100644 Binary files a/apps/locale/zh/LC_MESSAGES/djangojs.mo and b/apps/locale/zh/LC_MESSAGES/djangojs.mo differ diff --git a/apps/locale/zh/LC_MESSAGES/djangojs.po b/apps/locale/zh/LC_MESSAGES/djangojs.po index 7fb11c852..720927f06 100644 --- a/apps/locale/zh/LC_MESSAGES/djangojs.po +++ b/apps/locale/zh/LC_MESSAGES/djangojs.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-08-08 14:48+0800\n" +"POT-Creation-Date: 2018-11-21 19:14+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,58 +17,58 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: static/js/jumpserver.js:158 +#: static/js/jumpserver.js:168 msgid "Update is successful!" msgstr "更新成功" -#: static/js/jumpserver.js:160 +#: static/js/jumpserver.js:170 msgid "An unknown error occurred while updating.." msgstr "更新时发生未知错误" -#: static/js/jumpserver.js:205 static/js/jumpserver.js:247 -#: static/js/jumpserver.js:252 +#: static/js/jumpserver.js:236 static/js/jumpserver.js:273 +#: static/js/jumpserver.js:276 msgid "Error" msgstr "错误" -#: static/js/jumpserver.js:205 +#: static/js/jumpserver.js:236 msgid "Being used by the asset, please unbind the asset first." msgstr "正在被资产使用中,请先解除资产绑定" -#: static/js/jumpserver.js:212 static/js/jumpserver.js:260 +#: static/js/jumpserver.js:242 static/js/jumpserver.js:283 msgid "Delete the success" msgstr "删除成功" -#: static/js/jumpserver.js:219 +#: static/js/jumpserver.js:248 msgid "Are you sure about deleting it?" msgstr "你确定删除吗 ?" -#: static/js/jumpserver.js:224 static/js/jumpserver.js:273 +#: static/js/jumpserver.js:252 static/js/jumpserver.js:293 msgid "Cancel" msgstr "取消" -#: static/js/jumpserver.js:227 static/js/jumpserver.js:276 +#: static/js/jumpserver.js:254 static/js/jumpserver.js:295 msgid "Confirm" msgstr "确认" -#: static/js/jumpserver.js:247 +#: static/js/jumpserver.js:273 msgid "" "The organization contains undeleted information. Please try again after " "deleting" msgstr "组织中包含未删除信息,请删除后重试" -#: static/js/jumpserver.js:252 +#: static/js/jumpserver.js:276 msgid "" "Do not perform this operation under this organization. Try again after " "switching to another organization" msgstr "请勿在此组织下执行此操作,切换到其他组织后重试" -#: static/js/jumpserver.js:267 +#: static/js/jumpserver.js:289 msgid "" "Please ensure that the following information in the organization has been " "deleted" msgstr "请确保组织内的以下信息已删除" -#: static/js/jumpserver.js:269 +#: static/js/jumpserver.js:290 msgid "" "User list、User group、Asset list、Domain list、Admin user、System user、" "Labels、Asset permission" @@ -76,32 +76,52 @@ msgstr "" "用户列表、用户组、资产列表、网域列表、管理用户、系统用户、标签管理、资产授权" "规则" -#: static/js/jumpserver.js:311 +#: static/js/jumpserver.js:329 msgid "Loading ..." msgstr "加载中 ..." -#: static/js/jumpserver.js:313 +#: static/js/jumpserver.js:330 msgid "Search" msgstr "搜索" -#: static/js/jumpserver.js:317 +#: static/js/jumpserver.js:333 #, javascript-format msgid "Selected item %d" msgstr "选中 %d 项" -#: static/js/jumpserver.js:322 +#: static/js/jumpserver.js:337 msgid "Per page _MENU_" msgstr "每页 _MENU_" -#: static/js/jumpserver.js:324 +#: static/js/jumpserver.js:338 msgid "" "Displays the results of items _START_ to _END_; A total of _TOTAL_ entries" msgstr "显示第 _START_ 至 _END_ 项结果; 总共 _TOTAL_ 项" -#: static/js/jumpserver.js:328 +#: static/js/jumpserver.js:341 msgid "No match" msgstr "没有匹配项" -#: static/js/jumpserver.js:330 +#: static/js/jumpserver.js:342 msgid "No record" msgstr "没有记录" + +#: static/js/jumpserver.js:701 +msgid "Password minimum length {N} bits" +msgstr "密码最小长度 {N} 位" + +#: static/js/jumpserver.js:702 +msgid "Must contain capital letters" +msgstr "必须包含大写字母" + +#: static/js/jumpserver.js:703 +msgid "Must contain lowercase letters" +msgstr "必须包含小写字母" + +#: static/js/jumpserver.js:704 +msgid "Must contain numeric characters" +msgstr "必须包含数字字符" + +#: static/js/jumpserver.js:705 +msgid "Must contain special characters" +msgstr "必须包含特殊字符" diff --git a/apps/perms/views.py b/apps/perms/views.py index 7782992d5..6b0a0d5d3 100644 --- a/apps/perms/views.py +++ b/apps/perms/views.py @@ -101,7 +101,7 @@ class AssetPermissionUserView(AdminUserRequiredMixin, ListView): template_name = 'perms/asset_permission_user.html' context_object_name = 'asset_permission' - paginate_by = settings.CONFIG.DISPLAY_PER_PAGE + paginate_by = settings.DISPLAY_PER_PAGE object = None def get(self, request, *args, **kwargs): @@ -133,7 +133,7 @@ class AssetPermissionAssetView(AdminUserRequiredMixin, ListView): template_name = 'perms/asset_permission_asset.html' context_object_name = 'asset_permission' - paginate_by = settings.CONFIG.DISPLAY_PER_PAGE + paginate_by = settings.DISPLAY_PER_PAGE object = None def get(self, request, *args, **kwargs): diff --git a/apps/static/js/jumpserver.js b/apps/static/js/jumpserver.js index a54313e97..466267c33 100644 --- a/apps/static/js/jumpserver.js +++ b/apps/static/js/jumpserver.js @@ -688,41 +688,69 @@ function setUrlParam(url, name, value) { return url } +// Password check rules +var rules_short_map_id = { + 'min': 'id_security_password_min_length', + 'upper': 'id_security_password_upper_case', + 'lower': 'id_security_password_lower_case', + 'number': 'id_security_password_number', + 'special': 'id_security_password_special_char' +}; + +var rules_id_map_label = { + 'id_security_password_min_length': gettext('Password minimum length {N} bits'), + 'id_security_password_upper_case': gettext('Must contain capital letters'), + 'id_security_password_lower_case': gettext('Must contain lowercase letters'), + 'id_security_password_number': gettext('Must contain numeric characters'), + 'id_security_password_special_char': gettext('Must contain special characters') +}; + +function getRuleLabel(rule){ + var label = ''; + if (rule.key === rules_short_map_id['min']){ + label = rules_id_map_label[rule.key].replace('{N}', rule.value) + } + else{ + label = rules_id_map_label[rule.key] + } + return label +} + // 校验密码-改变规则颜色 function checkPasswordRules(password, minLength) { if (wordMinLength(password, minLength)) { - $('#rule_SECURITY_PASSWORD_MIN_LENGTH').css('color', 'green') + $('#'+rules_short_map_id['min']).css('color', 'green') } else { - $('#rule_SECURITY_PASSWORD_MIN_LENGTH').css('color', '#908a8a') + $('#'+rules_short_map_id['min']).css('color', '#908a8a') } if (wordUpperCase(password)) { - $('#rule_SECURITY_PASSWORD_UPPER_CASE').css('color', 'green'); + $('#'+rules_short_map_id['upper']).css('color', 'green') } else { - $('#rule_SECURITY_PASSWORD_UPPER_CASE').css('color', '#908a8a') + $('#'+rules_short_map_id['upper']).css('color', '#908a8a') } if (wordLowerCase(password)) { - $('#rule_SECURITY_PASSWORD_LOWER_CASE').css('color', 'green') + $('#'+rules_short_map_id['lower']).css('color', 'green') } else { - $('#rule_SECURITY_PASSWORD_LOWER_CASE').css('color', '#908a8a') + $('#'+rules_short_map_id['lower']).css('color', '#908a8a') } if (wordNumber(password)) { - $('#rule_SECURITY_PASSWORD_NUMBER').css('color', 'green') + $('#'+rules_short_map_id['number']).css('color', 'green') } else { - $('#rule_SECURITY_PASSWORD_NUMBER').css('color', '#908a8a') + $('#'+rules_short_map_id['number']).css('color', '#908a8a') } if (wordSpecialChar(password)) { - $('#rule_SECURITY_PASSWORD_SPECIAL_CHAR').css('color', 'green') + $('#'+rules_short_map_id['special']).css('color', 'green') } else { - $('#rule_SECURITY_PASSWORD_SPECIAL_CHAR').css('color', '#908a8a') + $('#'+rules_short_map_id['special']).css('color', '#908a8a') } } @@ -749,11 +777,12 @@ function wordSpecialChar(word) { return word.match(/[`,~,!,@,#,\$,%,\^,&,\*,\(,\),\-,_,=,\+,\{,\},\[,\],\|,\\,;,',:,",\,,\.,<,>,\/,\?]+/) } + // 显示弹窗密码规则 function popoverPasswordRules(password_check_rules, $el) { var message = ""; - jQuery.each(password_check_rules, function (idx, rules) { - message += "
  • " + rules.label + "
  • "; + jQuery.each(password_check_rules, function (idx, rule) { + message += "
  • " + getRuleLabel(rule) + "
  • "; }); //$('#id_password_rules').html(message); $el.html(message) diff --git a/apps/terminal/models.py b/apps/terminal/models.py index 111ea2867..27d5af402 100644 --- a/apps/terminal/models.py +++ b/apps/terminal/models.py @@ -9,7 +9,6 @@ from django.conf import settings from users.models import User from orgs.mixins import OrgModelMixin -from common.models import common_settings from common.utils import get_command_storage_setting, get_replay_storage_setting from .backends.command.models import AbstractSessionCommand @@ -61,11 +60,11 @@ class Terminal(models.Model): configs = {} for k in dir(settings): if k.startswith('TERMINAL'): - configs[k] = getattr(common_settings, k) + configs[k] = getattr(settings, k) configs.update(self.get_common_storage()) configs.update(self.get_replay_storage()) configs.update({ - 'SECURITY_MAX_IDLE_TIME': common_settings.SECURITY_MAX_IDLE_TIME + 'SECURITY_MAX_IDLE_TIME': settings.SECURITY_MAX_IDLE_TIME }) return configs diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 8192ae095..b7a2f7e6a 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -14,7 +14,6 @@ from django.utils import timezone from django.shortcuts import reverse from common.utils import get_signer, date_expired_default -from common.models import common_settings from orgs.mixins import OrgManager from orgs.utils import current_org @@ -284,7 +283,7 @@ class User(AbstractUser): @property def otp_force_enabled(self): - if common_settings.SECURITY_MFA_AUTH: + if settings.SECURITY_MFA_AUTH: return True return self.otp_level == 2 diff --git a/apps/users/templates/users/reset_password.html b/apps/users/templates/users/reset_password.html index 1e533ba89..9ca385ce4 100644 --- a/apps/users/templates/users/reset_password.html +++ b/apps/users/templates/users/reset_password.html @@ -99,7 +99,7 @@ container = $('#container'), progress = $('#id_progress'), password_check_rules = {{ password_check_rules|safe }}, - minLength = {{ min_length }}, + minLength = 6, top = 146, left = 170, i18n_fallback = { "veryWeak": "{% trans 'Very weak' %}", @@ -110,6 +110,12 @@ "veryStrong": "{% trans 'Very strong' %}" }; + jQuery.each(password_check_rules, function (idx, rules) { + if(rules.key === 'id_security_password_min_length'){ + minLength = rules.value + } + }); + // 初始化popover initPopover(container, progress, idPassword, el, password_check_rules, i18n_fallback); diff --git a/apps/users/templates/users/user_password_update.html b/apps/users/templates/users/user_password_update.html index e028992e9..8056edb87 100644 --- a/apps/users/templates/users/user_password_update.html +++ b/apps/users/templates/users/user_password_update.html @@ -92,7 +92,7 @@ container = $('#container'), progress = $('#id_progress'), password_check_rules = {{ password_check_rules|safe }}, - minLength = {{ min_length }}, + minLength = 6, top = idPassword.offset().top - $('.navbar').outerHeight(true) - $('.page-heading').outerHeight(true) - 10 + 34, left = 377, i18n_fallback = { @@ -104,6 +104,12 @@ "veryStrong": "{% trans 'Very strong' %}" }; + jQuery.each(password_check_rules, function (idx, rules) { + if(rules.key === 'id_security_password_min_length'){ + minLength = rules.value + } + }); + // 初始化popover initPopover(container, progress, idPassword, el, password_check_rules, i18n_fallback); diff --git a/apps/users/templates/users/user_update.html b/apps/users/templates/users/user_update.html index 5026b090d..e991af83b 100644 --- a/apps/users/templates/users/user_update.html +++ b/apps/users/templates/users/user_update.html @@ -27,7 +27,7 @@ container = $('#container'), progress = $('#id_progress'), password_check_rules = {{ password_check_rules|safe }}, - minLength = {{ min_length }}, + minLength = 6, top = idPassword.offset().top - $('.navbar').outerHeight(true) - $('.page-heading').outerHeight(true) - 10 + 34, left = 377, i18n_fallback = { @@ -39,6 +39,12 @@ "veryStrong": "{% trans 'Very strong' %}" }; + jQuery.each(password_check_rules, function (idx, rules) { + if(rules.key === 'id_security_password_min_length'){ + minLength = rules.value + } + }); + // 初始化popover initPopover(container, progress, idPassword, el, password_check_rules, i18n_fallback); diff --git a/apps/users/utils.py b/apps/users/utils.py index 0098f1b99..75c1c9b55 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -19,8 +19,8 @@ from django.core.cache import cache from common.tasks import send_mail_async from common.utils import reverse, get_object_or_none -from common.models import common_settings, Setting from common.forms import SecuritySettingForm +from common.models import Setting from .models import User, LoginLog @@ -275,56 +275,27 @@ def check_otp_code(otp_secret_key, otp_code): def get_password_check_rules(): check_rules = [] - min_length = settings.DEFAULT_PASSWORD_MIN_LENGTH - min_name = 'SECURITY_PASSWORD_MIN_LENGTH' - base_filed = SecuritySettingForm.base_fields - password_setting = Setting.objects.filter(name__startswith='SECURITY_PASSWORD') - - if not password_setting: - # 用户还没有设置过密码校验规则 - label = base_filed.get(min_name).label - label += ' ' + str(min_length) + _('Bit') - id = 'rule_' + min_name - rules = {'id': id, 'label': label} - check_rules.append(rules) - - for setting in password_setting: - if setting.cleaned_value: - id = 'rule_' + setting.name - label = base_filed.get(setting.name).label - if setting.name == min_name: - label += str(setting.cleaned_value) + _('Bit') - min_length = setting.cleaned_value - rules = {'id': id, 'label': label} - check_rules.append(rules) - - return check_rules, min_length + for rule in settings.SECURITY_PASSWORD_RULES: + key = "id_{}".format(rule.lower()) + value = getattr(settings, rule) + if not value: + continue + check_rules.append({'key': key, 'value': int(value)}) + return check_rules def check_password_rules(password): - min_field_name = 'SECURITY_PASSWORD_MIN_LENGTH' - upper_field_name = 'SECURITY_PASSWORD_UPPER_CASE' - lower_field_name = 'SECURITY_PASSWORD_LOWER_CASE' - number_field_name = 'SECURITY_PASSWORD_NUMBER' - special_field_name = 'SECURITY_PASSWORD_SPECIAL_CHAR' - min_length = getattr(common_settings, min_field_name) - - password_setting = Setting.objects.filter(name__startswith='SECURITY_PASSWORD') - if not password_setting: - pattern = r"^.{" + str(min_length) + ",}$" - else: - pattern = r"^" - for setting in password_setting: - if setting.cleaned_value and setting.name == upper_field_name: - pattern += '(?=.*[A-Z])' - elif setting.cleaned_value and setting.name == lower_field_name: - pattern += '(?=.*[a-z])' - elif setting.cleaned_value and setting.name == number_field_name: - pattern += '(?=.*\d)' - elif setting.cleaned_value and setting.name == special_field_name: - pattern += '(?=.*[`~!@#\$%\^&\*\(\)-=_\+\[\]\{\}\|;:\'",\.<>\/\?])' - pattern += '[a-zA-Z\d`~!@#\$%\^&\*\(\)-=_\+\[\]\{\}\|;:\'",\.<>\/\?]' - + pattern = r"^" + if settings.SECURITY_PASSWORD_UPPER_CASE: + pattern += '(?=.*[A-Z])' + if settings.SECURITY_PASSWORD_LOWER_CASE: + pattern += '(?=.*[a-z])' + if settings.SECURITY_PASSWORD_NUMBER: + pattern += '(?=.*\d)' + if settings.SECURITY_PASSWORD_SPECIAL_CHAR: + pattern += '(?=.*[`~!@#\$%\^&\*\(\)-=_\+\[\]\{\}\|;:\'\",\.<>\/\?])' + pattern += '[a-zA-Z\d`~!@#\$%\^&\*\(\)-=_\+\[\]\{\}\|;:\'\",\.<>\/\?]' + pattern += '.{' + str(settings.SECURITY_PASSWORD_MIN_LENGTH-1) + ',}$' match_obj = re.match(pattern, password) return bool(match_obj) @@ -339,7 +310,7 @@ def increase_login_failed_count(username, ip): count = cache.get(key_limit) count = count + 1 if count else 1 - limit_time = common_settings.SECURITY_LOGIN_LIMIT_TIME + limit_time = settings.SECURITY_LOGIN_LIMIT_TIME cache.set(key_limit, count, int(limit_time)*60) @@ -355,8 +326,8 @@ def is_block_login(username, ip): key_block = key_prefix_block.format(username) count = cache.get(key_limit, 0) - limit_count = common_settings.SECURITY_LOGIN_LIMIT_COUNT - limit_time = common_settings.SECURITY_LOGIN_LIMIT_TIME + limit_count = settings.SECURITY_LOGIN_LIMIT_COUNT + limit_time = settings.SECURITY_LOGIN_LIMIT_TIME if count >= limit_count: cache.set(key_block, 1, int(limit_time)*60) diff --git a/apps/users/views/login.py b/apps/users/views/login.py index 971b1cf2d..3901696ab 100644 --- a/apps/users/views/login.py +++ b/apps/users/views/login.py @@ -17,11 +17,10 @@ from django.views.decorators.csrf import csrf_protect from django.views.decorators.debug import sensitive_post_parameters from django.views.generic.base import TemplateView from django.views.generic.edit import FormView -from formtools.wizard.views import SessionWizardView from django.conf import settings +from formtools.wizard.views import SessionWizardView from common.utils import get_object_or_none, get_request_ip -from common.models import common_settings from ..models import User, LoginLog from ..utils import send_reset_password_mail, check_otp_code, \ redirect_user_first_login_or_index, get_user_or_tmp_user, \ @@ -269,13 +268,11 @@ class UserResetPasswordView(TemplateView): def get(self, request, *args, **kwargs): token = request.GET.get('token') user = User.validate_reset_token(token) - - check_rules, min_length = get_password_check_rules() - password_rules = {'password_check_rules': check_rules, 'min_length': min_length} - kwargs.update(password_rules) - if not user: kwargs.update({'errors': _('Token invalid or expired')}) + else: + check_rules = get_password_check_rules() + kwargs.update({'password_check_rules': check_rules}) return super().get(request, *args, **kwargs) def post(self, request, *args, **kwargs): @@ -326,7 +323,7 @@ class UserFirstLoginView(LoginRequiredMixin, SessionWizardView): user.is_public_key_valid = True user.save() context = { - 'user_guide_url': common_settings.USER_GUIDE_URL + 'user_guide_url': settings.USER_GUIDE_URL } return render(self.request, 'users/first_login_done.html', context) diff --git a/apps/users/views/user.py b/apps/users/views/user.py index 43d951ec1..93dc94020 100644 --- a/apps/users/views/user.py +++ b/apps/users/views/user.py @@ -14,6 +14,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth import authenticate, login as auth_login from django.contrib.messages.views import SuccessMessageMixin from django.core.cache import cache +from django.conf import settings from django.http import HttpResponse, JsonResponse from django.shortcuts import redirect from django.urls import reverse_lazy, reverse @@ -33,7 +34,6 @@ from django.contrib.auth import logout as auth_logout from common.const import create_success_msg, update_success_msg from common.mixins import JSONResponseMixin from common.utils import get_logger, get_object_or_none, is_uuid, ssh_key_gen -from common.models import Setting, common_settings from common.permissions import AdminUserRequiredMixin from orgs.utils import current_org from .. import forms @@ -106,12 +106,11 @@ class UserUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView): success_message = update_success_msg def get_context_data(self, **kwargs): - check_rules, min_length = get_password_check_rules() + check_rules = get_password_check_rules() context = { 'app': _('Users'), 'action': _('Update user'), 'password_check_rules': check_rules, - 'min_length': min_length } kwargs.update(context) return super().get_context_data(**kwargs) @@ -362,7 +361,7 @@ class UserProfileView(LoginRequiredMixin, TemplateView): template_name = 'users/user_profile.html' def get_context_data(self, **kwargs): - mfa_setting = common_settings.SECURITY_MFA_AUTH + mfa_setting = settings.SECURITY_MFA_AUTH context = { 'action': _('Profile'), 'mfa_setting': mfa_setting if mfa_setting is not None else False, @@ -399,12 +398,11 @@ class UserPasswordUpdateView(LoginRequiredMixin, UpdateView): return self.request.user def get_context_data(self, **kwargs): - check_rules, min_length = get_password_check_rules() + check_rules = get_password_check_rules() context = { 'app': _('Users'), 'action': _('Password update'), 'password_check_rules': check_rules, - 'min_length': min_length, } kwargs.update(context) return super().get_context_data(**kwargs) diff --git a/config_example.py b/config_example.py index d0fc4bff9..28341d442 100644 --- a/config_example.py +++ b/config_example.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- """ jumpserver.config ~~~~~~~~~~~~~~~~~ @@ -13,48 +15,63 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__)) class Config: - # Use it to encrypt or decrypt data - # SECURITY WARNING: keep the secret key used in production secret! - SECRET_KEY = os.environ.get('SECRET_KEY') or '2vym+ky!997d5kkcc64mnz06y1mmui3lut#(^wd=%s_qj$1%x' + """ + Jumpserver Config File + Jumpserver 配置文件 - # Django security setting, if your disable debug model, you should setting that - ALLOWED_HOSTS = ['*'] + Jumpserver use this config for drive django framework running, + You can set is value or set the same envirment value, + Jumpserver look for config order: file => env => default + + Jumpserver使用配置来驱动Django框架的运行, + 你可以在该文件中设置,或者设置同样名称的环境变量, + Jumpserver使用配置的顺序: 文件 => 环境变量 => 默认值 + """ + # SECURITY WARNING: keep the secret key used in production secret! + # 加密秘钥 生产环境中请修改为随机字符串,请勿外泄 + SECRET_KEY = '2vym+ky!997d5kkcc64mnz06y1mmui3lut#(^wd=%s_qj$1%x' # Development env open this, when error occur display the full process track, Production disable it - DEBUG = os.environ.get("DEBUG") or True + # DEBUG 模式 开启DEBUG后遇到错误时可以看到更多日志 + # DEBUG = True # DEBUG, INFO, WARNING, ERROR, CRITICAL can set. See https://docs.djangoproject.com/en/1.10/topics/logging/ - LOG_LEVEL = os.environ.get("LOG_LEVEL") or 'DEBUG' - LOG_DIR = os.path.join(BASE_DIR, 'logs') + # 日志级别 + # LOG_LEVEL = 'DEBUG' + # LOG_DIR = os.path.join(BASE_DIR, 'logs') # Database setting, Support sqlite3, mysql, postgres .... + # 数据库设置 # See https://docs.djangoproject.com/en/1.10/ref/settings/#databases # SQLite setting: - DB_ENGINE = 'sqlite3' - DB_NAME = os.path.join(BASE_DIR, 'data', 'db.sqlite3') + # 使用单文件sqlite数据库 + # DB_ENGINE = 'sqlite3' + # DB_NAME = os.path.join(BASE_DIR, 'data', 'db.sqlite3') # MySQL or postgres setting like: - # DB_ENGINE = os.environ.get("DB_ENGINE") or 'mysql' - # DB_HOST = os.environ.get("DB_HOST") or '127.0.0.1' - # DB_PORT = os.environ.get("DB_PORT") or 3306 - # DB_USER = os.environ.get("DB_USER") or 'jumpserver' - # DB_PASSWORD = os.environ.get("DB_PASSWORD") or 'weakPassword' - # DB_NAME = os.environ.get("DB_NAME") or 'jumpserver' + # 使用Mysql作为数据库 + DB_ENGINE = 'mysql' + DB_HOST = '127.0.0.1' + DB_PORT = 3306 + DB_USER = 'jumpserver' + DB_PASSWORD = '' + DB_NAME = 'jumpserver' # When Django start it will bind this host and port # ./manage.py runserver 127.0.0.1:8080 + # 运行时绑定端口 HTTP_BIND_HOST = '0.0.0.0' HTTP_LISTEN_PORT = 8080 # Use Redis as broker for celery and web socket - REDIS_HOST = os.environ.get("REDIS_HOST") or '127.0.0.1' - REDIS_PORT = os.environ.get("REDIS_PORT") or 6379 - REDIS_PASSWORD = os.environ.get("REDIS_PASSWORD") or '' - REDIS_DB_CELERY = os.environ.get('REDIS_DB') or 3 - REDIS_DB_CACHE = os.environ.get('REDIS_DB') or 4 + # Redis配置 + REDIS_HOST = '127.0.0.1' + REDIS_PORT = 6379 + REDIS_PASSWORD = '' # Use OpenID authorization + # 使用OpenID 来进行认证设置 # BASE_SITE_URL = 'http://localhost:8080' # AUTH_OPENID = False # True or False # AUTH_OPENID_SERVER_URL = 'https://openid-auth-server.com/'