diff --git a/apps/authentication/templates/authentication/auth_fail_flash_message_standalone.html b/apps/authentication/templates/authentication/auth_fail_flash_message_standalone.html index 38426bde7..1ac57cd92 100644 --- a/apps/authentication/templates/authentication/auth_fail_flash_message_standalone.html +++ b/apps/authentication/templates/authentication/auth_fail_flash_message_standalone.html @@ -48,7 +48,7 @@ var time = '{{ interval }}' var error = '{{ error }}' var auto_redirect = '{{ auto_redirect }}' - + if (error) { message = error } else { @@ -62,7 +62,7 @@ time--; setTimeout(redirect_page, 1000); } else { - window.location.href = "{{ redirect_url }}"; + window.location.href = '{{ redirect_url|escapejs }}'; } } @@ -71,4 +71,3 @@ } {% endblock %} - diff --git a/apps/authentication/views/dingtalk.py b/apps/authentication/views/dingtalk.py index 03a6e8eeb..ed1d50c9a 100644 --- a/apps/authentication/views/dingtalk.py +++ b/apps/authentication/views/dingtalk.py @@ -10,16 +10,16 @@ from django.views import View from rest_framework.exceptions import APIException from rest_framework.permissions import AllowAny, IsAuthenticated -from authentication.decorators import post_save_next_to_session_if_guard_redirect, pre_save_next_to_session from authentication import errors from authentication.const import ConfirmType +from authentication.decorators import post_save_next_to_session_if_guard_redirect, pre_save_next_to_session from authentication.mixins import AuthMixin from authentication.notifications import OAuthBindMessage from authentication.permissions import UserConfirmation from common.sdk.im.dingtalk import URL, DingTalk from common.utils import get_logger from common.utils.common import get_request_ip -from common.utils.django import get_object_or_none, reverse, safe_next_url +from common.utils.django import get_object_or_none, reverse from common.utils.random import random_string from common.views.mixins import PermissionsMixin, UserConfirmRequiredExceptionMixin from users.models import User @@ -237,7 +237,7 @@ class DingTalkOAuthLoginCallbackView(AuthMixin, DingTalkOAuthMixin, View): appsecret=settings.DINGTALK_APPSECRET, agentid=settings.DINGTALK_AGENTID ) - userid, __ = dingtalk.get_user_id_by_code(code) + userid, __ = dingtalk.get_user_id_by_code_for_oauth(code) if not userid: # 正常流程不会出这个错误,hack 行为 msg = _('Failed to get user from DingTalk') diff --git a/apps/common/sdk/im/dingtalk/__init__.py b/apps/common/sdk/im/dingtalk/__init__.py index 2740b456a..cb6075749 100644 --- a/apps/common/sdk/im/dingtalk/__init__.py +++ b/apps/common/sdk/im/dingtalk/__init__.py @@ -1,4 +1,5 @@ import base64 +import hashlib import hmac import time @@ -16,7 +17,7 @@ def sign(secret, data): digest = hmac.HMAC( key=secret.encode('utf8'), msg=data.encode('utf8'), - digestmod=hmac._hashlib.sha256 + digestmod=hashlib.sha256 ).digest() signature = base64.standard_b64encode(digest).decode('utf8') # signature = urllib.parse.quote(signature, safe='') @@ -33,6 +34,7 @@ class URL: OAUTH_CONNECT = 'https://oapi.dingtalk.com/connect/oauth2/sns_authorize' GET_USER_ACCESSTOKEN = 'https://api.dingtalk.com/v1.0/oauth2/userAccessToken' GET_USER_INFO = 'https://api.dingtalk.com/v1.0/contact/users/me' + GET_USERINFO_BYCODE = "https://oapi.dingtalk.com/sns/getuserinfo_bycode" GET_TOKEN = 'https://oapi.dingtalk.com/gettoken' SEND_MESSAGE_BY_TEMPLATE = 'https://oapi.dingtalk.com/topapi/message/corpconversation/sendbytemplate' SEND_MESSAGE = 'https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2' @@ -145,6 +147,14 @@ class DingTalk: headers={'x-acs-dingtalk-access-token': token}, check_errcode_is_0=False) return user + def get_user_id_by_code_for_oauth(self, code): + # https://open.dingtalk.com/document/orgapp/use-a-dingtalk-account-to-log-on-to-a-third-party + user = self._request.post(URL.GET_USERINFO_BYCODE, json={"tmp_auth_code": code}, + check_errcode_is_0=False, with_sign=True) + unionid = user["user_info"]['unionid'] + userid = self.get_userid_by_unionid(unionid) + return userid, None + def get_user_id_by_code(self, code): user_info = self.get_userinfo_bycode(code) unionid = user_info['unionId'] diff --git a/apps/jumpserver/context_processor.py b/apps/jumpserver/context_processor.py index a5731bcb3..6b03efc3a 100644 --- a/apps/jumpserver/context_processor.py +++ b/apps/jumpserver/context_processor.py @@ -2,30 +2,24 @@ # import datetime import os - from django.conf import settings from django.templatetags.static import static from django.utils.translation import gettext_lazy as _ -from jumpserver.vendor import get_vendor_value, is_default_vendor - default_interface = dict(( - ('logo_logout', get_vendor_value('logo_logout')), - ('logo_index', get_vendor_value('logo_index')), - ('login_image', get_vendor_value('login_image')), - ('favicon', get_vendor_value('favicon')), - ('login_title', get_vendor_value('login_title', default=_('JumpServer - An open-source PAM'))), - ('theme', get_vendor_value('theme', default='classic_green')), + ('logo_logout', static('img/logo.png')), + ('logo_index', static('img/logo_text_white.png')), + ('login_image', static('img/login_image.png')), + ('logo_white', static('img/logo_white.png')), + ('logo_text_white', static('img/logo_text_white.png')), + ('favicon', static('img/facio.ico')), + ('login_title', _('JumpServer - An open-source PAM')), + ('theme', 'classic_green'), ('theme_info', {}), - ('footer_content', get_vendor_value('footer_content', default='')), - ('ext', get_vendor_value('interface_ext_defaults', default={})), - ('vendor', settings.VENDOR), + ('footer_content', ''), ('version', os.environ.get("CURRENT_VERSION", "")) )) -if not is_default_vendor(): - default_interface['theme_info'] = get_vendor_value('theme_info', default={}) - current_year = datetime.datetime.now().year default_context = { @@ -49,4 +43,4 @@ def jumpserver_processor(request): 'FORCE_SCRIPT_NAME': settings.FORCE_SCRIPT_NAME, 'SECURITY_VIEW_AUTH_NEED_MFA': settings.SECURITY_VIEW_AUTH_NEED_MFA, }) - return context + return context \ No newline at end of file diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index b0d3283f0..b84b6bd9f 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -280,9 +280,6 @@ SUGGESTION_LIMIT = CONFIG.SUGGESTION_LIMIT MCP_ENABLED = CONFIG.MCP_ENABLED VENDOR = CONFIG.VENDOR -VENDOR_TEMPLATES_DIR = Path(STATIC_DIR) / VENDOR.lower() -if Path(VENDOR_TEMPLATES_DIR).is_dir(): - TEMPLATES[0]['DIRS'].insert(0, VENDOR_TEMPLATES_DIR) # Trusted IP TRUSTED_IP_VERIFY_ENABLED = CONFIG.TRUSTED_IP_VERIFY_ENABLED diff --git a/apps/jumpserver/vendor.py b/apps/jumpserver/vendor.py deleted file mode 100644 index d877f2291..000000000 --- a/apps/jumpserver/vendor.py +++ /dev/null @@ -1,133 +0,0 @@ -import json -from pathlib import Path - -from django.conf import settings -from django.contrib.staticfiles import finders -from django.templatetags.static import static -from django.utils.translation import get_language - -DEFAULT_VENDOR = "jumpserver" -DEFAULT_LOGIN_TEMPLATE = "authentication/login.html" -DEFAULT_THEME = "classic_green" - -VENDOR_THEMES_DIR = settings.VENDOR_TEMPLATES_DIR / "themes" - -VENDOR_LOWER = settings.VENDOR.lower() - - -def is_default_vendor() -> bool: - return VENDOR_LOWER == DEFAULT_VENDOR - - -def find_theme_path(theme_dirs, theme_name: str) -> Path | None: - filename = f"{theme_name}.json" - for d in theme_dirs: - p = d / filename - if p.is_file(): - return p - - -def _default_theme_dir() -> Path: - data_dir = Path(settings.BASE_DIR) - return data_dir / "xpack" / "plugins" / "interface" / "themes" - - -def _build_theme() -> str: - return DEFAULT_THEME if is_default_vendor() else VENDOR_LOWER - - -def _build_theme_info() -> dict: - default_theme_path = find_theme_path([_default_theme_dir()], DEFAULT_THEME) - - search_dirs = [_default_theme_dir()] if is_default_vendor() else [ - settings.VENDOR_TEMPLATES_DIR / 'themes', - _default_theme_dir(), - ] - theme_name = DEFAULT_THEME if is_default_vendor() else VENDOR_LOWER - - theme_path = find_theme_path(search_dirs, theme_name) or default_theme_path - if not theme_path: - return {} - return json.loads(theme_path.read_text(encoding="utf-8")) - - -def _build_vendor_info() -> dict: - if is_default_vendor(): - return {} - info_path = settings.VENDOR_TEMPLATES_DIR / "info.json" - if not info_path.exists(): - return {} - return json.loads(info_path.read_text(encoding="utf-8")) - - -def _build_vendor_info_value(key: str, default=None): - info = _build_vendor_info() - return info.get(key, default) - - -def _build_vendor_interface_ext() -> dict: - value = _build_vendor_info_value("interface_ext", default={}) - return value if isinstance(value, dict) else {} - -def _build_vendor_translate_ext() -> dict: - value = _build_vendor_info_value("translate", default={}) - return value if isinstance(value, dict) else {} - - -def _build_vendor_interface_ext_defaults() -> dict: - defaults = {} - translate = get_vendor_value('translate', default={}) - for field_name, field_info in _build_vendor_interface_ext().items(): - if "default" in field_info: - default_value = field_info["default"] - if field_info['type'] == "image": - defaults[field_name] = _build_asset(default_value) - else: - defaults[field_name] = translate.get(get_language(), {}).get(default_value, default_value) - return defaults - - -def _build_asset(filename: str) -> str: - if is_default_vendor(): - return static(filename) - - vendor_path = f"{VENDOR_LOWER}/{filename}" - if finders.find(vendor_path): - return static(vendor_path) - return static(filename) - - -def _build_optional_asset(filename: str, default=None): - if not is_default_vendor(): - vendor_path = f"{VENDOR_LOWER}/{filename}" - if finders.find(vendor_path): - return static(vendor_path) - - if finders.find(filename): - return static(filename) - return default - - -VENDOR_BUILDERS = { - "theme": _build_theme, - "theme_info": _build_theme_info, - "logo_logout": lambda: _build_asset("img/logo.png"), - "logo_index": lambda: _build_asset("img/logo_text_white.png"), - "login_image": lambda: _build_asset("img/login_image.png"), - "favicon": lambda: _build_asset("img/facio.ico"), - "logo_white": lambda: _build_asset("img/logo_white.png"), - "logo_text_white": lambda: _build_asset("img/logo_text_white.png"), - "login_title": lambda: _build_vendor_info_value("login_title"), - "footer_content": lambda: _build_vendor_info_value("footer_content"), - "interface_ext": _build_vendor_interface_ext, - "translate": _build_vendor_translate_ext, - "interface_ext_defaults": _build_vendor_interface_ext_defaults, -} - - -def get_vendor_value(kind: str, default=None): - builder = VENDOR_BUILDERS.get(kind) - if not builder: - return default - value = builder() - return default if value is None else value diff --git a/apps/jumpserver/views/schema.py b/apps/jumpserver/views/schema.py index 8e1c26907..e193376a2 100644 --- a/apps/jumpserver/views/schema.py +++ b/apps/jumpserver/views/schema.py @@ -1,5 +1,7 @@ import re +from django.apps import apps +from django.conf import settings from drf_spectacular.openapi import AutoSchema from drf_spectacular.generators import SchemaGenerator @@ -70,6 +72,20 @@ class CustomAutoSchema(AutoSchema): return '_'.join(tokenized_path + [action]) + def get_description(self): + description = super().get_description() + base_dir = str(settings.BASE_DIR) + my_apps = [ + app.label for app in apps.get_app_configs() + if app.module.__file__ and app.module.__file__.startswith(base_dir) + ] + view_app = str(self.view.__class__.__module__.split('.')[0]) + if view_app in my_apps: + # 内部 app 的 view 注释不展示在文档里 + return '' + else: + return description + def get_filter_parameters(self): if not self.should_filter(): return [] diff --git a/apps/notifications/site_msg.py b/apps/notifications/site_msg.py index 8a489f008..4318b24cc 100644 --- a/apps/notifications/site_msg.py +++ b/apps/notifications/site_msg.py @@ -1,9 +1,11 @@ +from datetime import timedelta + from django.db import transaction from common.utils import get_logger from common.utils.timezone import local_now from users.models import User -from .models import MessageContent as SiteMessageModel, SiteMessage +from .models import MessageContent, SiteMessage logger = get_logger(__file__) @@ -12,19 +14,22 @@ class SiteMessageUtil: @classmethod def send_msg(cls, subject, message, user_ids=(), group_ids=(), - sender=None, is_broadcast=False, display_mode=SiteMessageModel.DisplayMode.default): + sender=None, is_broadcast=False, display_mode=MessageContent.DisplayMode.default): if not any((user_ids, group_ids, is_broadcast)): raise ValueError('No recipient is specified') with transaction.atomic(): - site_msg = SiteMessageModel( + site_msg = MessageContent( subject=subject, message=message, is_broadcast=is_broadcast, sender=sender, display_mode=display_mode ) if is_broadcast: - user_ids = User.objects.all().values_list('id', flat=True) + # 广播消息时只为在线用户创建,未登录的用户不创建消息, + # 等用户登录后在 SiteMessage.create_site_msg_for_user_if_need 按需创建 + from audits.models import UserSession + user_ids = UserSession.objects.all().values_list('user_id', flat=True).distinct() elif group_ids: site_msg.groups.add(*group_ids) @@ -66,10 +71,32 @@ class SiteMessageUtil: @classmethod def get_user_display_msgs(cls, user_id): + # 获取用户未读的且需要展示的消息 msgs = SiteMessage.objects.filter(user_id=user_id, has_read=False).exclude( - content__display_mode=SiteMessageModel.DisplayMode.default + content__display_mode=MessageContent.DisplayMode.default ).prefetch_related('content') return msgs + + @classmethod + def create_site_msgs_for_user_if_need(cls, user_id): + ''' + 创建用户未读的且需要展示的消息 + 广播消息时只为在线用户创建,未登录的用户不创建消息,等用户登录后在这里按需创建 + 只创建用户没有的、最近24小时内的、需要广播的消息 + ''' + contents = MessageContent.objects.filter( + is_broadcast=True, + date_created__gt=local_now() - timedelta(hours=24) + ).exclude(display_mode=MessageContent.DisplayMode.default) + content_ids = set(contents.values_list('id', flat=True)) + has_content_ids = SiteMessage.objects.filter( + user_id=user_id, content_id__in=content_ids + ).values_list('content_id', flat=True) + to_create_content_ids = set(content_ids) - set(has_content_ids) + site_msgs = [ + SiteMessage(user_id=user_id, content_id=cid) for cid in to_create_content_ids + ] + SiteMessage.objects.bulk_create(site_msgs) @classmethod def mark_msgs_as_read(cls, user_id, msg_ids=None): diff --git a/apps/notifications/ws.py b/apps/notifications/ws.py index dbe0db85b..3d55f7cb5 100644 --- a/apps/notifications/ws.py +++ b/apps/notifications/ws.py @@ -66,6 +66,7 @@ class SiteMsgWebsocket(JsonWebsocketConsumer): # 先发一个消息再说 with safe_db_connection(): + SiteMessageUtil.create_site_msgs_for_user_if_need(user_id) self.send_site_msg() def handle_new_site_msg_recv(msg): diff --git a/apps/settings/serializers/public.py b/apps/settings/serializers/public.py index e5e6cedc1..707d7ecf8 100644 --- a/apps/settings/serializers/public.py +++ b/apps/settings/serializers/public.py @@ -90,6 +90,7 @@ class PrivateSettingSerializer(PublicSettingSerializer): JDMC_ENABLED = serializers.BooleanField() FLOWER_ENABLED = serializers.BooleanField() REMOTE_APP_STORE_URL = serializers.CharField() + VENDOR = serializers.CharField() class ServerInfoSerializer(serializers.Serializer): diff --git a/apps/settings/utils/common.py b/apps/settings/utils/common.py index a7c250ed1..77e16ebb7 100644 --- a/apps/settings/utils/common.py +++ b/apps/settings/utils/common.py @@ -9,8 +9,9 @@ from common.utils import lookup_domain def get_interface_setting_or_default(): if not settings.XPACK_ENABLED: return default_interface + from xpack.plugins.interface.models import Interface - return Interface.get_interface_setting() + return Interface.get_interface_setting(default_interface) def get_login_title(): diff --git a/apps/templates/flash_message_standalone.html b/apps/templates/flash_message_standalone.html index da34e1aae..97a75e67a 100644 --- a/apps/templates/flash_message_standalone.html +++ b/apps/templates/flash_message_standalone.html @@ -52,7 +52,7 @@ {% else %} message = '{{ message|safe }}' {% endif %} - var redirect_url = '{{ redirect_url }}' + var redirect_url = '{{ redirect_url|escapejs }}' function redirect_page() { if (time >= 0) { @@ -69,4 +69,3 @@ {% endif %} {% endblock %} - diff --git a/apps/terminal/connect_methods.py b/apps/terminal/connect_methods.py index f136817df..82f7751f0 100644 --- a/apps/terminal/connect_methods.py +++ b/apps/terminal/connect_methods.py @@ -50,12 +50,15 @@ class NativeClient(TextChoices): Protocol.mysql: [cls.db_client, cls.db_guide], Protocol.mariadb: [cls.db_client, cls.db_guide], Protocol.redis: [cls.db_client, cls.db_guide], - Protocol.mongodb: [cls.db_client, cls.db_guide], - Protocol.oracle: [cls.db_client, cls.db_guide], Protocol.postgresql: [cls.db_client, cls.db_guide], Protocol.sqlserver: [cls.db_client, cls.db_guide], Protocol.vnc: [cls.vnc_client, cls.vnc_guide], } + if settings.VENDOR.lower() == 'jumpserver': + clients.update({ + Protocol.mongodb: [cls.db_client, cls.db_guide], + Protocol.oracle: [cls.db_client, cls.db_guide], + }) return clients @classmethod diff --git a/apps/terminal/serializers/endpoint.py b/apps/terminal/serializers/endpoint.py index 69288316c..d407d306e 100644 --- a/apps/terminal/serializers/endpoint.py +++ b/apps/terminal/serializers/endpoint.py @@ -1,4 +1,5 @@ from django.utils.translation import gettext_lazy as _ +from django.conf import settings from rest_framework import serializers from acls.serializers.rules import address_validator, ip_group_help_text @@ -30,6 +31,15 @@ class EndpointSerializer(BulkModelSerializer): ) }, } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.remove_fields_if_need() + + def remove_fields_if_need(self): + if settings.VENDOR.lower() != 'jumpserver': + self.fields.pop('oracle_port') + self.fields.pop('mongodb_port') def get_extra_kwargs(self): extra_kwargs = super().get_extra_kwargs() diff --git a/apps/tickets/templates/tickets/approve_check_password.html b/apps/tickets/templates/tickets/approve_check_password.html index dabaaf7d8..fac814961 100644 --- a/apps/tickets/templates/tickets/approve_check_password.html +++ b/apps/tickets/templates/tickets/approve_check_password.html @@ -111,7 +111,6 @@ .field-value { color: #1F2329; - display: inline-block; } } } diff --git a/apps/tickets/views/approve.py b/apps/tickets/views/approve.py index 553fcfe69..92409e2eb 100644 --- a/apps/tickets/views/approve.py +++ b/apps/tickets/views/approve.py @@ -3,16 +3,14 @@ from __future__ import unicode_literals -from django.core.cache import cache -from django.http import HttpResponse from django.conf import settings +from django.core.cache import cache from django.shortcuts import redirect, reverse from django.utils.translation import gettext as _ from django.views.generic.base import TemplateView -from common.utils import get_logger, FlashMessageUtil from common.exceptions import JMSException -from users.models import User +from common.utils import get_logger, FlashMessageUtil from orgs.utils import tmp_to_root_org from tickets.const import TicketType from tickets.errors import AlreadyClosed @@ -20,6 +18,7 @@ from tickets.models import ( Ticket, ApplyAssetTicket, ApplyLoginTicket, ApplyLoginAssetTicket, ApplyCommandTicket ) +from users.models import User logger = get_logger(__name__) @@ -49,7 +48,7 @@ class TicketDirectApproveView(TemplateView): @property def login_url(self): - return reverse('authentication:login') + '?admin=1' + return reverse('authentication:login') + f'?admin=1&{self.redirect_field_name}=/' def redirect_message_response(self, **kwargs): message_data = self.message_data @@ -76,10 +75,11 @@ class TicketDirectApproveView(TemplateView): def get(self, request, *args, **kwargs): if not (settings.TICKETS_DIRECT_APPROVE or request.user.is_authenticated): direct_url = reverse('tickets:direct-approve', kwargs={'token': kwargs['token']}) + redirect_url = f'{reverse('authentication:login') + '?admin=1'}&{self.redirect_field_name}={direct_url}' message_data = { 'title': _('Ticket approval'), 'message': _('After successful authentication, this ticket can be approved directly'), - 'redirect_url': f'{self.login_url}&{self.redirect_field_name}={direct_url}', + 'redirect_url': redirect_url, 'auto_redirect': True, } redirect_url = FlashMessageUtil.gen_message_url(message_data)