diff --git a/apps/authentication/backends/saml2/backends.py b/apps/authentication/backends/saml2/backends.py index 0cacdf920..8b7cfe3d0 100644 --- a/apps/authentication/backends/saml2/backends.py +++ b/apps/authentication/backends/saml2/backends.py @@ -17,8 +17,7 @@ logger = get_logger(__file__) class SAML2Backend(ModelBackend): - @staticmethod - def user_can_authenticate(user): + def user_can_authenticate(self, user): is_valid = getattr(user, 'is_valid', None) return is_valid or is_valid is None @@ -42,9 +41,10 @@ class SAML2Backend(ModelBackend): log_prompt = "Process authenticate [SAML2AuthCodeBackend]: {}" logger.debug(log_prompt.format('Start')) if saml_user_data is None: - logger.debug(log_prompt.format('saml_user_data is missing')) + logger.error(log_prompt.format('saml_user_data is missing')) return None + logger.debug(log_prompt.format('saml data, {}'.format(saml_user_data))) username = saml_user_data.get('username') if not username: logger.debug(log_prompt.format('username is missing')) diff --git a/apps/authentication/backends/saml2/views.py b/apps/authentication/backends/saml2/views.py index 26c88b5e2..9841fcd1d 100644 --- a/apps/authentication/backends/saml2/views.py +++ b/apps/authentication/backends/saml2/views.py @@ -1,5 +1,4 @@ -import json -import os +import copy from django.views import View from django.contrib import auth as auth @@ -40,18 +39,20 @@ class PrepareRequestMixin: idp_metadata_url = settings.SAML2_IDP_METADATA_URL logger.debug('Start getting IDP configuration') + xml_idp_settings = None try: - xml_idp_settings = IdPMetadataParse.parse(idp_metadata_xml) + if idp_metadata_xml.strip(): + xml_idp_settings = IdPMetadataParse.parse(idp_metadata_xml) except Exception as err: - xml_idp_settings = None logger.warning('Failed to get IDP metadata XML settings, error: %s', str(err)) + url_idp_settings = None try: - url_idp_settings = IdPMetadataParse.parse_remote( - idp_metadata_url, timeout=20 - ) + if idp_metadata_url.strip(): + url_idp_settings = IdPMetadataParse.parse_remote( + idp_metadata_url, timeout=20 + ) except Exception as err: - url_idp_settings = None logger.warning('Failed to get IDP metadata URL settings, error: %s', str(err)) idp_settings = url_idp_settings or xml_idp_settings @@ -92,14 +93,19 @@ class PrepareRequestMixin: @staticmethod def get_advanced_settings(): - other_settings = {} - other_settings_path = settings.SAML2_OTHER_SETTINGS_PATH - if os.path.exists(other_settings_path): - with open(other_settings_path, 'r') as json_data: - try: - other_settings = json.loads(json_data.read()) - except Exception as error: - logger.error('Get other settings error: %s', error) + try: + other_settings = dict(settings.SAML2_SP_ADVANCED_SETTINGS) + other_settings = copy.deepcopy(other_settings) + except Exception as error: + logger.error('Get other settings error: %s', error) + other_settings = {} + + security_default = { + 'wantAttributeStatement': False, + 'allowRepeatAttributeName': True + } + security = other_settings.get('security', {}) + security_default.update(security) default = { "organization": { @@ -108,9 +114,10 @@ class PrepareRequestMixin: "displayname": "JumpServer", "url": "https://jumpserver.org/" } - } + }, } default.update(other_settings) + default['security'] = security_default return default def get_sp_settings(self): @@ -157,9 +164,12 @@ class PrepareRequestMixin: user_attrs = {} real_key_index = len(settings.SITE_URL) + 1 attrs = saml_instance.get_attributes() + valid_attrs = ['username', 'name', 'email', 'comment', 'phone'] for attr, value in attrs.items(): attr = attr[real_key_index:] + if attr not in valid_attrs: + continue user_attrs[attr] = self.value_to_str(value) return user_attrs @@ -167,7 +177,7 @@ class PrepareRequestMixin: class Saml2AuthRequestView(View, PrepareRequestMixin): def get(self, request): - log_prompt = "Process GET requests [SAML2AuthRequestView]: {}" + log_prompt = "Process SAML GET requests: {}" logger.debug(log_prompt.format('Start')) try: @@ -186,12 +196,12 @@ class Saml2EndSessionView(View, PrepareRequestMixin): http_method_names = ['get', 'post', ] def get(self, request): - log_prompt = "Process GET requests [SAML2EndSessionView]: {}" + log_prompt = "Process SAML GET requests: {}" logger.debug(log_prompt.format('Start')) return self.post(request) def post(self, request): - log_prompt = "Process POST requests [SAML2EndSessionView]: {}" + log_prompt = "Process SAML POST requests: {}" logger.debug(log_prompt.format('Start')) logout_url = settings.LOGOUT_REDIRECT_URL or '/' @@ -212,7 +222,7 @@ class Saml2EndSessionView(View, PrepareRequestMixin): class Saml2AuthCallbackView(View, PrepareRequestMixin): def post(self, request): - log_prompt = "Process POST requests [SAML2AuthCallbackView]: {}" + log_prompt = "Process SAML2 POST requests: {}" post_data = request.POST try: @@ -227,24 +237,25 @@ class Saml2AuthCallbackView(View, PrepareRequestMixin): logger.debug(log_prompt.format('Process saml response')) saml_instance.process_response(request_id=request_id) - errors = saml_instance.get_errors() + errors = saml_instance.get_last_error_reason() - if not errors: - if 'AuthNRequestID' in request.session: - del request.session['AuthNRequestID'] + if errors: + logger.error(log_prompt.format('Saml response has error: %s' % str(errors))) + return HttpResponseRedirect(settings.AUTH_SAML2_AUTHENTICATION_FAILURE_REDIRECT_URI) - logger.debug(log_prompt.format('Process authenticate')) - saml_user_data = self.get_attributes(saml_instance) - user = auth.authenticate(request=request, saml_user_data=saml_user_data) - if user and user.is_valid: - logger.debug(log_prompt.format('Login: {}'.format(user))) - auth.login(self.request, user) + if 'AuthNRequestID' in request.session: + del request.session['AuthNRequestID'] - logger.debug(log_prompt.format('Redirect')) - next_url = saml_instance.redirect_to(post_data.get('RelayState', '/')) - return HttpResponseRedirect(next_url) - logger.error(log_prompt.format('Saml response has error: %s' % str(errors))) - return HttpResponseRedirect(settings.AUTH_SAML2_AUTHENTICATION_FAILURE_REDIRECT_URI) + logger.debug(log_prompt.format('Process authenticate')) + saml_user_data = self.get_attributes(saml_instance) + user = auth.authenticate(request=request, saml_user_data=saml_user_data) + if user and user.is_valid: + logger.debug(log_prompt.format('Login: {}'.format(user))) + auth.login(self.request, user) + + logger.debug(log_prompt.format('Redirect')) + next_url = saml_instance.redirect_to(post_data.get('RelayState', '/')) + return HttpResponseRedirect(next_url) @csrf_exempt def dispatch(self, *args, **kwargs): diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index ecccf2375..ce7cd24f9 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -46,57 +46,35 @@ class UserLoginView(mixins.AuthMixin, FormView): # show jumpserver login page if request http://{JUMP-SERVER}/?admin=1 if self.request.GET.get("admin", 0): return None + + auth_types = [m for m in self.get_support_auth_methods() if m.get('auto_redirect')] + if not auth_types: + return None + + # 明确直接登录哪个 + login_to = settings.LOGIN_REDIRECT_TO_BACKEND.upper() + if login_to == 'DIRECT': + return None + + auth_method = next(filter(lambda x: x['name'] == login_to, auth_types), None) + if not auth_method: + auth_method = auth_types[0] + + auth_name, redirect_url = auth_method['name'], auth_method['url'] next_url = request.GET.get('next') or '/' - auth_type = '' - if settings.AUTH_OPENID: - auth_type = 'OIDC' - openid_auth_url = reverse(settings.AUTH_OPENID_AUTH_LOGIN_URL_NAME) - openid_auth_url = openid_auth_url + f'?next={next_url}' - else: - openid_auth_url = None + query_string = request.GET.urlencode() + redirect_url = '{}?next={}&{}'.format(redirect_url, next_url, query_string) - if settings.AUTH_CAS: - auth_type = 'CAS' - cas_auth_url = reverse(settings.CAS_LOGIN_URL_NAME) + f'?next={next_url}' - else: - cas_auth_url = None - - if settings.AUTH_SAML2: - auth_type = 'saml2' - saml2_auth_url = reverse(settings.SAML2_LOGIN_URL_NAME) + f'?next={next_url}' - else: - saml2_auth_url = None - - if not any([openid_auth_url, cas_auth_url, saml2_auth_url]): - return None - - login_redirect = settings.LOGIN_REDIRECT_TO_BACKEND.lower() - if login_redirect in ['direct']: - return None - if login_redirect in ['cas'] and cas_auth_url: - auth_url = cas_auth_url - elif login_redirect in ['openid', 'oidc'] and openid_auth_url: - auth_url = openid_auth_url - elif login_redirect in ['saml2'] and saml2_auth_url: - auth_url = saml2_auth_url - else: - auth_url = openid_auth_url or cas_auth_url or saml2_auth_url - - if settings.LOGIN_REDIRECT_TO_BACKEND or not settings.LOGIN_REDIRECT_MSG_ENABLED: - redirect_url = auth_url - else: + if settings.LOGIN_REDIRECT_MSG_ENABLED: message_data = { 'title': _('Redirecting'), - 'message': _("Redirecting to {} authentication").format(auth_type), - 'redirect_url': auth_url, + 'message': _("Redirecting to {} authentication").format(auth_name), + 'redirect_url': redirect_url, 'interval': 3, 'has_cancel': True, 'cancel_url': reverse('authentication:login') + '?admin=1' } redirect_url = FlashMessageUtil.gen_message_url(message_data) - - query_string = request.GET.urlencode() - redirect_url = "{}&{}".format(redirect_url, query_string) return redirect_url def get(self, request, *args, **kwargs): @@ -165,25 +143,28 @@ class UserLoginView(mixins.AuthMixin, FormView): 'name': 'OpenID', 'enabled': settings.AUTH_OPENID, 'url': reverse('authentication:openid:login'), - 'logo': static('img/login_oidc_logo.png') + 'logo': static('img/login_oidc_logo.png'), + 'auto_redirect': True # 是否支持自动重定向 }, { 'name': 'CAS', 'enabled': settings.AUTH_CAS, 'url': reverse('authentication:cas:cas-login'), - 'logo': static('img/login_cas_logo.png') + 'logo': static('img/login_cas_logo.png'), + 'auto_redirect': True }, { 'name': 'SAML2', 'enabled': settings.AUTH_SAML2, 'url': reverse('authentication:saml2:saml2-login'), - 'logo': static('img/login_cas_logo.png') + 'logo': static('img/login_cas_logo.png'), + 'auto_redirect': True }, { 'name': _('WeCom'), 'enabled': settings.AUTH_WECOM, 'url': reverse('authentication:wecom-qr-login'), - 'logo': static('img/login_wecom_logo.png') + 'logo': static('img/login_wecom_logo.png'), }, { 'name': _('DingTalk'), diff --git a/apps/common/sdk/im/utils.py b/apps/common/sdk/im/utils.py index 2386d0eff..86f41cdc3 100644 --- a/apps/common/sdk/im/utils.py +++ b/apps/common/sdk/im/utils.py @@ -29,7 +29,7 @@ def set_default(data: dict, default: dict): class DictWrapper: - def __init__(self, data:dict): + def __init__(self, data: dict): self.raw_data = data def __getitem__(self, item): @@ -51,7 +51,7 @@ class DictWrapper: return str(self.raw_data) def __repr__(self): - return str(self.raw_data) + return repr(self.raw_data) def as_request(func): diff --git a/apps/common/sdk/im/wecom/__init__.py b/apps/common/sdk/im/wecom/__init__.py index 7640228e9..6d7c2bf66 100644 --- a/apps/common/sdk/im/wecom/__init__.py +++ b/apps/common/sdk/im/wecom/__init__.py @@ -124,6 +124,9 @@ class WeCom(RequestMixin): return users self._requests.check_errcode_is_0(data) + if 'invaliduser' not in data: + return () + invaliduser = data['invaliduser'] if not invaliduser: return () diff --git a/apps/common/utils/common.py b/apps/common/utils/common.py index fb50009b2..e5b9de4fd 100644 --- a/apps/common/utils/common.py +++ b/apps/common/utils/common.py @@ -281,7 +281,6 @@ def get_docker_mem_usage_if_limit(): return ((usage_in_bytes - inactive_file) / limit_in_bytes) * 100 except Exception as e: - logger.debug(f'Get memory usage by docker limit: {e}') return None diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 9bffc38fa..654164fe9 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -234,7 +234,18 @@ class Config(dict): 'SAML2_LOGOUT_COMPLETELY': True, 'AUTH_SAML2_ALWAYS_UPDATE_USER': True, 'SAML2_RENAME_ATTRIBUTES': {'uid': 'username', 'email': 'email'}, - 'SAML2_OTHER_SETTINGS_PATH': '', + 'SAML2_SP_ADVANCED_SETTINGS': { + "organization": { + "en": { + "name": "JumpServer", + "displayname": "JumpServer", + "url": "https://jumpserver.org/" + } + }, + "strict": True, + "security": { + } + }, 'SAML2_IDP_METADATA_URL': '', 'SAML2_IDP_METADATA_XML': '', 'SAML2_SP_KEY_CONTENT': '', diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index 42ba9958a..883f63d07 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -129,7 +129,7 @@ AUTH_SAML2_AUTHENTICATION_FAILURE_REDIRECT_URI = CONFIG.AUTH_SAML2_AUTHENTICATIO AUTH_SAML2_ALWAYS_UPDATE_USER = CONFIG.AUTH_SAML2_ALWAYS_UPDATE_USER SAML2_LOGOUT_COMPLETELY = CONFIG.SAML2_LOGOUT_COMPLETELY SAML2_RENAME_ATTRIBUTES = CONFIG.SAML2_RENAME_ATTRIBUTES -SAML2_OTHER_SETTINGS_PATH = CONFIG.SAML2_OTHER_SETTINGS_PATH +SAML2_SP_ADVANCED_SETTINGS = CONFIG.SAML2_SP_ADVANCED_SETTINGS SAML2_LOGIN_URL_NAME = "authentication:saml2:saml2-login" SAML2_LOGOUT_URL_NAME = "authentication:saml2:saml2-logout" diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index a592d4b78..32096ba67 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -2857,7 +2857,7 @@ msgstr "服务端地址" #: settings/serializers/auth/cas.py:13 msgid "Proxy server url" -msgstr "代理服务地址" +msgstr "回调地址" #: settings/serializers/auth/cas.py:14 settings/serializers/auth/saml2.py:29 msgid "Logout completely" diff --git a/apps/orgs/caches.py b/apps/orgs/caches.py index 3d4236b1c..ae1a610ce 100644 --- a/apps/orgs/caches.py +++ b/apps/orgs/caches.py @@ -72,7 +72,7 @@ class OrgResourceStatisticsCache(OrgRelatedCache): self.org = org def get_key_suffix(self): - return f'' + return f'org_{self.org.id}' def get_current_org(self): return self.org diff --git a/apps/settings/serializers/auth/saml2.py b/apps/settings/serializers/auth/saml2.py index 724bcf17a..ca102bf0e 100644 --- a/apps/settings/serializers/auth/saml2.py +++ b/apps/settings/serializers/auth/saml2.py @@ -17,6 +17,9 @@ class SAML2SettingSerializer(serializers.Serializer): SAML2_IDP_METADATA_XML = serializers.CharField( allow_blank=True, required=False, label=_('IDP Metadata XML') ) + SAML2_SP_ADVANCED_SETTINGS = serializers.JSONField( + required=False, label=_('SP ADVANCED SETTINGS') + ) SAML2_SP_KEY_CONTENT = serializers.CharField( allow_blank=True, required=False, write_only=True, label=_('SP Private Key') diff --git a/apps/terminal/api/command.py b/apps/terminal/api/command.py index 9e3065e99..8b66652f1 100644 --- a/apps/terminal/api/command.py +++ b/apps/terminal/api/command.py @@ -121,8 +121,11 @@ class CommandViewSet(JMSBulkModelViewSet): qs = storage.get_command_queryset() commands = self.filter_queryset(qs) merged_commands.extend(commands[:]) # ES 默认只取 10 条数据 - - merged_commands.sort(key=lambda command: command.timestamp, reverse=True) + order = self.request.query_params.get('order', None) + if order == 'timestamp': + merged_commands.sort(key=lambda command: command.timestamp) + else: + merged_commands.sort(key=lambda command: command.timestamp, reverse=True) page = self.paginate_queryset(merged_commands) if page is not None: serializer = self.get_serializer(page, many=True) diff --git a/apps/tickets/models/flow.py b/apps/tickets/models/flow.py index 9542c4f2c..6add3696e 100644 --- a/apps/tickets/models/flow.py +++ b/apps/tickets/models/flow.py @@ -7,7 +7,7 @@ from common.mixins.models import CommonModelMixin from common.db.encoder import ModelJSONFieldEncoder from orgs.mixins.models import OrgModelMixin from orgs.models import Organization -from orgs.utils import tmp_to_root_org +from orgs.utils import tmp_to_root_org, tmp_to_org from ..const import TicketType, TicketApprovalLevel, TicketApprovalStrategy from ..signals import post_or_update_change_ticket_flow_approval @@ -64,9 +64,13 @@ class TicketFlow(CommonModelMixin, OrgModelMixin): return '{}'.format(self.type) @classmethod - def get_org_related_flows(cls): - flows = cls.objects.all() + def get_org_related_flows(cls, org_id=None): + if org_id: + with tmp_to_org(org_id): + flows = cls.objects.all() + else: + flows = cls.objects.all() cur_flow_types = flows.values_list('type', flat=True) with tmp_to_root_org(): - diff_global_flows = cls.objects.exclude(type__in=cur_flow_types).filter(org_id=Organization.ROOT_ID) + diff_global_flows = cls.objects.exclude(type__in=cur_flow_types) return flows | diff_global_flows diff --git a/apps/tickets/serializers/ticket/ticket.py b/apps/tickets/serializers/ticket/ticket.py index 636f06f4a..276da4267 100644 --- a/apps/tickets/serializers/ticket/ticket.py +++ b/apps/tickets/serializers/ticket/ticket.py @@ -108,7 +108,8 @@ class TicketApplySerializer(TicketSerializer): def validate(self, attrs): ticket_type = attrs.get('type') - flow = TicketFlow.get_org_related_flows().filter(type=ticket_type).first() + org_id = attrs.get('org_id') + flow = TicketFlow.get_org_related_flows(org_id=org_id).filter(type=ticket_type).first() if flow: attrs['flow'] = flow else: diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 64144f9aa..ce709c936 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -62,7 +62,7 @@ pytz==2018.3 PyYAML==5.4 redis==3.5.3 requests==2.25.1 -jms-storage==0.0.39 +jms-storage==0.0.40 s3transfer==0.5.0 simplejson==3.13.2 six==1.11.0