From c16319ec48390bee6367ea7a7d33e4ed4612568a Mon Sep 17 00:00:00 2001 From: xinwen Date: Wed, 24 Mar 2021 19:01:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=BC=81=E4=B8=9A?= =?UTF-8?q?=E5=BE=AE=E4=BF=A1=EF=BC=8C=E9=92=89=E9=92=89=E6=89=AB=E7=A0=81?= =?UTF-8?q?=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/audits/signals_handler.py | 2 + apps/authentication/api/__init__.py | 3 + apps/authentication/api/dingtalk.py | 35 + apps/authentication/api/password.py | 26 + apps/authentication/api/wecom.py | 35 + apps/authentication/backends/api.py | 18 + apps/authentication/errors.py | 27 + apps/authentication/mixins.py | 66 +- apps/authentication/serializers.py | 9 +- .../templates/authentication/login.html | 24 +- apps/authentication/urls/api_urls.py | 7 + apps/authentication/urls/view_urls.py | 16 + apps/authentication/views/__init__.py | 2 + apps/authentication/views/dingtalk.py | 243 ++++++ apps/authentication/views/login.py | 19 +- apps/authentication/views/wecom.py | 241 ++++++ apps/common/message/__init__.py | 0 apps/common/message/backends/__init__.py | 0 .../message/backends/dingtalk/__init__.py | 168 +++++ apps/common/message/backends/exceptions.py | 28 + apps/common/message/backends/mixin.py | 94 +++ apps/common/message/backends/utils.py | 78 ++ .../common/message/backends/wecom/__init__.py | 194 +++++ apps/common/mixins/api.py | 23 + apps/common/request_log.py | 15 + apps/common/utils/random.py | 1 - apps/jumpserver/conf.py | 10 + apps/jumpserver/context_processor.py | 2 + apps/jumpserver/settings/auth.py | 19 + apps/jumpserver/urls.py | 2 +- apps/locale/zh/LC_MESSAGES/django.mo | Bin 72376 -> 74427 bytes apps/locale/zh/LC_MESSAGES/django.po | 693 +++++++++++------- apps/ops/apps.py | 2 + apps/orgs/signals_handler/common.py | 2 - apps/perms/api/asset/user_permission/mixin.py | 18 +- apps/settings/api/__init__.py | 2 + apps/settings/api/common.py | 8 +- apps/settings/api/dingtalk.py | 38 + apps/settings/api/wecom.py | 38 + apps/settings/serializers/settings.py | 20 +- apps/settings/urls/api_urls.py | 2 + apps/static/img/login_dingtalk_log.png | Bin 0 -> 5062 bytes apps/static/img/login_wecom_log.png | Bin 0 -> 137407 bytes apps/terminal/apps.py | 2 + .../migrations/0034_auto_20210506_1448.py | 23 + apps/users/models/user.py | 15 +- apps/users/permissions.py | 10 + apps/users/serializers/user.py | 1 + 48 files changed, 1984 insertions(+), 297 deletions(-) create mode 100644 apps/authentication/api/dingtalk.py create mode 100644 apps/authentication/api/password.py create mode 100644 apps/authentication/api/wecom.py create mode 100644 apps/authentication/views/dingtalk.py create mode 100644 apps/authentication/views/wecom.py create mode 100644 apps/common/message/__init__.py create mode 100644 apps/common/message/backends/__init__.py create mode 100644 apps/common/message/backends/dingtalk/__init__.py create mode 100644 apps/common/message/backends/exceptions.py create mode 100644 apps/common/message/backends/mixin.py create mode 100644 apps/common/message/backends/utils.py create mode 100644 apps/common/message/backends/wecom/__init__.py create mode 100644 apps/common/request_log.py create mode 100644 apps/settings/api/dingtalk.py create mode 100644 apps/settings/api/wecom.py create mode 100644 apps/static/img/login_dingtalk_log.png create mode 100644 apps/static/img/login_wecom_log.png create mode 100644 apps/users/migrations/0034_auto_20210506_1448.py create mode 100644 apps/users/permissions.py diff --git a/apps/audits/signals_handler.py b/apps/audits/signals_handler.py index c99c52bc9..930a1f03d 100644 --- a/apps/audits/signals_handler.py +++ b/apps/audits/signals_handler.py @@ -57,6 +57,8 @@ class AuthBackendLabelMapping(LazyObject): backend_label_mapping[settings.AUTH_BACKEND_PUBKEY] = _('SSH Key') backend_label_mapping[settings.AUTH_BACKEND_MODEL] = _('Password') backend_label_mapping[settings.AUTH_BACKEND_SSO] = _('SSO') + backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _('WeCom') + backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _('DingTalk') return backend_label_mapping def _setup(self): diff --git a/apps/authentication/api/__init__.py b/apps/authentication/api/__init__.py index 12b83421f..6ef54b09b 100644 --- a/apps/authentication/api/__init__.py +++ b/apps/authentication/api/__init__.py @@ -7,3 +7,6 @@ from .mfa import * from .access_key import * from .login_confirm import * from .sso import * +from .wecom import * +from .dingtalk import * +from .password import * diff --git a/apps/authentication/api/dingtalk.py b/apps/authentication/api/dingtalk.py new file mode 100644 index 000000000..e4b2ea85b --- /dev/null +++ b/apps/authentication/api/dingtalk.py @@ -0,0 +1,35 @@ +from rest_framework.views import APIView +from rest_framework.request import Request +from rest_framework.response import Response + +from users.permissions import IsAuthPasswdTimeValid +from users.models import User +from common.utils import get_logger +from common.permissions import IsOrgAdmin +from common.mixins.api import RoleUserMixin, RoleAdminMixin +from authentication import errors + +logger = get_logger(__file__) + + +class DingTalkQRUnBindBase(APIView): + user: User + + def post(self, request: Request, **kwargs): + user = self.user + + if not user.dingtalk_id: + raise errors.DingTalkNotBound + + user.dingtalk_id = '' + user.save() + return Response() + + +class DingTalkQRUnBindForUserApi(RoleUserMixin, DingTalkQRUnBindBase): + permission_classes = (IsAuthPasswdTimeValid,) + + +class DingTalkQRUnBindForAdminApi(RoleAdminMixin, DingTalkQRUnBindBase): + user_id_url_kwarg = 'user_id' + permission_classes = (IsOrgAdmin,) diff --git a/apps/authentication/api/password.py b/apps/authentication/api/password.py new file mode 100644 index 000000000..af8b41358 --- /dev/null +++ b/apps/authentication/api/password.py @@ -0,0 +1,26 @@ +from rest_framework.generics import CreateAPIView +from rest_framework.response import Response + +from authentication.serializers import PasswordVerifySerializer +from common.permissions import IsValidUser +from authentication.mixins import authenticate +from authentication.errors import PasswdInvalid +from authentication.mixins import AuthMixin + + +class UserPasswordVerifyApi(AuthMixin, CreateAPIView): + permission_classes = (IsValidUser,) + serializer_class = PasswordVerifySerializer + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + password = serializer.validated_data['password'] + user = self.request.user + + user = authenticate(request=request, username=user.username, password=password) + if not user: + raise PasswdInvalid + + self.set_passwd_verify_on_session(user) + return Response() diff --git a/apps/authentication/api/wecom.py b/apps/authentication/api/wecom.py new file mode 100644 index 000000000..1ab5ff725 --- /dev/null +++ b/apps/authentication/api/wecom.py @@ -0,0 +1,35 @@ +from rest_framework.views import APIView +from rest_framework.request import Request +from rest_framework.response import Response + +from users.permissions import IsAuthPasswdTimeValid +from users.models import User +from common.utils import get_logger +from common.permissions import IsOrgAdmin +from common.mixins.api import RoleUserMixin, RoleAdminMixin +from authentication import errors + +logger = get_logger(__file__) + + +class WeComQRUnBindBase(APIView): + user: User + + def post(self, request: Request, **kwargs): + user = self.user + + if not user.wecom_id: + raise errors.WeComNotBound + + user.wecom_id = '' + user.save() + return Response() + + +class WeComQRUnBindForUserApi(RoleUserMixin, WeComQRUnBindBase): + permission_classes = (IsAuthPasswdTimeValid,) + + +class WeComQRUnBindForAdminApi(RoleAdminMixin, WeComQRUnBindBase): + user_id_url_kwarg = 'user_id' + permission_classes = (IsOrgAdmin,) diff --git a/apps/authentication/backends/api.py b/apps/authentication/backends/api.py index 1fd315abb..63356eff6 100644 --- a/apps/authentication/backends/api.py +++ b/apps/authentication/backends/api.py @@ -205,3 +205,21 @@ class SSOAuthentication(ModelBackend): def authenticate(self, request, sso_token=None, **kwargs): pass + + +class WeComAuthentication(ModelBackend): + """ + 什么也不做呀😺 + """ + + def authenticate(self, request, **kwargs): + pass + + +class DingTalkAuthentication(ModelBackend): + """ + 什么也不做呀😺 + """ + + def authenticate(self, request, **kwargs): + pass diff --git a/apps/authentication/errors.py b/apps/authentication/errors.py index 03368baa8..bcd83c97d 100644 --- a/apps/authentication/errors.py +++ b/apps/authentication/errors.py @@ -19,6 +19,7 @@ reason_user_inactive = 'user_inactive' reason_user_expired = 'user_expired' reason_backend_not_match = 'backend_not_match' reason_acl_not_allow = 'acl_not_allow' +only_local_users_are_allowed = 'only_local_users_are_allowed' reason_choices = { reason_password_failed: _('Username/password check failed'), @@ -32,6 +33,7 @@ reason_choices = { reason_user_expired: _("This account is expired"), reason_backend_not_match: _("Auth backend not match"), reason_acl_not_allow: _("ACL is not allowed"), + only_local_users_are_allowed: _("Only local users are allowed") } old_reason_choices = { '0': '-', @@ -291,3 +293,28 @@ class PasswordRequireResetError(JMSException): def __init__(self, url, *args, **kwargs): super().__init__(*args, **kwargs) self.url = url + + +class WeComCodeInvalid(JMSException): + default_code = 'wecom_code_invalid' + default_detail = 'Code invalid, can not get user info' + + +class WeComBindAlready(JMSException): + default_code = 'wecom_bind_already' + default_detail = 'WeCom already binded' + + +class WeComNotBound(JMSException): + default_code = 'wecom_not_bound' + default_detail = 'WeCom is not bound' + + +class DingTalkNotBound(JMSException): + default_code = 'dingtalk_not_bound' + default_detail = 'DingTalk is not bound' + + +class PasswdInvalid(JMSException): + default_code = 'passwd_invalid' + default_detail = _('Your password is invalid') diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index 9b81d17fe..5eeceb7c3 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -5,6 +5,7 @@ from urllib.parse import urlencode from functools import partial import time +from django.core.cache import cache from django.conf import settings from django.contrib import auth from django.utils.translation import ugettext as _ @@ -12,7 +13,7 @@ from django.contrib.auth import ( BACKEND_SESSION_KEY, _get_backends, PermissionDenied, user_login_failed, _clean_credentials ) -from django.shortcuts import reverse +from django.shortcuts import reverse, redirect from common.utils import get_object_or_none, get_request_ip, get_logger, bulk_get, FlashMessageUtil from users.models import User @@ -82,6 +83,8 @@ class AuthMixin: request = None partial_credential_error = None + key_prefix_captcha = "_LOGIN_INVALID_{}" + def get_user_from_session(self): if self.request.session.is_empty(): raise errors.SessionEmptyError() @@ -110,11 +113,7 @@ class AuthMixin: ip = ip or get_request_ip(self.request) return ip - def check_is_block(self, raise_exception=True): - if hasattr(self.request, 'data'): - username = self.request.data.get("username") - else: - username = self.request.POST.get("username") + def _check_is_block(self, username, raise_exception=True): ip = self.get_request_ip() if LoginBlockUtil(username, ip).is_block(): logger.warn('Ip was blocked' + ': ' + username + ':' + ip) @@ -124,6 +123,13 @@ class AuthMixin: else: return exception + def check_is_block(self, raise_exception=True): + if hasattr(self.request, 'data'): + username = self.request.data.get("username") + else: + username = self.request.POST.get("username") + self._check_is_block(username, raise_exception) + def decrypt_passwd(self, raw_passwd): # 获取解密密钥,对密码进行解密 rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY) @@ -140,6 +146,9 @@ class AuthMixin: def raise_credential_error(self, error): raise self.partial_credential_error(error=error) + def _set_partial_credential_error(self, username, ip, request): + self.partial_credential_error = partial(errors.CredentialError, username=username, ip=ip, request=request) + def get_auth_data(self, decrypt_passwd=False): request = self.request if hasattr(request, 'data'): @@ -151,7 +160,7 @@ class AuthMixin: username, password, challenge, public_key, auto_login = bulk_get(data, *items, default='') password = password + challenge.strip() ip = self.get_request_ip() - self.partial_credential_error = partial(errors.CredentialError, username=username, ip=ip, request=request) + self._set_partial_credential_error(username=username, ip=ip, request=request) if decrypt_passwd: password = self.decrypt_passwd(password) @@ -184,6 +193,21 @@ class AuthMixin: if not is_allowed: raise errors.LoginIPNotAllowed(username=user.username, request=self.request) + def set_login_failed_mark(self): + ip = self.get_request_ip() + cache.set(self.key_prefix_captcha.format(ip), 1, 3600) + + def set_passwd_verify_on_session(self, user: User): + self.request.session['user_id'] = str(user.id) + self.request.session['auth_password'] = 1 + self.request.session['auth_password_expired_at'] = time.time() + settings.AUTH_EXPIRED_SECONDS + + def check_is_need_captcha(self): + # 最近有登录失败时需要填写验证码 + ip = get_request_ip(self.request) + need = cache.get(self.key_prefix_captcha.format(ip)) + return need + def check_user_auth(self, decrypt_passwd=False): self.check_is_block() request = self.request @@ -204,6 +228,27 @@ class AuthMixin: request.session['auth_backend'] = getattr(user, 'backend', settings.AUTH_BACKEND_MODEL) return user + def _check_is_local_user(self, user: User): + if user.source != User.Source.local: + raise self.raise_credential_error(error=errors.only_local_users_are_allowed) + + def check_oauth2_auth(self, user: User, auth_backend): + ip = self.get_request_ip() + request = self.request + + self._set_partial_credential_error(user.username, ip, request) + self._check_is_local_user(user) + self._check_is_block(user.username) + self._check_login_acl(user, ip) + + LoginBlockUtil(user.username, ip).clean_failed_count() + MFABlockUtils(user.username, ip).clean_failed_count() + + request.session['auth_password'] = 1 + request.session['user_id'] = str(user.id) + request.session['auth_backend'] = auth_backend + return user + @classmethod def generate_reset_password_url_with_flash_msg(cls, user, message): reset_passwd_url = reverse('authentication:reset-password') @@ -354,3 +399,10 @@ class AuthMixin: sender=self.__class__, username=username, request=self.request, reason=reason ) + + def redirect_to_guard_view(self): + guard_url = reverse('authentication:login-guard') + args = self.request.META.get('QUERY_STRING', '') + if args: + guard_url = "%s?%s" % (guard_url, args) + return redirect(guard_url) diff --git a/apps/authentication/serializers.py b/apps/authentication/serializers.py index 5f2bc231b..72b54e3ee 100644 --- a/apps/authentication/serializers.py +++ b/apps/authentication/serializers.py @@ -10,13 +10,14 @@ from applications.models import Application from users.serializers import UserProfileSerializer from assets.serializers import ProtocolsField from perms.serializers.asset.permission import ActionsField -from .models import AccessKey, LoginConfirmSetting, SSOToken +from .models import AccessKey, LoginConfirmSetting __all__ = [ 'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer', 'MFAChallengeSerializer', 'LoginConfirmSettingSerializer', 'SSOTokenSerializer', - 'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer', 'RDPFileSerializer' + 'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer', 'RDPFileSerializer', + 'PasswordVerifySerializer', ] @@ -31,6 +32,10 @@ class OtpVerifySerializer(serializers.Serializer): code = serializers.CharField(max_length=6, min_length=6) +class PasswordVerifySerializer(serializers.Serializer): + password = serializers.CharField() + + class BearerTokenSerializer(serializers.Serializer): username = serializers.CharField(allow_null=True, required=False, write_only=True) password = serializers.CharField(write_only=True, allow_null=True, diff --git a/apps/authentication/templates/authentication/login.html b/apps/authentication/templates/authentication/login.html index 722ef5d16..0104d0e44 100644 --- a/apps/authentication/templates/authentication/login.html +++ b/apps/authentication/templates/authentication/login.html @@ -117,6 +117,15 @@ float: right; margin: 10px 10px 0 0; } + .more-login-item { + border-right: 1px dashed #dedede; + padding-left: 5px; + padding-right: 5px; + } + + .more-login-item:last-child { + border: none; + } @@ -182,10 +191,10 @@
- {% if AUTH_OPENID or AUTH_CAS %} + {% if AUTH_OPENID or AUTH_CAS or AUTH_WECOM or AUTH_DINGTALK %}
- {% trans "More login options" %} + {% trans "More login options" %} {% if AUTH_OPENID %} {% endif %} + {% if AUTH_WECOM %} + + {% endif %} + {% if AUTH_DINGTALK %} + + {% endif %} +
{% else %}
diff --git a/apps/authentication/urls/api_urls.py b/apps/authentication/urls/api_urls.py index d9b302800..0849cc82a 100644 --- a/apps/authentication/urls/api_urls.py +++ b/apps/authentication/urls/api_urls.py @@ -14,10 +14,17 @@ router.register('connection-token', api.UserConnectionTokenViewSet, 'connection- urlpatterns = [ # path('token/', api.UserToken.as_view(), name='user-token'), + path('wecom/qr/unbind/', api.WeComQRUnBindForUserApi.as_view(), name='wecom-qr-unbind'), + path('wecom/qr/unbind//', api.WeComQRUnBindForAdminApi.as_view(), name='wecom-qr-unbind-for-admin'), + + path('dingtalk/qr/unbind/', api.DingTalkQRUnBindForUserApi.as_view(), name='dingtalk-qr-unbind'), + path('dingtalk/qr/unbind//', api.DingTalkQRUnBindForAdminApi.as_view(), name='dingtalk-qr-unbind-for-admin'), + path('auth/', api.TokenCreateApi.as_view(), name='user-auth'), path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'), path('mfa/challenge/', api.MFAChallengeApi.as_view(), name='mfa-challenge'), path('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'), + path('password/verify/', api.UserPasswordVerifyApi.as_view(), name='user-password-verify'), path('login-confirm-ticket/status/', api.TicketStatusApi.as_view(), name='login-confirm-ticket-status'), path('login-confirm-settings//', api.LoginConfirmSettingUpdateApi.as_view(), name='login-confirm-setting-update') ] diff --git a/apps/authentication/urls/view_urls.py b/apps/authentication/urls/view_urls.py index c4e4de00a..8e754340c 100644 --- a/apps/authentication/urls/view_urls.py +++ b/apps/authentication/urls/view_urls.py @@ -21,6 +21,22 @@ urlpatterns = [ path('password/reset/', users_view.UserResetPasswordView.as_view(), name='reset-password'), path('password/verify/', users_view.UserVerifyPasswordView.as_view(), name='user-verify-password'), + path('wecom/bind/success-flash-msg/', views.FlashWeComBindSucceedMsgView.as_view(), name='wecom-bind-success-flash-msg'), + path('wecom/bind/failed-flash-msg/', views.FlashWeComBindFailedMsgView.as_view(), name='wecom-bind-failed-flash-msg'), + path('wecom/bind/start/', views.WeComEnableStartView.as_view(), name='wecom-bind-start'), + path('wecom/qr/bind/', views.WeComQRBindView.as_view(), name='wecom-qr-bind'), + path('wecom/qr/login/', views.WeComQRLoginView.as_view(), name='wecom-qr-login'), + path('wecom/qr/bind//callback/', views.WeComQRBindCallbackView.as_view(), name='wecom-qr-bind-callback'), + path('wecom/qr/login/callback/', views.WeComQRLoginCallbackView.as_view(), name='wecom-qr-login-callback'), + + path('dingtalk/bind/success-flash-msg/', views.FlashDingTalkBindSucceedMsgView.as_view(), name='dingtalk-bind-success-flash-msg'), + path('dingtalk/bind/failed-flash-msg/', views.FlashDingTalkBindFailedMsgView.as_view(), name='dingtalk-bind-failed-flash-msg'), + path('dingtalk/bind/start/', views.DingTalkEnableStartView.as_view(), name='dingtalk-bind-start'), + path('dingtalk/qr/bind/', views.DingTalkQRBindView.as_view(), name='dingtalk-qr-bind'), + path('dingtalk/qr/login/', views.DingTalkQRLoginView.as_view(), name='dingtalk-qr-login'), + path('dingtalk/qr/bind//callback/', views.DingTalkQRBindCallbackView.as_view(), name='dingtalk-qr-bind-callback'), + path('dingtalk/qr/login/callback/', views.DingTalkQRLoginCallbackView.as_view(), name='dingtalk-qr-login-callback'), + # Profile path('profile/pubkey/generate/', users_view.UserPublicKeyGenerateView.as_view(), name='user-pubkey-generate'), path('profile/otp/enable/start/', users_view.UserOtpEnableStartView.as_view(), name='user-otp-enable-start'), diff --git a/apps/authentication/views/__init__.py b/apps/authentication/views/__init__.py index 5a1a40f7a..0467e321a 100644 --- a/apps/authentication/views/__init__.py +++ b/apps/authentication/views/__init__.py @@ -2,3 +2,5 @@ # from .login import * from .mfa import * +from .wecom import * +from .dingtalk import * diff --git a/apps/authentication/views/dingtalk.py b/apps/authentication/views/dingtalk.py new file mode 100644 index 000000000..521a93c26 --- /dev/null +++ b/apps/authentication/views/dingtalk.py @@ -0,0 +1,243 @@ +import urllib + +from django.http.response import HttpResponseRedirect +from django.utils.decorators import method_decorator +from django.utils.translation import ugettext as _ +from django.views.decorators.cache import never_cache +from django.views.generic import TemplateView +from django.views import View +from django.conf import settings +from django.http.request import HttpRequest +from rest_framework.permissions import IsAuthenticated, AllowAny + +from users.views import UserVerifyPasswordView +from users.utils import is_auth_password_time_valid +from users.models import User +from common.utils import get_logger +from common.utils.random import random_string +from common.utils.django import reverse, get_object_or_none +from common.message.backends.dingtalk import URL +from common.mixins.views import PermissionsMixin +from authentication import errors +from authentication.mixins import AuthMixin +from common.message.backends.dingtalk import DingTalk + +logger = get_logger(__file__) + + +DINGTALK_STATE_SESSION_KEY = '_dingtalk_state' + + +class DingTalkQRMixin(PermissionsMixin, View): + def verify_state(self): + state = self.request.GET.get('state') + session_state = self.request.session.get(DINGTALK_STATE_SESSION_KEY) + if state != session_state: + return False + return True + + def get_verify_state_failed_response(self, redirect_uri): + msg = _("You've been hacked") + return self.get_failed_reponse(redirect_uri, msg, msg) + + def get_qr_url(self, redirect_uri): + state = random_string(16) + self.request.session[DINGTALK_STATE_SESSION_KEY] = state + + params = { + 'appid': settings.DINGTALK_APPKEY, + 'response_type': 'code', + 'scope': 'snsapi_login', + 'state': state, + 'redirect_uri': redirect_uri, + } + url = URL.QR_CONNECT + '?' + urllib.parse.urlencode(params) + return url + + def get_success_reponse(self, redirect_url, title, msg): + ok_flash_msg_url = reverse('authentication:dingtalk-bind-success-flash-msg') + ok_flash_msg_url += '?' + urllib.parse.urlencode({ + 'redirect_url': redirect_url, + 'title': title, + 'msg': msg + }) + return HttpResponseRedirect(ok_flash_msg_url) + + def get_failed_reponse(self, redirect_url, title, msg): + failed_flash_msg_url = reverse('authentication:dingtalk-bind-failed-flash-msg') + failed_flash_msg_url += '?' + urllib.parse.urlencode({ + 'redirect_url': redirect_url, + 'title': title, + 'msg': msg + }) + return HttpResponseRedirect(failed_flash_msg_url) + + def get_already_bound_response(self, redirect_url): + msg = _('DingTalk is already bound') + response = self.get_failed_reponse(redirect_url, msg, msg) + return response + + +class DingTalkQRBindView(DingTalkQRMixin, View): + permission_classes = (IsAuthenticated,) + + def get(self, request: HttpRequest): + user = request.user + redirect_url = request.GET.get('redirect_url') + + if not is_auth_password_time_valid(request.session): + msg = _('Please verify your password first') + response = self.get_failed_reponse(redirect_url, msg, msg) + return response + + redirect_uri = reverse('authentication:dingtalk-qr-bind-callback', kwargs={'user_id': user.id}, external=True) + redirect_uri += '?' + urllib.parse.urlencode({'redirect_url': redirect_url}) + + url = self.get_qr_url(redirect_uri) + return HttpResponseRedirect(url) + + +class DingTalkQRBindCallbackView(DingTalkQRMixin, View): + permission_classes = (IsAuthenticated,) + + def get(self, request: HttpRequest, user_id): + code = request.GET.get('code') + redirect_url = request.GET.get('redirect_url') + + if not self.verify_state(): + return self.get_verify_state_failed_response(redirect_url) + + user = get_object_or_none(User, id=user_id) + if user is None: + logger.error(f'DingTalkQR bind callback error, user_id invalid: user_id={user_id}') + msg = _('Invalid user_id') + response = self.get_failed_reponse(redirect_url, msg, msg) + return response + + if user.dingtalk_id: + response = self.get_already_bound_response(redirect_url) + return response + + dingtalk = DingTalk( + appid=settings.DINGTALK_APPKEY, + appsecret=settings.DINGTALK_APPSECRET, + agentid=settings.DINGTALK_AGENTID + ) + userid = dingtalk.get_userid_by_code(code) + + if not userid: + msg = _('DingTalk query user failed') + response = self.get_failed_reponse(redirect_url, msg, msg) + return response + + user.dingtalk_id = userid + user.save() + + msg = _('Binding DingTalk successfully') + response = self.get_success_reponse(redirect_url, msg, msg) + return response + + +class DingTalkEnableStartView(UserVerifyPasswordView): + + def get_success_url(self): + referer = self.request.META.get('HTTP_REFERER') + redirect_url = self.request.GET.get("redirect_url") + + success_url = reverse('authentication:dingtalk-qr-bind') + + success_url += '?' + urllib.parse.urlencode({ + 'redirect_url': redirect_url or referer + }) + + return success_url + + +class DingTalkQRLoginView(DingTalkQRMixin, View): + permission_classes = (AllowAny,) + + def get(self, request: HttpRequest): + redirect_url = request.GET.get('redirect_url') + + redirect_uri = reverse('authentication:dingtalk-qr-login-callback', external=True) + redirect_uri += '?' + urllib.parse.urlencode({'redirect_url': redirect_url}) + + url = self.get_qr_url(redirect_uri) + return HttpResponseRedirect(url) + + +class DingTalkQRLoginCallbackView(AuthMixin, DingTalkQRMixin, View): + permission_classes = (AllowAny,) + + def get(self, request: HttpRequest): + code = request.GET.get('code') + redirect_url = request.GET.get('redirect_url') + login_url = reverse('authentication:login') + + if not self.verify_state(): + return self.get_verify_state_failed_response(redirect_url) + + dingtalk = DingTalk( + appid=settings.DINGTALK_APPKEY, + appsecret=settings.DINGTALK_APPSECRET, + agentid=settings.DINGTALK_AGENTID + ) + userid = dingtalk.get_userid_by_code(code) + if not userid: + # 正常流程不会出这个错误,hack 行为 + msg = _('Failed to get user from DingTalk') + response = self.get_failed_reponse(login_url, title=msg, msg=msg) + return response + + user = get_object_or_none(User, dingtalk_id=userid) + if user is None: + title = _('DingTalk is not bound') + msg = _('Please login with a password and then bind the WoCom') + response = self.get_failed_reponse(login_url, title=title, msg=msg) + return response + + try: + self.check_oauth2_auth(user, settings.AUTH_BACKEND_DINGTALK) + except errors.AuthFailedError as e: + self.set_login_failed_mark() + msg = e.msg + response = self.get_failed_reponse(login_url, title=msg, msg=msg) + return response + + return self.redirect_to_guard_view() + + +@method_decorator(never_cache, name='dispatch') +class FlashDingTalkBindSucceedMsgView(TemplateView): + template_name = 'flash_message_standalone.html' + + def get(self, request, *args, **kwargs): + title = request.GET.get('title') + msg = request.GET.get('msg') + + context = { + 'title': title or _('Binding DingTalk successfully'), + 'messages': msg or _('Binding DingTalk successfully'), + 'interval': 5, + 'redirect_url': request.GET.get('redirect_url'), + 'auto_redirect': True, + } + return self.render_to_response(context) + + +@method_decorator(never_cache, name='dispatch') +class FlashDingTalkBindFailedMsgView(TemplateView): + template_name = 'flash_message_standalone.html' + + def get(self, request, *args, **kwargs): + title = request.GET.get('title') + msg = request.GET.get('msg') + + context = { + 'title': title or _('Binding DingTalk failed'), + 'messages': msg or _('Binding DingTalk failed'), + 'interval': 5, + 'redirect_url': request.GET.get('redirect_url'), + 'auto_redirect': True, + } + return self.render_to_response(context) diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index 2412ef54a..083418940 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals import os import datetime -from django.core.cache import cache from django.contrib.auth import login as auth_login, logout as auth_logout from django.http import HttpResponse from django.shortcuts import reverse, redirect @@ -38,7 +37,6 @@ __all__ = [ @method_decorator(csrf_protect, name='dispatch') @method_decorator(never_cache, name='dispatch') class UserLoginView(mixins.AuthMixin, FormView): - key_prefix_captcha = "_LOGIN_INVALID_{}" redirect_field_name = 'next' template_name = 'authentication/login.html' @@ -90,10 +88,9 @@ class UserLoginView(mixins.AuthMixin, FormView): try: self.check_user_auth(decrypt_passwd=True) except errors.AuthFailedError as e: - e = self.check_is_block(raise_exception=False) or e form.add_error(None, e.msg) - ip = self.get_request_ip() - cache.set(self.key_prefix_captcha.format(ip), 1, 3600) + self.set_login_failed_mark() + form_cls = get_user_login_form_cls(captcha=True) new_form = form_cls(data=form.data) new_form._errors = form.errors @@ -105,16 +102,8 @@ class UserLoginView(mixins.AuthMixin, FormView): self.clear_rsa_key() return self.redirect_to_guard_view() - def redirect_to_guard_view(self): - guard_url = reverse('authentication:login-guard') - args = self.request.META.get('QUERY_STRING', '') - if args: - guard_url = "%s?%s" % (guard_url, args) - return redirect(guard_url) - def get_form_class(self): - ip = get_request_ip(self.request) - if cache.get(self.key_prefix_captcha.format(ip)): + if self.check_is_need_captcha(): return get_user_login_form_cls(captcha=True) else: return get_user_login_form_cls() @@ -142,6 +131,8 @@ class UserLoginView(mixins.AuthMixin, FormView): 'demo_mode': os.environ.get("DEMO_MODE"), 'AUTH_OPENID': settings.AUTH_OPENID, 'AUTH_CAS': settings.AUTH_CAS, + 'AUTH_WECOM': settings.AUTH_WECOM, + 'AUTH_DINGTALK': settings.AUTH_DINGTALK, 'rsa_public_key': rsa_public_key, 'forgot_password_url': forgot_password_url } diff --git a/apps/authentication/views/wecom.py b/apps/authentication/views/wecom.py new file mode 100644 index 000000000..5dc683f87 --- /dev/null +++ b/apps/authentication/views/wecom.py @@ -0,0 +1,241 @@ +import urllib + +from django.http.response import HttpResponseRedirect +from django.utils.decorators import method_decorator +from django.utils.translation import ugettext as _ +from django.views.decorators.cache import never_cache +from django.views.generic import TemplateView +from django.views import View +from django.conf import settings +from django.http.request import HttpRequest +from rest_framework.permissions import IsAuthenticated, AllowAny + +from users.views import UserVerifyPasswordView +from users.utils import is_auth_password_time_valid +from users.models import User +from common.utils import get_logger +from common.utils.random import random_string +from common.utils.django import reverse, get_object_or_none +from common.message.backends.wecom import URL +from common.message.backends.wecom import WeCom +from common.mixins.views import PermissionsMixin +from authentication import errors +from authentication.mixins import AuthMixin + +logger = get_logger(__file__) + + +WECOM_STATE_SESSION_KEY = '_wecom_state' + + +class WeComQRMixin(PermissionsMixin, View): + def verify_state(self): + state = self.request.GET.get('state') + session_state = self.request.session.get(WECOM_STATE_SESSION_KEY) + if state != session_state: + return False + return True + + def get_verify_state_failed_response(self, redirect_uri): + msg = _("You've been hacked") + return self.get_failed_reponse(redirect_uri, msg, msg) + + def get_qr_url(self, redirect_uri): + state = random_string(16) + self.request.session[WECOM_STATE_SESSION_KEY] = state + + params = { + 'appid': settings.WECOM_CORPID, + 'agentid': settings.WECOM_AGENTID, + 'state': state, + 'redirect_uri': redirect_uri, + } + url = URL.QR_CONNECT + '?' + urllib.parse.urlencode(params) + return url + + def get_success_reponse(self, redirect_url, title, msg): + ok_flash_msg_url = reverse('authentication:wecom-bind-success-flash-msg') + ok_flash_msg_url += '?' + urllib.parse.urlencode({ + 'redirect_url': redirect_url, + 'title': title, + 'msg': msg + }) + return HttpResponseRedirect(ok_flash_msg_url) + + def get_failed_reponse(self, redirect_url, title, msg): + failed_flash_msg_url = reverse('authentication:wecom-bind-failed-flash-msg') + failed_flash_msg_url += '?' + urllib.parse.urlencode({ + 'redirect_url': redirect_url, + 'title': title, + 'msg': msg + }) + return HttpResponseRedirect(failed_flash_msg_url) + + def get_already_bound_response(self, redirect_url): + msg = _('WeCom is already bound') + response = self.get_failed_reponse(redirect_url, msg, msg) + return response + + +class WeComQRBindView(WeComQRMixin, View): + permission_classes = (IsAuthenticated,) + + def get(self, request: HttpRequest): + user = request.user + redirect_url = request.GET.get('redirect_url') + + if not is_auth_password_time_valid(request.session): + msg = _('Please verify your password first') + response = self.get_failed_reponse(redirect_url, msg, msg) + return response + + redirect_uri = reverse('authentication:wecom-qr-bind-callback', kwargs={'user_id': user.id}, external=True) + redirect_uri += '?' + urllib.parse.urlencode({'redirect_url': redirect_url}) + + url = self.get_qr_url(redirect_uri) + return HttpResponseRedirect(url) + + +class WeComQRBindCallbackView(WeComQRMixin, View): + permission_classes = (IsAuthenticated,) + + def get(self, request: HttpRequest, user_id): + code = request.GET.get('code') + redirect_url = request.GET.get('redirect_url') + + if not self.verify_state(): + return self.get_verify_state_failed_response(redirect_url) + + user = get_object_or_none(User, id=user_id) + if user is None: + logger.error(f'WeComQR bind callback error, user_id invalid: user_id={user_id}') + msg = _('Invalid user_id') + response = self.get_failed_reponse(redirect_url, msg, msg) + return response + + if user.wecom_id: + response = self.get_already_bound_response(redirect_url) + return response + + wecom = WeCom( + corpid=settings.WECOM_CORPID, + corpsecret=settings.WECOM_CORPSECRET, + agentid=settings.WECOM_AGENTID + ) + wecom_userid, __ = wecom.get_user_id_by_code(code) + if not wecom_userid: + msg = _('WeCom query user failed') + response = self.get_failed_reponse(redirect_url, msg, msg) + return response + + user.wecom_id = wecom_userid + user.save() + + msg = _('Binding WeCom successfully') + response = self.get_success_reponse(redirect_url, msg, msg) + return response + + +class WeComEnableStartView(UserVerifyPasswordView): + + def get_success_url(self): + referer = self.request.META.get('HTTP_REFERER') + redirect_url = self.request.GET.get("redirect_url") + + success_url = reverse('authentication:wecom-qr-bind') + + success_url += '?' + urllib.parse.urlencode({ + 'redirect_url': redirect_url or referer + }) + + return success_url + + +class WeComQRLoginView(WeComQRMixin, View): + permission_classes = (AllowAny,) + + def get(self, request: HttpRequest): + redirect_url = request.GET.get('redirect_url') + + redirect_uri = reverse('authentication:wecom-qr-login-callback', external=True) + redirect_uri += '?' + urllib.parse.urlencode({'redirect_url': redirect_url}) + + url = self.get_qr_url(redirect_uri) + return HttpResponseRedirect(url) + + +class WeComQRLoginCallbackView(AuthMixin, WeComQRMixin, View): + permission_classes = (AllowAny,) + + def get(self, request: HttpRequest): + code = request.GET.get('code') + redirect_url = request.GET.get('redirect_url') + login_url = reverse('authentication:login') + + if not self.verify_state(): + return self.get_verify_state_failed_response(redirect_url) + + wecom = WeCom( + corpid=settings.WECOM_CORPID, + corpsecret=settings.WECOM_CORPSECRET, + agentid=settings.WECOM_AGENTID + ) + wecom_userid, __ = wecom.get_user_id_by_code(code) + if not wecom_userid: + # 正常流程不会出这个错误,hack 行为 + msg = _('Failed to get user from WeCom') + response = self.get_failed_reponse(login_url, title=msg, msg=msg) + return response + + user = get_object_or_none(User, wecom_id=wecom_userid) + if user is None: + title = _('WeCom is not bound') + msg = _('Please login with a password and then bind the WoCom') + response = self.get_failed_reponse(login_url, title=title, msg=msg) + return response + + try: + self.check_oauth2_auth(user, settings.AUTH_BACKEND_WECOM) + except errors.AuthFailedError as e: + self.set_login_failed_mark() + msg = e.msg + response = self.get_failed_reponse(login_url, title=msg, msg=msg) + return response + + return self.redirect_to_guard_view() + + +@method_decorator(never_cache, name='dispatch') +class FlashWeComBindSucceedMsgView(TemplateView): + template_name = 'flash_message_standalone.html' + + def get(self, request, *args, **kwargs): + title = request.GET.get('title') + msg = request.GET.get('msg') + + context = { + 'title': title or _('Binding WeCom successfully'), + 'messages': msg or _('Binding WeCom successfully'), + 'interval': 5, + 'redirect_url': request.GET.get('redirect_url'), + 'auto_redirect': True, + } + return self.render_to_response(context) + + +@method_decorator(never_cache, name='dispatch') +class FlashWeComBindFailedMsgView(TemplateView): + template_name = 'flash_message_standalone.html' + + def get(self, request, *args, **kwargs): + title = request.GET.get('title') + msg = request.GET.get('msg') + + context = { + 'title': title or _('Binding WeCom failed'), + 'messages': msg or _('Binding WeCom failed'), + 'interval': 5, + 'redirect_url': request.GET.get('redirect_url'), + 'auto_redirect': True, + } + return self.render_to_response(context) diff --git a/apps/common/message/__init__.py b/apps/common/message/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/common/message/backends/__init__.py b/apps/common/message/backends/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/common/message/backends/dingtalk/__init__.py b/apps/common/message/backends/dingtalk/__init__.py new file mode 100644 index 000000000..0ca9d5dc5 --- /dev/null +++ b/apps/common/message/backends/dingtalk/__init__.py @@ -0,0 +1,168 @@ +import time +import hmac +import base64 + +from common.message.backends.utils import request +from common.message.backends.utils import digest +from common.message.backends.mixin import BaseRequest + + +def sign(secret, data): + + digest = hmac.HMAC( + key=secret.encode('utf8'), + msg=data.encode('utf8'), + digestmod=hmac._hashlib.sha256).digest() + signature = base64.standard_b64encode(digest).decode('utf8') + # signature = urllib.parse.quote(signature, safe='') + # signature = signature.replace('+', '%20').replace('*', '%2A').replace('~', '%7E').replace('/', '%2F') + return signature + + +class ErrorCode: + INVALID_TOKEN = 88 + + +class URL: + QR_CONNECT = 'https://oapi.dingtalk.com/connect/qrconnect' + GET_USER_INFO_BY_CODE = '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' + GET_SEND_MSG_PROGRESS = 'https://oapi.dingtalk.com/topapi/message/corpconversation/getsendprogress' + GET_USERID_BY_UNIONID = 'https://oapi.dingtalk.com/topapi/user/getbyunionid' + + +class DingTalkRequests(BaseRequest): + invalid_token_errcode = ErrorCode.INVALID_TOKEN + + def __init__(self, appid, appsecret, agentid, timeout=None): + self._appid = appid + self._appsecret = appsecret + self._agentid = agentid + + super().__init__(timeout=timeout) + + def get_access_token_cache_key(self): + return digest(self._appid, self._appsecret) + + def request_access_token(self): + # https://developers.dingtalk.com/document/app/obtain-orgapp-token?spm=ding_open_doc.document.0.0.3a256573JEWqIL#topic-1936350 + params = {'appkey': self._appid, 'appsecret': self._appsecret} + data = self.raw_request('get', url=URL.GET_TOKEN, params=params) + + access_token = data['access_token'] + expires_in = data['expires_in'] + return access_token, expires_in + + @request + def get(self, url, params=None, + with_token=False, with_sign=False, + check_errcode_is_0=True, + **kwargs): + pass + + @request + def post(self, url, json=None, params=None, + with_token=False, with_sign=False, + check_errcode_is_0=True, + **kwargs): + pass + + def _add_sign(self, params: dict): + timestamp = str(int(time.time() * 1000)) + signature = sign(self._appsecret, timestamp) + accessKey = self._appid + + params['timestamp'] = timestamp + params['signature'] = signature + params['accessKey'] = accessKey + + def request(self, method, url, params=None, + with_token=False, with_sign=False, + check_errcode_is_0=True, + **kwargs): + if not isinstance(params, dict): + params = {} + + if with_token: + params['access_token'] = self.access_token + + if with_sign: + self._add_sign(params) + + data = self.raw_request(method, url, params=params, **kwargs) + if check_errcode_is_0: + self.check_errcode_is_0(data) + + return data + + +class DingTalk: + def __init__(self, appid, appsecret, agentid, timeout=None): + self._appid = appid + self._appsecret = appsecret + self._agentid = agentid + + self._request = DingTalkRequests( + appid=appid, appsecret=appsecret, agentid=agentid, + timeout=timeout + ) + + def get_userinfo_bycode(self, code): + # https://developers.dingtalk.com/document/app/obtain-the-user-information-based-on-the-sns-temporary-authorization?spm=ding_open_doc.document.0.0.3a256573y8Y7yg#topic-1995619 + body = { + "tmp_auth_code": code + } + + data = self._request.post(URL.GET_USER_INFO_BY_CODE, json=body, with_sign=True) + return data['user_info'] + + def get_userid_by_code(self, code): + user_info = self.get_userinfo_bycode(code) + unionid = user_info['unionid'] + userid = self.get_userid_by_unionid(unionid) + return userid + + def get_userid_by_unionid(self, unionid): + body = { + 'unionid': unionid + } + data = self._request.post(URL.GET_USERID_BY_UNIONID, json=body, with_token=True) + userid = data['result']['userid'] + return userid + + def send_by_template(self, template_id, user_ids, dept_ids, data): + body = { + 'agent_id': self._agentid, + 'template_id': template_id, + 'userid_list': ','.join(user_ids), + 'dept_id_list': ','.join(dept_ids), + 'data': data + } + data = self._request.post(URL.SEND_MESSAGE_BY_TEMPLATE, json=body, with_token=True) + + def send_text(self, user_ids, msg): + body = { + 'agent_id': self._agentid, + 'userid_list': ','.join(user_ids), + # 'dept_id_list': '', + 'to_all_user': False, + 'msg': { + 'msgtype': 'text', + 'text': { + 'content': msg + } + } + } + data = self._request.post(URL.SEND_MESSAGE, json=body, with_token=True) + return data + + def get_send_msg_progress(self, task_id): + body = { + 'agent_id': self._agentid, + 'task_id': task_id + } + + data = self._request.post(URL.GET_SEND_MSG_PROGRESS, json=body, with_token=True) + return data diff --git a/apps/common/message/backends/exceptions.py b/apps/common/message/backends/exceptions.py new file mode 100644 index 000000000..f72e8694d --- /dev/null +++ b/apps/common/message/backends/exceptions.py @@ -0,0 +1,28 @@ +from django.utils.translation import gettext_lazy as _ + +from rest_framework.exceptions import APIException + + +class HTTPNot200(APIException): + default_code = 'http_not_200' + default_detail = 'HTTP status is not 200' + + +class ErrCodeNot0(APIException): + default_code = 'errcode_not_0' + default_detail = 'Error code is not 0' + + +class ResponseDataKeyError(APIException): + default_code = 'response_data_key_error' + default_detail = 'Response data key error' + + +class NetError(APIException): + default_code = 'net_error' + default_detail = _('Network error, please contact system administrator') + + +class AccessTokenError(APIException): + default_code = 'access_token_error' + default_detail = 'Access token error, check config' diff --git a/apps/common/message/backends/mixin.py b/apps/common/message/backends/mixin.py new file mode 100644 index 000000000..3beb60272 --- /dev/null +++ b/apps/common/message/backends/mixin.py @@ -0,0 +1,94 @@ +import requests +from requests import exceptions as req_exce +from rest_framework.exceptions import PermissionDenied +from django.core.cache import cache + +from .utils import DictWrapper +from common.utils.common import get_logger +from common.utils import lazyproperty +from common.message.backends.utils import set_default + +from . import exceptions as exce + +logger = get_logger(__name__) + + +class RequestMixin: + def check_errcode_is_0(self, data: DictWrapper): + errcode = data['errcode'] + if errcode != 0: + # 如果代码写的对,配置没问题,这里不该出错,系统性错误,直接抛异常 + errmsg = data['errmsg'] + logger.error(f'Response 200 but errcode is not 0: ' + f'errcode={errcode} ' + f'errmsg={errmsg} ') + raise exce.ErrCodeNot0(detail=str(data.raw_data)) + + def check_http_is_200(self, response): + if response.status_code != 200: + # 正常情况下不会返回非 200 响应码 + logger.error(f'Response error: ' + f'status_code={response.status_code} ' + f'url={response.url}' + f'\ncontent={response.content}') + raise exce.HTTPNot200 + + +class BaseRequest(RequestMixin): + invalid_token_errcode = -1 + + def __init__(self, timeout=None): + self._request_kwargs = { + 'timeout': timeout + } + self.init_access_token() + + def request_access_token(self): + raise NotImplementedError + + def get_access_token_cache_key(self): + raise NotImplementedError + + def is_token_invalid(self, data): + errcode = data['errcode'] + if errcode == self.invalid_token_errcode: + return True + return False + + @lazyproperty + def access_token_cache_key(self): + return self.get_access_token_cache_key() + + def init_access_token(self): + access_token = cache.get(self.access_token_cache_key) + if access_token: + self.access_token = access_token + return + self.refresh_access_token() + + def refresh_access_token(self): + access_token, expires_in = self.request_access_token() + self.access_token = access_token + cache.set(self.access_token_cache_key, access_token, expires_in) + + def raw_request(self, method, url, **kwargs): + set_default(kwargs, self._request_kwargs) + raw_data = '' + for i in range(3): + # 循环为了防止 access_token 失效 + try: + response = getattr(requests, method)(url, **kwargs) + self.check_http_is_200(response) + raw_data = response.json() + data = DictWrapper(raw_data) + + if self.is_token_invalid(data): + self.refresh_access_token() + continue + + return data + except req_exce.ReadTimeout as e: + logger.exception(e) + raise exce.NetError + logger.error(f'Get access_token error, check config: url={url} data={raw_data}') + raise PermissionDenied(raw_data) diff --git a/apps/common/message/backends/utils.py b/apps/common/message/backends/utils.py new file mode 100644 index 000000000..6c6f2b593 --- /dev/null +++ b/apps/common/message/backends/utils.py @@ -0,0 +1,78 @@ +import hashlib +import inspect +from inspect import Parameter + +from common.utils.common import get_logger +from common.message.backends import exceptions as exce + +logger = get_logger(__name__) + + +def digest(corpid, corpsecret): + md5 = hashlib.md5() + md5.update(corpid.encode()) + md5.update(corpsecret.encode()) + digest = md5.hexdigest() + return digest + + +def update_values(default: dict, others: dict): + for key in default.keys(): + if key in others: + default[key] = others[key] + + +def set_default(data: dict, default: dict): + for key in default.keys(): + if key not in data: + data[key] = default[key] + + +class DictWrapper: + def __init__(self, data:dict): + self.raw_data = data + + def __getitem__(self, item): + # 网络请求返回的数据,不能完全信任,所以字典操作包在异常里 + try: + return self.raw_data[item] + except KeyError as e: + msg = f'Response 200 but get field from json error: error={e} data={self.raw_data}' + logger.error(msg) + raise exce.ResponseDataKeyError(detail=msg) + + def __getattr__(self, item): + return getattr(self.raw_data, item) + + def __contains__(self, item): + return item in self.raw_data + + def __str__(self): + return str(self.raw_data) + + def __repr__(self): + return str(self.raw_data) + + +def request(func): + def inner(*args, **kwargs): + signature = inspect.signature(func) + bound_args = signature.bind(*args, **kwargs) + bound_args.apply_defaults() + + arguments = bound_args.arguments + self = arguments['self'] + request_method = func.__name__ + + parameters = {} + for k, v in signature.parameters.items(): + if k == 'self': + continue + if v.kind is Parameter.VAR_KEYWORD: + parameters.update(arguments[k]) + continue + parameters[k] = arguments[k] + + response = self.request(request_method, **parameters) + return response + return inner diff --git a/apps/common/message/backends/wecom/__init__.py b/apps/common/message/backends/wecom/__init__.py new file mode 100644 index 000000000..257da47a0 --- /dev/null +++ b/apps/common/message/backends/wecom/__init__.py @@ -0,0 +1,194 @@ +from typing import Iterable, AnyStr + +from django.utils.translation import ugettext_lazy as _ +from rest_framework.exceptions import APIException +from requests.exceptions import ReadTimeout +import requests +from django.core.cache import cache + +from common.utils.common import get_logger +from common.message.backends.utils import digest, DictWrapper, update_values, set_default +from common.message.backends.utils import request +from common.message.backends.mixin import RequestMixin, BaseRequest + +logger = get_logger(__name__) + + +class WeComError(APIException): + default_code = 'wecom_error' + default_detail = _('WeCom error, please contact system administrator') + + +class URL: + GET_TOKEN = 'https://qyapi.weixin.qq.com/cgi-bin/gettoken' + SEND_MESSAGE = 'https://qyapi.weixin.qq.com/cgi-bin/message/send' + QR_CONNECT = 'https://open.work.weixin.qq.com/wwopen/sso/qrConnect' + + # https://open.work.weixin.qq.com/api/doc/90000/90135/91437 + GET_USER_ID_BY_CODE = 'https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo' + GET_USER_DETAIL = 'https://qyapi.weixin.qq.com/cgi-bin/user/get' + + +class ErrorCode: + # https://open.work.weixin.qq.com/api/doc/90000/90139/90313#%E9%94%99%E8%AF%AF%E7%A0%81%EF%BC%9A81013 + RECIPIENTS_INVALID = 81013 # UserID、部门ID、标签ID全部非法或无权限。 + + # https://open.work.weixin.qq.com/api/doc/90000/90135/91437 + INVALID_CODE = 40029 + + INVALID_TOKEN = 40014 # 无效的 access_token + + +class WeComRequests(BaseRequest): + """ + 处理系统级错误,抛出 API 异常,直接生成 HTTP 响应,业务代码无需关心这些错误 + - 确保 status_code == 200 + - 确保 access_token 无效时重试 + """ + invalid_token_errcode = ErrorCode.INVALID_TOKEN + + def __init__(self, corpid, corpsecret, agentid, timeout=None): + self._corpid = corpid + self._corpsecret = corpsecret + self._agentid = agentid + + super().__init__(timeout=timeout) + + def get_access_token_cache_key(self): + return digest(self._corpid, self._corpsecret) + + def request_access_token(self): + params = {'corpid': self._corpid, 'corpsecret': self._corpsecret} + data = self.raw_request('get', url=URL.GET_TOKEN, params=params) + + access_token = data['access_token'] + expires_in = data['expires_in'] + return access_token, expires_in + + @request + def get(self, url, params=None, with_token=True, + check_errcode_is_0=True, **kwargs): + # self.request ... + pass + + @request + def post(self, url, params=None, json=None, + with_token=True, check_errcode_is_0=True, + **kwargs): + # self.request ... + pass + + def request(self, method, url, + params=None, + with_token=True, + check_errcode_is_0=True, + **kwargs): + + if not isinstance(params, dict): + params = {} + + if with_token: + params['access_token'] = self.access_token + + data = self.raw_request(method, url, params=params, **kwargs) + if check_errcode_is_0: + self.check_errcode_is_0(data) + return data + + +class WeCom(RequestMixin): + """ + 非业务数据导致的错误直接抛异常,说明是系统配置错误,业务代码不用理会 + """ + + def __init__(self, corpid, corpsecret, agentid, timeout=None): + self._corpid = corpid + self._corpsecret = corpsecret + self._agentid = agentid + + self._requests = WeComRequests( + corpid=corpid, + corpsecret=corpsecret, + agentid=agentid, + timeout=timeout + ) + + def send_text(self, users: Iterable, msg: AnyStr, **kwargs): + """ + https://open.work.weixin.qq.com/api/doc/90000/90135/90236 + + 对于业务代码,只需要关心由 用户id 或 消息不对 导致的错误,其他错误不予理会 + """ + users = tuple(users) + + extra_params = { + "safe": 0, + "enable_id_trans": 0, + "enable_duplicate_check": 0, + "duplicate_check_interval": 1800 + } + update_values(extra_params, kwargs) + + body = { + "touser": '|'.join(users), + "msgtype": "text", + "agentid": self._agentid, + "text": { + "content": msg + }, + **extra_params + } + data = self._requests.post(URL.SEND_MESSAGE, json=body, check_errcode_is_0=False) + + errcode = data['errcode'] + if errcode == ErrorCode.RECIPIENTS_INVALID: + # 全部接收人无权限或不存在 + return users + self.check_errcode_is_0(data) + + invaliduser = data['invaliduser'] + if not invaliduser: + return () + + if isinstance(invaliduser, str): + logger.error(f'WeCom send text 200, but invaliduser is not str: invaliduser={invaliduser}') + raise WeComError + + invalid_users = invaliduser.split('|') + return invalid_users + + def get_user_id_by_code(self, code): + # # https://open.work.weixin.qq.com/api/doc/90000/90135/91437 + + params = { + 'code': code, + } + data = self._requests.get(URL.GET_USER_ID_BY_CODE, params=params, check_errcode_is_0=False) + + errcode = data['errcode'] + if errcode == ErrorCode.INVALID_CODE: + logger.warn(f'WeCom get_user_id_by_code invalid code: code={code}') + return None, None + + self.check_errcode_is_0(data) + + USER_ID = 'UserId' + OPEN_ID = 'OpenId' + + if USER_ID in data: + return data[USER_ID], USER_ID + elif OPEN_ID in data: + return data[OPEN_ID], OPEN_ID + else: + logger.error(f'WeCom response 200 but get field from json error: fields=UserId|OpenId') + raise WeComError + + def get_user_detail(self, id): + # https://open.work.weixin.qq.com/api/doc/90000/90135/90196 + + params = { + 'userid': id, + } + + data = self._requests.get(URL.GET_USER_DETAIL, params) + return data diff --git a/apps/common/mixins/api.py b/apps/common/mixins/api.py index ea629bb84..a0d5875c6 100644 --- a/apps/common/mixins/api.py +++ b/apps/common/mixins/api.py @@ -6,10 +6,12 @@ from threading import Thread from collections import defaultdict from itertools import chain +from django.conf import settings from django.db.models.signals import m2m_changed from django.core.cache import cache from django.http import JsonResponse from django.utils.translation import ugettext as _ +from django.contrib.auth import get_user_model from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework.decorators import action @@ -25,6 +27,9 @@ __all__ = [ ] +UserModel = get_user_model() + + class JSONResponseMixin(object): """JSON mixin""" @staticmethod @@ -332,3 +337,21 @@ class AllowBulkDestoryMixin: """ query = str(filtered.query) return '`id` IN (' in query or '`id` =' in query + + +class RoleAdminMixin: + kwargs: dict + user_id_url_kwarg = 'pk' + + @lazyproperty + def user(self): + user_id = self.kwargs.get(self.user_id_url_kwarg) + return UserModel.objects.get(id=user_id) + + +class RoleUserMixin: + request: Request + + @lazyproperty + def user(self): + return self.request.user diff --git a/apps/common/request_log.py b/apps/common/request_log.py new file mode 100644 index 000000000..c35e6b84a --- /dev/null +++ b/apps/common/request_log.py @@ -0,0 +1,15 @@ +from django.http.request import HttpRequest +from django.http.response import HttpResponse + +from orgs.utils import current_org + + +class RequestLogMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request: HttpRequest): + print(f'Request {request.method} --> ', request.get_raw_uri()) + response: HttpResponse = self.get_response(request) + print(f'Response {current_org.name} {request.method} {response.status_code} --> ', request.get_raw_uri()) + return response diff --git a/apps/common/utils/random.py b/apps/common/utils/random.py index 055966947..a9ef0421f 100644 --- a/apps/common/utils/random.py +++ b/apps/common/utils/random.py @@ -4,7 +4,6 @@ import struct import random import socket import string -import secrets string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_{}~' diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index b51c7dfba..57c754942 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -216,6 +216,16 @@ class Config(dict): 'AUTH_SSO': False, 'AUTH_SSO_AUTHKEY_TTL': 60 * 15, + 'AUTH_WECOM': False, + 'WECOM_CORPID': '', + 'WECOM_AGENTID': '', + 'WECOM_CORPSECRET': '', + + 'AUTH_DINGTALK': False, + 'DINGTALK_AGENTID': '', + 'DINGTALK_APPKEY': '', + 'DINGTALK_APPSECRET': '', + 'OTP_VALID_WINDOW': 2, 'OTP_ISSUER_NAME': 'JumpServer', 'EMAIL_SUFFIX': 'jumpserver.org', diff --git a/apps/jumpserver/context_processor.py b/apps/jumpserver/context_processor.py index ad8c8c90f..fbd7016d3 100644 --- a/apps/jumpserver/context_processor.py +++ b/apps/jumpserver/context_processor.py @@ -14,6 +14,8 @@ def jumpserver_processor(request): 'LOGIN_IMAGE_URL': static('img/login_image.png'), 'FAVICON_URL': static('img/facio.ico'), 'LOGIN_CAS_LOGO_URL': static('img/login_cas_logo.png'), + 'LOGIN_WECOM_LOGO_URL': static('img/login_wecom_log.png'), + 'LOGIN_DINGTALK_LOGO_URL': static('img/login_dingtalk_log.png'), 'JMS_TITLE': _('JumpServer Open Source Bastion Host'), 'VERSION': settings.VERSION, 'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2021', diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index 579a0e66f..4fb77cb47 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -101,6 +101,19 @@ CAS_CHECK_NEXT = lambda _next_page: True AUTH_SSO = CONFIG.AUTH_SSO AUTH_SSO_AUTHKEY_TTL = CONFIG.AUTH_SSO_AUTHKEY_TTL +# WECOM Auth +AUTH_WECOM = CONFIG.AUTH_WECOM +WECOM_CORPID = CONFIG.WECOM_CORPID +WECOM_AGENTID = CONFIG.WECOM_AGENTID +WECOM_CORPSECRET = CONFIG.WECOM_CORPSECRET + +# DingDing auth +AUTH_DINGTALK = CONFIG.AUTH_DINGTALK +DINGTALK_AGENTID = CONFIG.DINGTALK_AGENTID +DINGTALK_APPKEY = CONFIG.DINGTALK_APPKEY +DINGTALK_APPSECRET = CONFIG.DINGTALK_APPSECRET + + # Other setting TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION LOGIN_CONFIRM_ENABLE = CONFIG.LOGIN_CONFIRM_ENABLE @@ -115,6 +128,8 @@ AUTH_BACKEND_OIDC_CODE = 'jms_oidc_rp.backends.OIDCAuthCodeBackend' AUTH_BACKEND_RADIUS = 'authentication.backends.radius.RadiusBackend' AUTH_BACKEND_CAS = 'authentication.backends.cas.CASBackend' AUTH_BACKEND_SSO = 'authentication.backends.api.SSOAuthentication' +AUTH_BACKEND_WECOM = 'authentication.backends.api.WeComAuthentication' +AUTH_BACKEND_DINGTALK = 'authentication.backends.api.DingTalkAuthentication' AUTHENTICATION_BACKENDS = [AUTH_BACKEND_MODEL, AUTH_BACKEND_PUBKEY] @@ -128,6 +143,10 @@ if AUTH_RADIUS: AUTHENTICATION_BACKENDS.insert(0, AUTH_BACKEND_RADIUS) if AUTH_SSO: AUTHENTICATION_BACKENDS.append(AUTH_BACKEND_SSO) +if AUTH_WECOM: + AUTHENTICATION_BACKENDS.append(AUTH_BACKEND_WECOM) +if AUTH_DINGTALK: + AUTHENTICATION_BACKENDS.append(AUTH_BACKEND_DINGTALK) ONLY_ALLOW_EXIST_USER_AUTH = CONFIG.ONLY_ALLOW_EXIST_USER_AUTH diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index ca9870043..687b7f2ae 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -23,7 +23,7 @@ api_v1 = [ path('applications/', include('applications.urls.api_urls', namespace='api-applications')), path('tickets/', include('tickets.urls.api_urls', namespace='api-tickets')), path('acls/', include('acls.urls.api_urls', namespace='api-acls')), - path('prometheus/metrics/', api.PrometheusMetricsApi.as_view()) + path('prometheus/metrics/', api.PrometheusMetricsApi.as_view()), ] app_view_patterns = [ diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 2bc2e78e23f4cc398283cac0b6957f9ae0142639..c7bd94308a579bcb334dd636c31e63416167f69f 100644 GIT binary patch delta 23230 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\n" "Language-Team: JumpServer team\n" @@ -93,13 +93,13 @@ msgstr "动作" #: acls/models/login_acl.py:28 acls/models/login_asset_acl.py:20 #: acls/serializers/login_acl.py:33 assets/models/label.py:15 #: audits/models.py:36 audits/models.py:56 audits/models.py:69 -#: audits/serializers.py:84 authentication/models.py:44 +#: audits/serializers.py:94 authentication/models.py:44 #: authentication/models.py:97 orgs/models.py:18 orgs/models.py:418 #: perms/models/base.py:50 templates/index.html:78 #: terminal/backends/command/models.py:18 #: terminal/backends/command/serializers.py:12 terminal/models/session.py:38 #: tickets/models/comment.py:17 users/models/user.py:184 -#: users/models/user.py:733 users/models/user.py:759 +#: users/models/user.py:743 users/models/user.py:769 #: users/serializers/group.py:20 #: users/templates/users/user_asset_permission.html:38 #: users/templates/users/user_asset_permission.html:64 @@ -120,9 +120,9 @@ msgstr "系统用户" #: acls/models/login_asset_acl.py:22 #: applications/serializers/attrs/application_category/remote_app.py:33 #: assets/models/asset.py:355 assets/models/authbook.py:26 -#: assets/models/gathered_user.py:14 assets/serializers/admin_user.py:30 -#: assets/serializers/asset_user.py:47 assets/serializers/asset_user.py:84 -#: assets/serializers/system_user.py:192 audits/models.py:38 +#: assets/models/gathered_user.py:14 assets/serializers/admin_user.py:34 +#: assets/serializers/asset_user.py:48 assets/serializers/asset_user.py:89 +#: assets/serializers/system_user.py:195 audits/models.py:38 #: perms/models/asset_permission.py:99 templates/index.html:82 #: terminal/backends/command/models.py:19 #: terminal/backends/command/serializers.py:13 terminal/models/session.py:40 @@ -158,13 +158,13 @@ msgstr "" #: acls/serializers/login_acl.py:30 acls/serializers/login_asset_acl.py:31 #: applications/serializers/attrs/application_type/mysql_workbench.py:18 #: assets/models/asset.py:183 assets/models/domain.py:52 -#: assets/serializers/asset_user.py:46 settings/serializers/settings.py:112 +#: assets/serializers/asset_user.py:47 settings/serializers/settings.py:112 #: users/templates/users/_granted_assets.html:26 #: users/templates/users/user_asset_permission.html:156 msgid "IP" msgstr "IP" -#: acls/serializers/login_acl.py:50 +#: acls/serializers/login_acl.py:55 msgid "The user `{}` is not in the current organization: `{}`" msgstr "用户 `{}` 不在当前组织: `{}`" @@ -198,7 +198,7 @@ msgstr "" "10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 (支持网域)" #: acls/serializers/login_asset_acl.py:35 assets/models/asset.py:184 -#: assets/serializers/asset_user.py:45 assets/serializers/gathered_user.py:20 +#: assets/serializers/asset_user.py:46 assets/serializers/gathered_user.py:23 #: settings/serializers/settings.py:111 #: users/templates/users/_granted_assets.html:25 #: users/templates/users/user_asset_permission.html:157 @@ -213,7 +213,7 @@ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. 可选的协议 #: acls/serializers/login_asset_acl.py:55 assets/models/asset.py:187 #: assets/models/domain.py:54 assets/models/user.py:123 -#: terminal/serializers/session.py:29 terminal/serializers/storage.py:69 +#: terminal/serializers/session.py:32 terminal/serializers/storage.py:69 msgid "Protocol" msgstr "协议" @@ -221,12 +221,12 @@ msgstr "协议" msgid "Unsupported protocols: {}" msgstr "不支持的协议: {}" -#: acls/serializers/login_asset_acl.py:94 -#: tickets/serializers/ticket/ticket.py:109 +#: acls/serializers/login_asset_acl.py:98 +#: tickets/serializers/ticket/ticket.py:111 msgid "The organization `{}` does not exist" msgstr "组织 `{}` 不存在" -#: acls/serializers/login_asset_acl.py:99 +#: acls/serializers/login_asset_acl.py:103 msgid "None of the reviewers belong to Organization `{}`" msgstr "所有复核人都不属于组织 `{}`" @@ -315,9 +315,9 @@ msgstr "目标URL" #: applications/serializers/attrs/application_type/custom.py:25 #: applications/serializers/attrs/application_type/mysql_workbench.py:34 #: applications/serializers/attrs/application_type/vmware_client.py:30 -#: assets/models/base.py:252 assets/serializers/asset_user.py:71 +#: assets/models/base.py:252 assets/serializers/asset_user.py:76 #: audits/signals_handler.py:58 authentication/forms.py:22 -#: authentication/templates/authentication/login.html:155 +#: authentication/templates/authentication/login.html:164 #: settings/serializers/settings.py:93 users/forms/profile.py:21 #: users/templates/users/user_otp_check_password.html:13 #: users/templates/users/user_password_update.html:43 @@ -354,10 +354,8 @@ msgid "You can't delete the root node ({})" msgstr "不能删除根节点 ({})" #: assets/api/node.py:75 -#, fuzzy -#| msgid "Deletion failed and the node contains children or assets" msgid "Deletion failed and the node contains assets" -msgstr "删除失败,节点包含子节点或资产" +msgstr "删除失败,节点包含资产" #: assets/backends/db.py:244 msgid "Could not remove asset admin user" @@ -499,7 +497,7 @@ msgstr "创建者" #: assets/models/label.py:25 common/db/models.py:72 common/mixins/models.py:50 #: ops/models/adhoc.py:38 ops/models/command.py:29 orgs/models.py:25 #: orgs/models.py:420 perms/models/base.py:56 users/models/group.py:18 -#: users/models/user.py:760 xpack/plugins/cloud/models.py:107 +#: users/models/user.py:770 xpack/plugins/cloud/models.py:107 msgid "Date created" msgstr "创建日期" @@ -571,7 +569,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:745 +#: users/models/user.py:755 msgid "System" msgstr "系统" @@ -676,7 +674,7 @@ msgstr "全称" msgid "Parent key" msgstr "ssh私钥" -#: assets/models/node.py:559 assets/serializers/system_user.py:191 +#: assets/models/node.py:559 assets/serializers/system_user.py:194 #: users/templates/users/user_asset_permission.html:41 #: users/templates/users/user_asset_permission.html:73 #: users/templates/users/user_asset_permission.html:158 @@ -789,7 +787,7 @@ msgstr "网域名称" msgid "Admin user name" msgstr "管理用户名称" -#: assets/serializers/asset.py:71 perms/serializers/asset/permission.py:47 +#: assets/serializers/asset.py:71 perms/serializers/asset/permission.py:49 msgid "Nodes name" msgstr "节点名称" @@ -805,22 +803,22 @@ msgstr "组织名称" msgid "Connectivity" msgstr "连接" -#: assets/serializers/asset_user.py:44 +#: assets/serializers/asset_user.py:45 #: authentication/templates/authentication/_access_key_modal.html:30 #: users/serializers/group.py:37 msgid "ID" msgstr "ID" -#: assets/serializers/asset_user.py:48 +#: assets/serializers/asset_user.py:49 msgid "Backend" msgstr "后端" -#: assets/serializers/asset_user.py:75 users/forms/profile.py:160 +#: assets/serializers/asset_user.py:80 users/forms/profile.py:160 #: users/models/user.py:585 users/templates/users/user_password_update.html:48 msgid "Public key" msgstr "SSH公钥" -#: assets/serializers/asset_user.py:79 users/models/user.py:582 +#: assets/serializers/asset_user.py:84 users/models/user.py:582 msgid "Private key" msgstr "ssh私钥" @@ -840,9 +838,9 @@ msgstr "应用数量" msgid "Gateways count" msgstr "网关数量" -#: assets/serializers/label.py:13 assets/serializers/system_user.py:45 -#: assets/serializers/system_user.py:166 -#: perms/serializers/asset/permission.py:72 +#: assets/serializers/label.py:13 assets/serializers/system_user.py:47 +#: assets/serializers/system_user.py:169 +#: perms/serializers/asset/permission.py:74 msgid "Assets amount" msgstr "资产数量" @@ -863,33 +861,33 @@ msgstr "不能包含: /" msgid "The same level node name cannot be the same" msgstr "同级别节点名字不能重复" -#: assets/serializers/system_user.py:44 assets/serializers/system_user.py:165 -#: perms/serializers/asset/permission.py:73 +#: assets/serializers/system_user.py:46 assets/serializers/system_user.py:168 +#: perms/serializers/asset/permission.py:75 msgid "Nodes amount" msgstr "节点数量" -#: assets/serializers/system_user.py:46 assets/serializers/system_user.py:167 -#: assets/serializers/system_user.py:193 +#: assets/serializers/system_user.py:48 assets/serializers/system_user.py:170 +#: assets/serializers/system_user.py:196 msgid "Login mode display" msgstr "登录模式(显示名称)" -#: assets/serializers/system_user.py:48 assets/serializers/system_user.py:169 +#: assets/serializers/system_user.py:50 assets/serializers/system_user.py:172 msgid "Ad domain" msgstr "Ad 网域" -#: assets/serializers/system_user.py:87 +#: assets/serializers/system_user.py:89 msgid "Username same with user with protocol {} only allow 1" msgstr "用户名和用户相同的一种协议只允许存在一个" -#: assets/serializers/system_user.py:100 +#: assets/serializers/system_user.py:102 msgid "* Automatic login mode must fill in the username." msgstr "自动登录模式,必须填写用户名" -#: assets/serializers/system_user.py:108 +#: assets/serializers/system_user.py:110 msgid "Path should starts with /" msgstr "路径应该以 / 开头" -#: assets/serializers/system_user.py:119 +#: assets/serializers/system_user.py:121 msgid "Password or private key required" msgstr "密码或密钥密码需要一个" @@ -1122,7 +1120,7 @@ msgstr "登录IP" msgid "Login city" msgstr "登录城市" -#: audits/models.py:104 audits/serializers.py:38 +#: audits/models.py:104 audits/serializers.py:45 msgid "User agent" msgstr "用户代理" @@ -1156,45 +1154,45 @@ msgstr "认证方式" msgid "Operate for display" msgstr "操作(显示名称)" -#: audits/serializers.py:26 +#: audits/serializers.py:30 msgid "Type for display" msgstr "类型(显示名称)" -#: audits/serializers.py:27 +#: audits/serializers.py:31 msgid "Status for display" msgstr "状态(显示名称)" -#: audits/serializers.py:28 +#: audits/serializers.py:32 msgid "MFA for display" msgstr "多因子认证状态(显示名称)" -#: audits/serializers.py:66 audits/serializers.py:81 ops/models/adhoc.py:247 -#: terminal/serializers/session.py:34 +#: audits/serializers.py:76 audits/serializers.py:91 ops/models/adhoc.py:247 +#: terminal/serializers/session.py:37 msgid "Is success" msgstr "是否成功" -#: audits/serializers.py:68 +#: audits/serializers.py:78 msgid "Hosts for display" msgstr "主机 (显示名称)" -#: audits/serializers.py:80 ops/models/command.py:26 +#: audits/serializers.py:90 ops/models/command.py:26 #: xpack/plugins/cloud/models.py:155 msgid "Result" msgstr "结果" -#: audits/serializers.py:82 terminal/serializers/storage.py:178 +#: audits/serializers.py:92 terminal/serializers/storage.py:178 msgid "Hosts" msgstr "主机" -#: audits/serializers.py:83 +#: audits/serializers.py:93 msgid "Run as" msgstr "运行用户" -#: audits/serializers.py:85 +#: audits/serializers.py:95 msgid "Run as for display" msgstr "运行用户(显示名称)" -#: audits/serializers.py:86 +#: audits/serializers.py:96 msgid "User for display" msgstr "用户(显示名称)" @@ -1206,6 +1204,12 @@ msgstr "SSH 密钥" msgid "SSO" msgstr "" +#: audits/signals_handler.py:60 +#: authentication/templates/authentication/login.html:210 +#: notifications/models.py:16 +msgid "WeCom" +msgstr "企业微信" + #: authentication/api/mfa.py:60 msgid "Code is invalid" msgstr "Code无效" @@ -1261,55 +1265,59 @@ msgstr "" msgid "Invalid token or cache refreshed." msgstr "" -#: authentication/errors.py:24 +#: authentication/errors.py:25 msgid "Username/password check failed" msgstr "用户名/密码 校验失败" -#: authentication/errors.py:25 +#: authentication/errors.py:26 msgid "Password decrypt failed" msgstr "密码解密失败" -#: authentication/errors.py:26 +#: authentication/errors.py:27 msgid "MFA failed" msgstr "多因子认证失败" -#: authentication/errors.py:27 +#: authentication/errors.py:28 msgid "MFA unset" msgstr "多因子认证没有设定" -#: authentication/errors.py:28 +#: authentication/errors.py:29 msgid "Username does not exist" msgstr "用户名不存在" -#: authentication/errors.py:29 +#: authentication/errors.py:30 msgid "Password expired" msgstr "密码已过期" -#: authentication/errors.py:30 +#: authentication/errors.py:31 msgid "Disabled or expired" msgstr "禁用或失效" -#: authentication/errors.py:31 +#: authentication/errors.py:32 msgid "This account is inactive." msgstr "此账户已禁用" -#: authentication/errors.py:32 +#: authentication/errors.py:33 msgid "This account is expired" msgstr "此账户已过期" -#: authentication/errors.py:33 +#: authentication/errors.py:34 msgid "Auth backend not match" msgstr "没有匹配到认证后端" -#: authentication/errors.py:34 +#: authentication/errors.py:35 msgid "ACL is not allowed" msgstr "ACL 不被允许" -#: authentication/errors.py:44 +#: authentication/errors.py:36 +msgid "Wecom login only for local user" +msgstr "" + +#: authentication/errors.py:46 msgid "No session found, check your cookie" msgstr "会话已变更,刷新页面" -#: authentication/errors.py:46 +#: authentication/errors.py:48 #, python-brace-format msgid "" "The username or password you entered is incorrect, please enter it again. " @@ -1319,13 +1327,13 @@ msgstr "" "您输入的用户名或密码不正确,请重新输入。 您还可以尝试 {times_try} 次(账号将" "被临时 锁定 {block_time} 分钟)" -#: authentication/errors.py:52 authentication/errors.py:56 +#: authentication/errors.py:54 authentication/errors.py:58 msgid "" "The account has been locked (please contact admin to unlock it or try again " "after {} minutes)" msgstr "账号已被锁定(请联系管理员解锁 或 {}分钟后重试)" -#: authentication/errors.py:60 +#: authentication/errors.py:62 #, python-brace-format msgid "" "MFA code invalid, or ntp sync server time, You can also try {times_try} " @@ -1334,46 +1342,50 @@ msgstr "" "MFA验证码不正确,或者服务器端时间不对。 您还可以尝试 {times_try} 次(账号将被" "临时 锁定 {block_time} 分钟)" -#: authentication/errors.py:65 +#: authentication/errors.py:67 msgid "MFA required" msgstr "需要多因子认证" -#: authentication/errors.py:66 +#: authentication/errors.py:68 msgid "MFA not set, please set it first" msgstr "多因子认证没有设置,请先完成设置" -#: authentication/errors.py:67 +#: authentication/errors.py:69 msgid "Login confirm required" msgstr "需要登录复核" -#: authentication/errors.py:68 +#: authentication/errors.py:70 msgid "Wait login confirm ticket for accept" msgstr "等待登录复核处理" -#: authentication/errors.py:69 +#: authentication/errors.py:71 msgid "Login confirm ticket was {}" msgstr "登录复核 {}" -#: authentication/errors.py:233 +#: authentication/errors.py:235 msgid "IP is not allowed" msgstr "来源 IP 不被允许登录" -#: authentication/errors.py:266 +#: authentication/errors.py:268 msgid "SSO auth closed" msgstr "SSO 认证关闭了" -#: authentication/errors.py:271 authentication/views/login.py:267 +#: authentication/errors.py:273 authentication/views/login.py:227 msgid "Your password is too simple, please change it for security" msgstr "你的密码过于简单,为了安全,请修改" -#: authentication/errors.py:280 authentication/views/login.py:282 +#: authentication/errors.py:282 authentication/views/login.py:242 msgid "You should to change your password before login" msgstr "登录完成前,请先修改密码" -#: authentication/errors.py:289 authentication/views/login.py:298 +#: authentication/errors.py:291 authentication/views/login.py:258 msgid "Your password has expired, please reset before logging in" msgstr "您的密码已过期,先修改再登录" +#: authentication/errors.py:320 +msgid "Your password is invalid" +msgstr "您的密码无效" + #: authentication/forms.py:26 msgid "{} days auto login" msgstr "{} 天内自动登录" @@ -1455,9 +1467,8 @@ msgid "Need MFA for view auth" msgstr "需要多因子认证来查看账号信息" #: authentication/templates/authentication/_mfa_confirm_modal.html:20 -#: authentication/views/login.py:286 authentication/views/login.py:302 -#: authentication/views/login.py:318 templates/_modal.html:23 -#: users/templates/users/user_password_verify.html:20 +#: authentication/views/login.py:246 authentication/views/login.py:262 +#: templates/_modal.html:23 users/templates/users/user_password_verify.html:20 msgid "Confirm" msgstr "确认" @@ -1465,33 +1476,38 @@ msgstr "确认" msgid "Code error" msgstr "代码错误" -#: authentication/templates/authentication/login.html:148 +#: authentication/templates/authentication/login.html:157 msgid "Welcome back, please enter username and password to login" msgstr "欢迎回来,请输入用户名和密码登录" -#: authentication/templates/authentication/login.html:174 +#: authentication/templates/authentication/login.html:183 #: users/templates/users/forgot_password.html:15 #: users/templates/users/forgot_password.html:16 msgid "Forgot password" msgstr "忘记密码" -#: authentication/templates/authentication/login.html:181 +#: authentication/templates/authentication/login.html:190 #: templates/_header_bar.html:83 msgid "Login" msgstr "登录" -#: authentication/templates/authentication/login.html:188 +#: authentication/templates/authentication/login.html:197 msgid "More login options" msgstr "更多登录方式" -#: authentication/templates/authentication/login.html:191 +#: authentication/templates/authentication/login.html:200 msgid "OpenID" msgstr "OpenID" -#: authentication/templates/authentication/login.html:196 +#: authentication/templates/authentication/login.html:205 msgid "CAS" msgstr "" +#: authentication/templates/authentication/login.html:215 +#: notifications/models.py:18 +msgid "DingTalk" +msgstr "钉钉" + #: authentication/templates/authentication/login_otp.html:17 msgid "One-time password" msgstr "一次性密码" @@ -1522,6 +1538,7 @@ msgid "Copy link" msgstr "复制链接" #: authentication/templates/authentication/login_wait_confirm.html:51 +#: templates/flash_message_standalone.html:38 msgid "Return" msgstr "返回" @@ -1529,19 +1546,52 @@ msgstr "返回" msgid "Copy success" msgstr "复制成功" -#: authentication/views/login.py:63 authentication/views/login.py:313 -msgid "Redirecting" -msgstr "跳转中" +#: authentication/views/dingtalk.py:40 authentication/views/wecom.py:40 +msgid "You've been hacked" +msgstr "" -#: authentication/views/login.py:64 -msgid "Redirecting to {} authentication" -msgstr "正在跳转到 {} 认证" +#: authentication/views/dingtalk.py:76 +msgid "DingTalk is already bound" +msgstr "" -#: authentication/views/login.py:88 +#: authentication/views/dingtalk.py:89 authentication/views/wecom.py:88 +msgid "Please verify your password first" +msgstr "请检查密码" + +#: authentication/views/dingtalk.py:113 authentication/views/wecom.py:112 +msgid "Invalid user_id" +msgstr "无效的 user_id" + +#: authentication/views/dingtalk.py:129 +msgid "DingTalk query user failed" +msgstr "" + +#: authentication/views/dingtalk.py:136 authentication/views/dingtalk.py:219 +#: authentication/views/dingtalk.py:220 +msgid "Binding DingTalk successfully" +msgstr "绑定 钉钉 成功" + +#: authentication/views/dingtalk.py:188 +msgid "Failed to get user from DingTalk" +msgstr "" + +#: authentication/views/dingtalk.py:194 +msgid "DingTalk is not bound" +msgstr "" + +#: authentication/views/dingtalk.py:195 authentication/views/wecom.py:193 +msgid "Please login with a password and then bind the WoCom" +msgstr "" + +#: authentication/views/dingtalk.py:237 authentication/views/dingtalk.py:238 +msgid "Binding DingTalk failed" +msgstr "" + +#: authentication/views/login.py:55 msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: authentication/views/login.py:213 +#: authentication/views/login.py:173 msgid "" "Wait for {} confirm, You also can copy link to her/him
\n" " Don't close this page" @@ -1549,27 +1599,48 @@ msgstr "" "等待 {} 确认, 你也可以复制链接发给他/她
\n" " 不要关闭本页面" -#: authentication/views/login.py:218 +#: authentication/views/login.py:178 msgid "No ticket found" msgstr "没有发现工单" -#: authentication/views/login.py:250 +#: authentication/views/login.py:210 msgid "Logout success" msgstr "退出登录成功" -#: authentication/views/login.py:251 +#: authentication/views/login.py:211 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" -#: authentication/views/login.py:266 authentication/views/login.py:281 -#: authentication/views/login.py:297 +#: authentication/views/login.py:226 authentication/views/login.py:241 +#: authentication/views/login.py:257 msgid "Please change your password" msgstr "请修改密码" -#: authentication/views/login.py:314 -msgid "Redirect to third party auth" +#: authentication/views/wecom.py:75 +msgid "WeCom is already bound" msgstr "" +#: authentication/views/wecom.py:127 +msgid "WeCom query user failed" +msgstr "" + +#: authentication/views/wecom.py:134 authentication/views/wecom.py:217 +#: authentication/views/wecom.py:218 +msgid "Binding WeCom successfully" +msgstr "绑定 企业微信 成功" + +#: authentication/views/wecom.py:186 +msgid "Failed to get user from WeCom" +msgstr "" + +#: authentication/views/wecom.py:192 +msgid "WeCom is not bound" +msgstr "没有绑定企业微信" + +#: authentication/views/wecom.py:235 authentication/views/wecom.py:236 +msgid "Binding WeCom failed" +msgstr "绑定企业微信失败" + #: common/const/__init__.py:6 #, python-format msgid "%(name)s was created successfully" @@ -1649,7 +1720,15 @@ msgstr "" msgid "Encrypt field using Secret Key" msgstr "" -#: common/mixins/api.py:52 +#: common/message/backends/exceptions.py:23 +msgid "Network error, please contact system administrator" +msgstr "网络错误,请联系系统管理员" + +#: common/message/backends/wecom/__init__.py:19 +msgid "WeCom error, please contact system administrator" +msgstr "企业微信错误,请联系系统管理员" + +#: common/mixins/api.py:57 msgid "Request file format may be wrong" msgstr "上传的文件格式错误 或 其它类型资源的文件" @@ -1677,7 +1756,7 @@ msgstr "字段必须唯一" msgid "Should not contains special characters" msgstr "不能包含特殊字符" -#: jumpserver/context_processor.py:17 +#: jumpserver/context_processor.py:19 msgid "JumpServer Open Source Bastion Host" msgstr "JumpServer 开源堡垒机" @@ -1685,7 +1764,7 @@ msgstr "JumpServer 开源堡垒机" msgid "

Flow service unavailable, check it

" msgstr "" -#: jumpserver/views/other.py:25 +#: jumpserver/views/other.py:27 msgid "" "
Luna is a separately deployed program, you need to deploy Luna, koko, " "configure nginx for url distribution,
If you see this page, " @@ -1694,11 +1773,11 @@ msgstr "" "
Luna是单独部署的一个程序,你需要部署luna,koko,
如果你看到了" "这个页面,证明你访问的不是nginx监听的端口,祝你好运
" -#: jumpserver/views/other.py:69 +#: jumpserver/views/other.py:71 msgid "Websocket server run on port: {}, you should proxy it on nginx" msgstr "Websocket 服务运行在端口: {}, 请检查nginx是否代理是否设置" -#: jumpserver/views/other.py:83 +#: jumpserver/views/other.py:85 msgid "" "
Koko is a separately deployed program, you need to deploy Koko, " "configure nginx for url distribution,
If you see this page, " @@ -1708,6 +1787,20 @@ msgstr "" "div>
如果你看到了这个页面,证明你访问的不是nginx监听的端口,祝你好运" +#: notifications/models.py:17 users/forms/profile.py:101 +#: users/models/user.py:557 +msgid "Email" +msgstr "邮件" + +#: notifications/models.py:67 templates/_nav.html:110 terminal/apps.py:9 +#: terminal/serializers/session.py:40 +msgid "Terminal" +msgstr "终端" + +#: notifications/models.py:68 ops/apps.py:9 +msgid "Operations" +msgstr "运维" + #: ops/api/celery.py:61 ops/api/celery.py:76 msgid "Waiting task start" msgstr "等待任务开始" @@ -1811,7 +1904,7 @@ msgid "Time" msgstr "时间" #: ops/models/adhoc.py:246 ops/models/command.py:28 -#: terminal/serializers/session.py:38 +#: terminal/serializers/session.py:41 msgid "Is finished" msgstr "是否完成" @@ -1843,10 +1936,18 @@ msgstr "任务开始" msgid "Command `{}` is forbidden ........" msgstr "命令 `{}` 不允许被执行 ......." -#: ops/models/command.py:113 +#: ops/models/command.py:115 msgid "Task end" msgstr "任务结束" +#: ops/notifications.py:10 +msgid "Server performance" +msgstr "服务器性能" + +#: ops/notifications.py:17 +msgid "Disk used more than 80%: {} => {}" +msgstr "磁盘使用率超过 80%: {} => {}" + #: ops/tasks.py:71 msgid "Clean task history period" msgstr "定期清除任务历史" @@ -1863,10 +1964,6 @@ msgstr "任务列表" msgid "Update task content: {}" msgstr "更新任务内容: {}" -#: ops/utils.py:74 -msgid "Disk used more than 80%: {} => {}" -msgstr "磁盘使用率超过 80%: {} => {}" - #: orgs/api.py:76 #, python-brace-format msgid "Have `{model._meta.verbose_name}` exists, Please delete" @@ -1877,8 +1974,8 @@ msgid "The current organization cannot be deleted" msgstr "当前组织不能被删除" #: orgs/mixins/models.py:45 orgs/mixins/serializers.py:25 orgs/models.py:36 -#: orgs/models.py:417 orgs/serializers.py:101 -#: tickets/serializers/ticket/ticket.py:81 +#: orgs/models.py:417 orgs/serializers.py:108 +#: tickets/serializers/ticket/ticket.py:83 msgid "Organization" msgstr "组织" @@ -1948,7 +2045,7 @@ msgid "Clipboard copy paste" msgstr "剪贴板复制粘贴" #: perms/models/asset_permission.py:102 -#: perms/serializers/asset/permission.py:69 +#: perms/serializers/asset/permission.py:71 msgid "Actions" msgstr "动作" @@ -1989,49 +2086,41 @@ msgid "" "permission type. ({})" msgstr "应用列表中包含与授权类型不同的应用。({})" -#: perms/serializers/asset/permission.py:43 -#: perms/serializers/asset/permission.py:67 users/serializers/user.py:34 -#: users/serializers/user.py:70 +#: perms/serializers/asset/permission.py:45 +#: perms/serializers/asset/permission.py:69 users/serializers/user.py:34 +#: users/serializers/user.py:82 msgid "Is expired" msgstr "是否过期" -#: perms/serializers/asset/permission.py:44 -#, fuzzy -#| msgid "Username" +#: perms/serializers/asset/permission.py:46 msgid "Users name" msgstr "用户名" -#: perms/serializers/asset/permission.py:45 -#, fuzzy -#| msgid "User groups amount" +#: perms/serializers/asset/permission.py:47 msgid "User groups name" msgstr "用户组数量" -#: perms/serializers/asset/permission.py:46 -#, fuzzy -#| msgid "Asset num" -msgid "Assets name" -msgstr "资产数量" - #: perms/serializers/asset/permission.py:48 -#, fuzzy -#| msgid "System users amount" -msgid "System users name" -msgstr "系统用户数量" +msgid "Assets name" +msgstr "资产名字" -#: perms/serializers/asset/permission.py:68 users/serializers/user.py:69 +#: perms/serializers/asset/permission.py:50 +msgid "System users name" +msgstr "系统用户名字" + +#: perms/serializers/asset/permission.py:70 users/serializers/user.py:81 msgid "Is valid" msgstr "账户是否有效" -#: perms/serializers/asset/permission.py:70 users/serializers/group.py:36 +#: perms/serializers/asset/permission.py:72 users/serializers/group.py:36 msgid "Users amount" msgstr "用户数量" -#: perms/serializers/asset/permission.py:71 +#: perms/serializers/asset/permission.py:73 msgid "User groups amount" msgstr "用户组数量" -#: perms/serializers/asset/permission.py:74 +#: perms/serializers/asset/permission.py:76 msgid "System users amount" msgstr "系统用户数量" @@ -2044,6 +2133,10 @@ msgstr "邮件已经发送{}, 请检查" msgid "Welcome to the JumpServer open source Bastion Host" msgstr "欢迎使用JumpServer开源堡垒机" +#: settings/api/dingtalk.py:36 settings/api/wecom.py:36 +msgid "OK" +msgstr "" + #: settings/api/ldap.py:189 msgid "Get ldap users is None" msgstr "获取 LDAP 用户为 None" @@ -2379,6 +2472,38 @@ msgstr "邮件收件人" msgid "Multiple user using , split" msgstr "多个用户,使用 , 分割" +#: settings/serializers/settings.py:193 +msgid "Corporation ID" +msgstr "企业 ID(CorpId)" + +#: settings/serializers/settings.py:194 +msgid "Agent ID" +msgstr "应用 ID(AgentId)" + +#: settings/serializers/settings.py:195 +msgid "Corporation Secret" +msgstr "凭证密钥(Secret)" + +#: settings/serializers/settings.py:196 +msgid "Enable WeCom Auth" +msgstr "启用企业微信认证" + +#: settings/serializers/settings.py:200 +msgid "AgentId" +msgstr "应用 ID(AgentId)" + +#: settings/serializers/settings.py:201 +msgid "AppKey" +msgstr "应用 Key(AppKey)" + +#: settings/serializers/settings.py:202 +msgid "AppSecret" +msgstr "应用密文(AppSecret)" + +#: settings/serializers/settings.py:203 +msgid "Enable DingTalk Auth" +msgstr "启用钉钉认证" + #: settings/utils/ldap.py:411 msgid "Host or port is disconnected: {}" msgstr "主机或端口不可连接: {}" @@ -2685,10 +2810,6 @@ msgstr "Web终端" msgid "File manager" msgstr "文件管理" -#: templates/_nav.html:110 terminal/serializers/session.py:37 -msgid "Terminal" -msgstr "终端" - #: templates/_nav.html:121 msgid "Job Center" msgstr "作业中心" @@ -2774,14 +2895,6 @@ msgstr "确认删除" msgid "Are you sure delete" msgstr "您确定删除吗?" -#: templates/flash_message_standalone.html:28 -msgid "Cancel" -msgstr "取消" - -#: templates/flash_message_standalone.html:37 -msgid "Go" -msgstr "跳转" - #: templates/index.html:11 msgid "Total users" msgstr "用户总数" @@ -3111,27 +3224,110 @@ msgstr "命令存储" msgid "Replay storage" msgstr "录像存储" -#: terminal/serializers/session.py:30 +#: terminal/notifications.py:29 +msgid "Terminal command alert" +msgstr "终端命令告警" + +#: terminal/notifications.py:38 +#, python-format +msgid "" +"\n" +" Command: %(command)s\n" +"
\n" +" Asset: %(host_name)s (%(host_ip)s)\n" +"
\n" +" User: %(user)s\n" +"
\n" +" Level: %(risk_level)s\n" +"
\n" +" Session:
session " +"detail\n" +"
\n" +" " +msgstr "" +"\n" +" 命令: %(command)s\n" +"
\n" +" 资产: %(host_name)s (%(host_ip)s)\n" +"
\n" +" 用户: %(user)s\n" +"
\n" +" 等级: %(risk_level)s\n" +"
\n" +" 会话: 会话详情\n" +"
\n" +" " + +#: terminal/notifications.py:73 +#, python-format +msgid "" +"Insecure Command Alert: [%(name)s->%(login_from)s@%(remote_addr)s] $" +"%(command)s" +msgstr "危险命令告警: [%(name)s->%(login_from)s@%(remote_addr)s] $%(command)s" + +#: terminal/notifications.py:90 +msgid "Batch command alert" +msgstr "批量命令告警" + +#: terminal/notifications.py:101 +#, python-format +msgid "" +"\n" +"
\n" +" Assets: %(assets)s\n" +"
\n" +" User: %(user)s\n" +"
\n" +" Level: %(risk_level)s\n" +"
\n" +"\n" +" ----------------- Commands ---------------- " +"
\n" +" %(command)s
\n" +" ----------------- Commands ---------------- " +"
\n" +" " +msgstr "" +"\n" +"
\n" +" 资产: %(assets)s\n" +"
\n" +" 用户: %(user)s\n" +"
\n" +" 等级: %(risk_level)s\n" +"
\n" +"\n" +" ----------------- 命令 ----------------
\n" +" %(command)s
\n" +" ----------------- 命令 ----------------
\n" +" " + +#: terminal/notifications.py:127 +#, python-format +msgid "Insecure Web Command Execution Alert: [%(name)s]" +msgstr "Web页面-> 命令执行 告警: [%(name)s]" + +#: terminal/serializers/session.py:33 msgid "User ID" msgstr "用户 ID" -#: terminal/serializers/session.py:31 +#: terminal/serializers/session.py:34 msgid "Asset ID" msgstr "资产 ID" -#: terminal/serializers/session.py:32 +#: terminal/serializers/session.py:35 msgid "System user ID" msgstr "系统用户 ID" -#: terminal/serializers/session.py:33 +#: terminal/serializers/session.py:36 msgid "Login from for display" msgstr "登录来源(显示名称)" -#: terminal/serializers/session.py:35 +#: terminal/serializers/session.py:38 msgid "Can replay" msgstr "是否可重放" -#: terminal/serializers/session.py:36 +#: terminal/serializers/session.py:39 msgid "Can join" msgstr "是否可加入" @@ -3200,82 +3396,10 @@ msgstr "文档类型" msgid "Ignore Certificate Verification" msgstr "忽略证书认证" -#: terminal/serializers/terminal.py:66 terminal/serializers/terminal.py:74 +#: terminal/serializers/terminal.py:73 terminal/serializers/terminal.py:81 msgid "Not found" msgstr "没有发现" -#: terminal/utils.py:78 -#, python-format -msgid "" -"Insecure Command Alert: [%(name)s->%(login_from)s@%(remote_addr)s] $" -"%(command)s" -msgstr "危险命令告警: [%(name)s->%(login_from)s@%(remote_addr)s] $%(command)s" - -#: terminal/utils.py:86 -#, python-format -msgid "" -"\n" -" Command: %(command)s\n" -"
\n" -" Asset: %(host_name)s (%(host_ip)s)\n" -"
\n" -" User: %(user)s\n" -"
\n" -" Level: %(risk_level)s\n" -"
\n" -" Session: session detail\n" -"
\n" -" " -msgstr "" -"\n" -" 命令: %(command)s\n" -"
\n" -" 资产: %(host_name)s (%(host_ip)s)\n" -"
\n" -" 用户: %(user)s\n" -"
\n" -" 等级: %(risk_level)s\n" -"
\n" -" 会话: 会话详情\n" -"
\n" -" " - -#: terminal/utils.py:113 -#, python-format -msgid "Insecure Web Command Execution Alert: [%(name)s]" -msgstr "Web页面-> 命令执行 告警: [%(name)s]" - -#: terminal/utils.py:121 -#, python-format -msgid "" -"\n" -"
\n" -" Assets: %(assets)s\n" -"
\n" -" User: %(user)s\n" -"
\n" -" Level: %(risk_level)s\n" -"
\n" -"\n" -" ----------------- Commands ----------------
\n" -" %(command)s
\n" -" ----------------- Commands ----------------
\n" -" " -msgstr "" -"\n" -"
\n" -" 资产: %(assets)s\n" -"
\n" -" 用户: %(user)s\n" -"
\n" -" 等级: %(risk_level)s\n" -"
\n" -"\n" -" ----------------- 命令 ----------------
\n" -" %(command)s
\n" -" ----------------- 命令 ----------------
\n" -" " - #: tickets/const.py:8 msgid "General" msgstr "一般" @@ -3636,13 +3760,13 @@ msgstr "动作 (显示名称)" msgid "Status display" msgstr "状态(显示名称)" -#: tickets/serializers/ticket/ticket.py:99 +#: tickets/serializers/ticket/ticket.py:101 msgid "" "The `type` in the submission data (`{}`) is different from the type in the " "request url (`{}`)" msgstr "提交数据中的类型 (`{}`) 与请求URL地址中的类型 (`{}`) 不一致" -#: tickets/serializers/ticket/ticket.py:120 +#: tickets/serializers/ticket/ticket.py:122 msgid "None of the assignees belong to Organization `{}` admins" msgstr "所有受理人都不属于组织 `{}` 下的管理员" @@ -3712,10 +3836,6 @@ msgstr "确认密码" msgid "Password does not match" msgstr "密码不一致" -#: users/forms/profile.py:101 users/models/user.py:557 -msgid "Email" -msgstr "邮件" - #: users/forms/profile.py:108 msgid "Old password" msgstr "原来密码" @@ -3781,11 +3901,11 @@ msgstr "用户来源" msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:741 +#: users/models/user.py:751 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:744 +#: users/models/user.py:754 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" @@ -3793,7 +3913,7 @@ msgstr "Administrator是初始的超级管理员" msgid "The old password is incorrect" msgstr "旧密码错误" -#: users/serializers/profile.py:36 users/serializers/user.py:113 +#: users/serializers/profile.py:36 users/serializers/user.py:125 msgid "Password does not match security rules" msgstr "密码不满足安全规则" @@ -3805,7 +3925,7 @@ msgstr "新密码不能是最近 {} 次的密码" msgid "The newly set password is inconsistent" msgstr "两次密码不一致" -#: users/serializers/profile.py:121 users/serializers/user.py:68 +#: users/serializers/profile.py:121 users/serializers/user.py:80 msgid "Is first login" msgstr "首次登录" @@ -3846,35 +3966,35 @@ msgstr "是否可更新" msgid "Can delete" msgstr "是否可删除" -#: users/serializers/user.py:39 users/serializers/user.py:75 +#: users/serializers/user.py:39 users/serializers/user.py:87 msgid "Organization role name" msgstr "组织角色名称" -#: users/serializers/user.py:71 +#: users/serializers/user.py:83 msgid "Avatar url" msgstr "头像路径" -#: users/serializers/user.py:73 +#: users/serializers/user.py:85 msgid "Groups name" msgstr "用户组名" -#: users/serializers/user.py:74 +#: users/serializers/user.py:86 msgid "Source name" msgstr "用户来源名" -#: users/serializers/user.py:76 +#: users/serializers/user.py:88 msgid "Super role name" msgstr "超级角色名称" -#: users/serializers/user.py:77 +#: users/serializers/user.py:89 msgid "Total role name" msgstr "汇总角色名称" -#: users/serializers/user.py:101 +#: users/serializers/user.py:113 msgid "Role limit to {}" msgstr "角色只能为 {}" -#: users/serializers/user.py:198 +#: users/serializers/user.py:210 msgid "name not unique" msgstr "名称重复" @@ -4925,12 +5045,85 @@ msgstr "旗舰版" msgid "Community edition" msgstr "社区版" -#~ msgid "The administrator require you to change your password this time" -#~ msgstr "管理员要求您本次修改密码" +#, python-format +#~ msgid "" +#~ "\n" +#~ " Command: %(command)s\n" +#~ "
\n" +#~ " Asset: %(host_name)s (%(host_ip)s)\n" +#~ "
\n" +#~ " User: %(user)s\n" +#~ "
\n" +#~ " Level: %(risk_level)s\n" +#~ "
\n" +#~ " Session: session detail\n" +#~ "
\n" +#~ " " +#~ msgstr "" +#~ "\n" +#~ " 命令: %(command)s\n" +#~ "
\n" +#~ " 资产: %(host_name)s (%(host_ip)s)\n" +#~ "
\n" +#~ " 用户: %(user)s\n" +#~ "
\n" +#~ " 等级: %(risk_level)s\n" +#~ "
\n" +#~ " 会话: 会话详情\n" +#~ "
\n" +#~ " " + +#, python-format +#~ msgid "" +#~ "\n" +#~ "
\n" +#~ " Assets: %(assets)s\n" +#~ "
\n" +#~ " User: %(user)s\n" +#~ "
\n" +#~ " Level: %(risk_level)s\n" +#~ "
\n" +#~ "\n" +#~ " ----------------- Commands ----------------
\n" +#~ " %(command)s
\n" +#~ " ----------------- Commands ----------------
\n" +#~ " " +#~ msgstr "" +#~ "\n" +#~ "
\n" +#~ " 资产: %(assets)s\n" +#~ "
\n" +#~ " 用户: %(user)s\n" +#~ "
\n" +#~ " 等级: %(risk_level)s\n" +#~ "
\n" +#~ "\n" +#~ " ----------------- 命令 ----------------
\n" +#~ " %(command)s
\n" +#~ " ----------------- 命令 ----------------
\n" +#~ " " + +#~ msgid "Ops" +#~ msgstr "选项" + +#~ msgid "Command Alert" +#~ msgstr "命令告警" + +#~ msgid "Agent Secret" +#~ msgstr "凭证密钥(secret)" + +#~ msgid "APP key" +#~ msgstr "APPKEY" + +#~ msgid "APP secret" +#~ msgstr "LDAP 地址" #~ msgid "Auth" #~ msgstr "认证" +#~ msgid "The administrator require you to change your password this time" +#~ msgstr "管理员要求您本次修改密码" + #~ msgid "Security and Role" #~ msgstr "角色安全" @@ -4985,9 +5178,6 @@ msgstr "社区版" #~ msgid "Join" #~ msgstr "加入" -#~ msgid "Update successfully!" -#~ msgstr "更新成功" - #~ msgid "Goto profile page enable MFA" #~ msgstr "请去个人信息页面启用自己的多因子认证" @@ -5000,6 +5190,9 @@ msgstr "社区版" #~ msgid "This will reset the user password and send a reset mail" #~ msgstr "将失效用户当前密码,并发送重设密码邮件到用户邮箱" +#~ msgid "Cancel" +#~ msgstr "取消" + #~ msgid "" #~ "The reset-ssh-public-key E-mail has been sent successfully. Please inform " #~ "the user to update his new ssh public key." diff --git a/apps/ops/apps.py b/apps/ops/apps.py index 01dfd05fa..8bdc04ce8 100644 --- a/apps/ops/apps.py +++ b/apps/ops/apps.py @@ -1,10 +1,12 @@ from __future__ import unicode_literals +from django.utils.translation import gettext_lazy as _ from django.apps import AppConfig class OpsConfig(AppConfig): name = 'ops' + verbose_name = _('Operations') def ready(self): from orgs.models import Organization diff --git a/apps/orgs/signals_handler/common.py b/apps/orgs/signals_handler/common.py index f59c4cb47..6beecd5cb 100644 --- a/apps/orgs/signals_handler/common.py +++ b/apps/orgs/signals_handler/common.py @@ -8,7 +8,6 @@ from django.dispatch import receiver from django.utils.functional import LazyObject from django.db.models.signals import m2m_changed from django.db.models.signals import post_save, post_delete, pre_delete -from django.utils.translation import ugettext as _ from orgs.utils import tmp_to_org from orgs.models import Organization, OrganizationMember @@ -19,7 +18,6 @@ from common.const.signals import PRE_REMOVE, POST_REMOVE from common.signals import django_ready from common.utils import get_logger from common.utils.connection import RedisPubSub -from common.exceptions import JMSException logger = get_logger(__file__) diff --git a/apps/perms/api/asset/user_permission/mixin.py b/apps/perms/api/asset/user_permission/mixin.py index 3b8733b3b..c0f25b8a4 100644 --- a/apps/perms/api/asset/user_permission/mixin.py +++ b/apps/perms/api/asset/user_permission/mixin.py @@ -3,8 +3,9 @@ from rest_framework.request import Request from common.permissions import IsOrgAdminOrAppUser, IsValidUser -from common.utils import lazyproperty from common.http import is_true +from common.mixins.api import RoleAdminMixin as _RoleAdminMixin +from common.mixins.api import RoleUserMixin as _RoleUserMixin from orgs.utils import tmp_to_root_org from users.models import User from perms.utils.asset.user_permission import UserGrantedTreeRefreshController @@ -20,24 +21,13 @@ class PermBaseMixin: return super().get(request, *args, **kwargs) -class RoleAdminMixin(PermBaseMixin): +class RoleAdminMixin(PermBaseMixin, _RoleAdminMixin): permission_classes = (IsOrgAdminOrAppUser,) - kwargs: dict - - @lazyproperty - def user(self): - user_id = self.kwargs.get('pk') - return User.objects.get(id=user_id) -class RoleUserMixin(PermBaseMixin): +class RoleUserMixin(PermBaseMixin, _RoleUserMixin): permission_classes = (IsValidUser,) - request: Request def get(self, request, *args, **kwargs): with tmp_to_root_org(): return super().get(request, *args, **kwargs) - - @lazyproperty - def user(self): - return self.request.user diff --git a/apps/settings/api/__init__.py b/apps/settings/api/__init__.py index 151617be5..39e009ed5 100644 --- a/apps/settings/api/__init__.py +++ b/apps/settings/api/__init__.py @@ -1,2 +1,4 @@ from .common import * from .ldap import * +from .wecom import * +from .dingtalk import * diff --git a/apps/settings/api/common.py b/apps/settings/api/common.py index 1fa578c50..bb3107dfd 100644 --- a/apps/settings/api/common.py +++ b/apps/settings/api/common.py @@ -125,7 +125,9 @@ class PublicSettingApi(generics.RetrieveAPIView): 'SECURITY_PASSWORD_LOWER_CASE': settings.SECURITY_PASSWORD_LOWER_CASE, 'SECURITY_PASSWORD_NUMBER': settings.SECURITY_PASSWORD_NUMBER, 'SECURITY_PASSWORD_SPECIAL_CHAR': settings.SECURITY_PASSWORD_SPECIAL_CHAR, - } + }, + "AUTH_WECOM": settings.AUTH_WECOM, + "AUTH_DINGTALK": settings.AUTH_DINGTALK, } } return instance @@ -141,6 +143,8 @@ class SettingsApi(generics.RetrieveUpdateAPIView): 'ldap': serializers.LDAPSettingSerializer, 'email': serializers.EmailSettingSerializer, 'email_content': serializers.EmailContentSettingSerializer, + 'wecom': serializers.WeComSettingSerializer, + 'dingtalk': serializers.DingTalkSettingSerializer, } def get_serializer_class(self): @@ -163,6 +167,8 @@ class SettingsApi(generics.RetrieveUpdateAPIView): category = self.request.query_params.get('category', '') for name, value in serializer.validated_data.items(): encrypted = name in encrypted_items + if encrypted and value in ['', None]: + continue data.append({ 'name': name, 'value': value, 'encrypted': encrypted, 'category': category diff --git a/apps/settings/api/dingtalk.py b/apps/settings/api/dingtalk.py new file mode 100644 index 000000000..e560f8626 --- /dev/null +++ b/apps/settings/api/dingtalk.py @@ -0,0 +1,38 @@ +import requests + +from rest_framework.views import Response +from rest_framework.generics import GenericAPIView +from django.utils.translation import gettext_lazy as _ + +from common.permissions import IsSuperUser +from common.message.backends.dingtalk import URL + +from .. import serializers + + +class DingTalkTestingAPI(GenericAPIView): + permission_classes = (IsSuperUser,) + serializer_class = serializers.DingTalkSettingSerializer + + def post(self, request): + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + + dingtalk_appkey = serializer.validated_data['DINGTALK_APPKEY'] + dingtalk_agentid = serializer.validated_data['DINGTALK_AGENTID'] + dingtalk_appsecret = serializer.validated_data['DINGTALK_APPSECRET'] + + try: + params = {'appkey': dingtalk_appkey, 'appsecret': dingtalk_appsecret} + resp = requests.get(url=URL.GET_TOKEN, params=params) + if resp.status_code != 200: + return Response(status=400, data={'error': resp.json()}) + + data = resp.json() + errcode = data['errcode'] + if errcode != 0: + return Response(status=400, data={'error': data['errmsg']}) + + return Response(status=200, data={'msg': _('OK')}) + except Exception as e: + return Response(status=400, data={'error': str(e)}) diff --git a/apps/settings/api/wecom.py b/apps/settings/api/wecom.py new file mode 100644 index 000000000..0fda33c61 --- /dev/null +++ b/apps/settings/api/wecom.py @@ -0,0 +1,38 @@ +import requests + +from rest_framework.views import Response +from rest_framework.generics import GenericAPIView +from django.utils.translation import gettext_lazy as _ + +from common.permissions import IsSuperUser +from common.message.backends.wecom import URL + +from .. import serializers + + +class WeComTestingAPI(GenericAPIView): + permission_classes = (IsSuperUser,) + serializer_class = serializers.WeComSettingSerializer + + def post(self, request): + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + + wecom_corpid = serializer.validated_data['WECOM_CORPID'] + wecom_agentid = serializer.validated_data['WECOM_AGENTID'] + wecom_corpsecret = serializer.validated_data['WECOM_CORPSECRET'] + + try: + params = {'corpid': wecom_corpid, 'corpsecret': wecom_corpsecret} + resp = requests.get(url=URL.GET_TOKEN, params=params) + if resp.status_code != 200: + return Response(status=400, data={'error': resp.json()}) + + data = resp.json() + errcode = data['errcode'] + if errcode != 0: + return Response(status=400, data={'error': data['errmsg']}) + + return Response(status=200, data={'msg': _('OK')}) + except Exception as e: + return Response(status=400, data={'error': str(e)}) diff --git a/apps/settings/serializers/settings.py b/apps/settings/serializers/settings.py index b64b95cb6..5d33a1d83 100644 --- a/apps/settings/serializers/settings.py +++ b/apps/settings/serializers/settings.py @@ -6,7 +6,7 @@ from rest_framework import serializers __all__ = [ 'BasicSettingSerializer', 'EmailSettingSerializer', 'EmailContentSettingSerializer', 'LDAPSettingSerializer', 'TerminalSettingSerializer', 'SecuritySettingSerializer', - 'SettingsSerializer' + 'SettingsSerializer', 'WeComSettingSerializer', 'DingTalkSettingSerializer', ] @@ -189,13 +189,29 @@ class SecuritySettingSerializer(serializers.Serializer): ) +class WeComSettingSerializer(serializers.Serializer): + WECOM_CORPID = serializers.CharField(max_length=256, required=True, label=_('Corporation ID')) + WECOM_AGENTID = serializers.CharField(max_length=256, required=True, label=_("Agent ID")) + WECOM_CORPSECRET = serializers.CharField(max_length=256, required=False, label=_("Corporation Secret"), write_only=True) + AUTH_WECOM = serializers.BooleanField(default=False, label=_('Enable WeCom Auth')) + + +class DingTalkSettingSerializer(serializers.Serializer): + DINGTALK_AGENTID = serializers.CharField(max_length=256, required=True, label=_("AgentId")) + DINGTALK_APPKEY = serializers.CharField(max_length=256, required=True, label=_("AppKey")) + DINGTALK_APPSECRET = serializers.CharField(max_length=256, required=False, label=_("AppSecret"), write_only=True) + AUTH_DINGTALK = serializers.BooleanField(default=False, label=_('Enable DingTalk Auth')) + + class SettingsSerializer( BasicSettingSerializer, EmailSettingSerializer, EmailContentSettingSerializer, LDAPSettingSerializer, TerminalSettingSerializer, - SecuritySettingSerializer + SecuritySettingSerializer, + WeComSettingSerializer, + DingTalkSettingSerializer, ): # encrypt_fields 现在使用 write_only 来判断了 diff --git a/apps/settings/urls/api_urls.py b/apps/settings/urls/api_urls.py index 0db9c7c54..86dfc6847 100644 --- a/apps/settings/urls/api_urls.py +++ b/apps/settings/urls/api_urls.py @@ -13,6 +13,8 @@ urlpatterns = [ path('ldap/users/', api.LDAPUserListApi.as_view(), name='ldap-user-list'), path('ldap/users/import/', api.LDAPUserImportAPI.as_view(), name='ldap-user-import'), path('ldap/cache/refresh/', api.LDAPCacheRefreshAPI.as_view(), name='ldap-cache-refresh'), + path('wecom/testing/', api.WeComTestingAPI.as_view(), name='wecom-testing'), + path('dingtalk/testing/', api.DingTalkTestingAPI.as_view(), name='dingtalk-testing'), path('setting/', api.SettingsApi.as_view(), name='settings-setting'), path('public/', api.PublicSettingApi.as_view(), name='public-setting'), diff --git a/apps/static/img/login_dingtalk_log.png b/apps/static/img/login_dingtalk_log.png new file mode 100644 index 0000000000000000000000000000000000000000..998f730ad056a13c1e9b06036be3851055524550 GIT binary patch literal 5062 zcmb7HXH=8jvVH>yA_&q%stOS#^d=xpfq>FN3qg8EO6WyEidYa3lwgQRlOQC7W#XJZacBS7vuE!$&&)jg$IQv_$rNxwTl20a00L0}*G?C3 zG6tvvXQ`=asL!0Gp`oFrJ$sIx^*lWt9X$s#3nMEJ=Ve|VPHyfiU@^ffd?NhZ+}GuV zL?k3-WMnQ2Dk#fKDTztTNd4smL`zFcPe*_0{P{~#SGliB{lDep6Toy9=myzQg7^Rm zCJ-eP=%fkY20#D>1tkdhM?f@Zs41wwCol%O+bsLoPR(*Exw5CtXG8768P z=BtvoSy&D268U@{5!v{qjO?=-d!^Nkr@`t;>@rXj2VXzb=e|=PoS;*L{{R4}s6mux zC{FJrm`=edKonF|XQ(Luj^Ho}8XlJ{bk*Pd!mE zQ8EFlK*OhhV*Z!LuL3YL#zU=?!C^TXy-nThHPppc9~DIfELbLtcIFasjK%>b7GCWa zyKcI}UGhh*uBI-NH!5 z)>()tC8Aziy>o;8oQrtZJ$$J3QSa7})2>L!gFro4&)$JYX4cGaugfDCmAjBwByOmb z=Ld@8VK1EEvJjZ!o^DuXK!VNhz3NqwhR}H`9&s&`C3AOw42@5MdfwGptQlr6La-R2 zG}&?E>CSb&*~ix_>tys_%!m;~1^L00G}v1}+rKjXZ@Z?Rv5C8z4zsAzSv1>tjf{;` zhUalS8m{Ox9W^FvZ*BrYtMk-6n z%*q)rLRYxu;0x3H8ghf>+F7#tMc&jZ2P~QHMi9+Nb0Vca`Pk#`I^<`zv{~Hd4}lk9I^K; z*W7eZ^v0XLb-C}pVr0#Jy_U;({-D9PE~TlXAvPm__`~!zQ)Xk3*4jPWY3@Sw^F0Ob zL0v4C%sS^2>aQ5HiUa_u|5m6PI!S@|l47B=8BW$ex##o8$$sNi2nZRj&)}D_>xY5V^Pg#D zVZPL}(b|wNloFim*xfAHKRg@xV#@J#Mw_Fp?JLI%fRy835m9U20{pHiodtiRjErgja!_`FxU;6fTRJxb` zl08niEZ8c9L+Zc=L(|u!QCp*gq5j_F4SC{N7Zf^Gn*hCE8u=I4ALS|{l3Lhc5=$JJ zD@Zi7jyH)+NjSYx_xQN=D(-{4xTIjZh=JG7v`d9 za9V zB9d^?Cp2%B%}kozN;J5%ZsJG$dW?b&`^UJ&wl6rAQmYR{PsaN*Bf85WZ+if)^R|E2 zh$S(T;oL2WZ;eQgeZ%S*&1Iy{wz8sesc$j6G%AFvYQDzmvjx0`a~LreGtv@_GZC*o zH(bOjG5EPE!8H3--d1IXUcLMtX3OHii-w!URGDTc0L^{CH=**68~~K(31Tt#KMV}= z9Xqcrvow=Fb)qu{#8f7DD0$jV5x7k7@m({MpF=HFN=LP6!FoZ$n5sVIZeQ|OOTs}O z!0yBvtFot04JcE}yU(x31A!m<(X1(B=4%bYG zpL_o~HI{xD5tLgK33>$p^vTaU*Oq0$dm}vPn4safsoE1jH3gBIXUyepgOqO&TK2#bwiw{XqmhI zEFpMqQ8l<~K(9L-8bnC1XRaF&3gv(NwHw~TyMA!9lAkK-tLZHOa9%mI-}lWuR2%L3 zR@M<8;aW||o3f*mb{diixMo$ab>vg?(Ox`i`}nNV(^3LPUi1XOdR;aVty1&Sypd_* zHlebUK!%Fy-l-&wv`)>=d3>9AKfDa*$D{9)t&_ZA#$Kk_Z4F6uwP&|Cblvm-44)(0 zy9^X|^Yebss8huqiuAU&<%-UiRB6=#uMe%h1#rS+SB>M*!%~$rk>(Z zDYG|=1>1z(6#*Mmvg^5&YlbNS;!_crtc$-OUgl{qg#qL4y9NgkyN5PktTxVH1 z#w{1sK}uL#)nL@jPE_efV9}a7&KD88vJZB#*{QLcg&Q8PKoZaIcE}a@$}V;hVxuqE zExv)}Eht)PJuK@Rh%gc;o((b>lF>b`$K)(m#&$z*fUgIAY4pfmA^ld)a5BVHD*)#; zr~dQ>4hA-I(Ge8CUfxs=N+Z;ELkDtQ-@`twC(dgL@Z!znRa<{XplZ6q8M}YcGezCD z6Mh2Fr_h9x{NIuSo>jBKHO0U#T5<#)AUKWn$BbC#F0K4sZROqt_pmVKj#vo6CHqGr ze%3_Mi6ghn$~i41h%?Es;l(S+%*(ikbe!k2T^AiDQ8~Se`PR)HA#B_kQcJjvLhM9L zd%s7xTrTpx?GTIgV$KN=)4I?=Y?u9(7DPR6PTp|m&fnrwa>qPS?MXd`i*Je#40~T_ zZp0T3nysxT>mF##{>(x-#_$IvvhzCU-EPe`zoF@aF{?}aM3dA5ELe+qkE|vk^cxT? zelIrR3(@%Ic6@f{du&?DJEY3ih!d`~h5R|pW7y`{cn8TkZruOb*sx^I~>CD5fGv&GdWPPG1+=TUz#a#a_3hXf>Og zU*jB@eg0(XMe!r9Nr9wr6qw2qWy{oq-;2iA?6!-VtamBPm5H&Z9twV%+>Jjj7v`UA z*58#I{PewfW=-FTC#}koj8Gx`1O|%Q#}yEbD#se@zjCp`NJH zN(KiHcfpS#qBq{Nye@a#_0sV2;t2fZHfK$dO-~<)cg4KOyhIUfj1IWGOVT{HDuYz< zYh=B1bgTU?T+slQO<5nRA)EM@v2$R`L(N{q2N}n1&f0awsT&k}7QTmjS}-`-L{t4t z#Kpgiw@7msPNFY(k?KIJQ{`{m^vbxI1rEQ|I%?!twa8 z<9T-)ae;P-d!NpNk2Fy-i#;HI6}_Fv)DiEizyy~|LPar2q|u5rTSbBv=u;WuDhgYT zjflpQO?COl?Z;gY%F2;0=I;LJtqO+B$5LrQ#>K5*UQM#8QfB^w{>=UD66PH%y>+Tc zBhrUI5p`;N2bhGa5rep}mVBOkc}HgYu;t~>z7HW3yKc|6-QULg@9_?P-gVnC*=L1+ zoiKE*)X_5Pyvw=0sUK@z`J(tngXk~P0mQPNi_k5loVn%yiN<#JZzxKtsr6B>fO3WJ zvUI~kbv1+{&j&mdY>X^VTrSfm_pY&chhMLgcJb_yl^%~rnrIX7&(l$UMpN1MzQrM8kFDMuFMQR? zu>bI)r@%6gU&)C7;Tt_4g36Go48(q2%h&r)IOk<-vXVk)JOGx(>yfrfgrq6lPD`^EAtNefKuXtBz$l z!PQ^1m|eoW$w!4t^(o?S%>#{$wO}lU#Fi#^!<)8)J2%n}c}FJQ?!{I=(SksH96|&e zn#MXXLYVon>6eZc2L)QtaG@Z_NQQgg{tGPtz?o6fw_4vT(lae=uUoo_n=tl}CvZW^ z1N8KHdI?Jo{@j8i8Gdg$uB2>gH#@$t6h>rii>^9k^Vdoae$;apPL#)ilfKWH46<$; znW)gtn9bTxcUj-hX=S{@Au+nX6zlBFy@|t(^=4iH$vis^Bl3y_?Uf`DS4NkugIK_j z_EYz(xp7>iUc`}Ov)ja7F>H1In<6Buf(Jc({s(z~XX_@iCMfJ?IlJS@%(C#}Frh0Qjxmovv-FoQ%Q?*Kc@)F;5ZoyPP8jf?@NnxB|; z$b$!$m3go>=5v-}9;U{8>srh>_MgRxuEh8R83!iL0bM>p^+)YnLV|j*H4+U+OZl%c z1WZH!LK=Y35jq_%0Da9n081hYpqjm){l|J6qCjnwg3Jahy*+RQX{7HqxO>CF*E2Bv z?cji?j!$MyY1AH4x8{hcs3RA>*_Nn(twLE%0CN{=Xm#_p4qZ-SuxuV$q)WW?V8OD3 zRCEmwCE3k=3!fYEAdam{?D>12PE2c_L3O$b5{OcTdt;SDz2W3Iy5~>Do4pW6RGt@R zL2ZiWSN>hN*H>QuTmMf5bP0TkhDI|13e_;Nj!lsWI?v@$Np`|IaMw<-m*g+F3_Nxe zP51H+k-Y~G6xH+Sfs|W7`Kks6OIp^R?@X$*-5@RZAF%na!PwUY7jp2lM6wjN?8jA| z*yg0~(NO5`&f$nSd}kzI<&oxh*`Q^((L@=x91?}K+gikv!pK;(eP{(YxJ zemS*o|EYbupG6cz_KEB}@XP*vBLC|+dhpQUBfp65+kZf8ufwyGBK!95|7E}EfrFw4 zj~qOFM0~Hm=r8*Z9y)dS^o1j5&R)GGanbg%jHEgD*px_;w!-KV3<+IQ~S z**iQr7nGJhwW@0hd>R;>(WIi|PkC@MXc+!N#N zkxMtUZ`VmF-!-)>`m}Q00roX$c6ye4VR)g^o@8E{t*lZZD-pLRM|w!D~KdEan)Jx=m4 z;rbi+K3Kh98;^*rAo9Bfgi7MO@7}@Wib�)QNC&!(EYe9DFXWyC0UyYhZsGUF5`; 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