diff --git a/apps/authentication/api/__init__.py b/apps/authentication/api/__init__.py index 6ef54b09b..c0064f9bd 100644 --- a/apps/authentication/api/__init__.py +++ b/apps/authentication/api/__init__.py @@ -9,4 +9,5 @@ from .login_confirm import * from .sso import * from .wecom import * from .dingtalk import * +from .feishu import * from .password import * diff --git a/apps/authentication/api/feishu.py b/apps/authentication/api/feishu.py new file mode 100644 index 000000000..1665d057a --- /dev/null +++ b/apps/authentication/api/feishu.py @@ -0,0 +1,45 @@ +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 FeiShuQRUnBindBase(APIView): + user: User + + def post(self, request: Request, **kwargs): + user = self.user + + if not user.feishu_id: + raise errors.FeiShuNotBound + + user.feishu_id = None + user.save() + return Response() + + +class FeiShuQRUnBindForUserApi(RoleUserMixin, FeiShuQRUnBindBase): + permission_classes = (IsAuthPasswdTimeValid,) + + +class FeiShuQRUnBindForAdminApi(RoleAdminMixin, FeiShuQRUnBindBase): + user_id_url_kwarg = 'user_id' + permission_classes = (IsOrgAdmin,) + + +class FeiShuEventSubscriptionCallback(APIView): + """ + # https://open.feishu.cn/document/ukTMukTMukTM/uUTNz4SN1MjL1UzM + """ + permission_classes = () + + def post(self, request: Request, *args, **kwargs): + return Response(data=request.data) diff --git a/apps/authentication/backends/api.py b/apps/authentication/backends/api.py index 892ebcc7c..79d420626 100644 --- a/apps/authentication/backends/api.py +++ b/apps/authentication/backends/api.py @@ -240,6 +240,15 @@ class DingTalkAuthentication(JMSModelBackend): pass +class FeiShuAuthentication(JMSModelBackend): + """ + 什么也不做呀😺 + """ + + def authenticate(self, request, **kwargs): + pass + + class AuthorizationTokenAuthentication(JMSModelBackend): """ 什么也不做呀😺 diff --git a/apps/authentication/errors.py b/apps/authentication/errors.py index bcd83c97d..ad8148182 100644 --- a/apps/authentication/errors.py +++ b/apps/authentication/errors.py @@ -315,6 +315,11 @@ class DingTalkNotBound(JMSException): default_detail = 'DingTalk is not bound' +class FeiShuNotBound(JMSException): + default_code = 'feishu_not_bound' + default_detail = 'FeiShu is not bound' + + class PasswdInvalid(JMSException): default_code = 'passwd_invalid' default_detail = _('Your password is invalid') diff --git a/apps/authentication/templates/authentication/login.html b/apps/authentication/templates/authentication/login.html index 0104d0e44..c54f792c7 100644 --- a/apps/authentication/templates/authentication/login.html +++ b/apps/authentication/templates/authentication/login.html @@ -191,7 +191,7 @@
- {% if AUTH_OPENID or AUTH_CAS or AUTH_WECOM or AUTH_DINGTALK %} + {% if AUTH_OPENID or AUTH_CAS or AUTH_WECOM or AUTH_DINGTALK or AUTH_FEISHU %}
{% trans "More login options" %} @@ -215,6 +215,11 @@ {% trans 'DingTalk' %} {% endif %} + {% if AUTH_FEISHU %} + + {% endif %}
{% else %} diff --git a/apps/authentication/urls/api_urls.py b/apps/authentication/urls/api_urls.py index 0849cc82a..d8613adf4 100644 --- a/apps/authentication/urls/api_urls.py +++ b/apps/authentication/urls/api_urls.py @@ -20,6 +20,10 @@ urlpatterns = [ 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('feishu/qr/unbind/', api.FeiShuQRUnBindForUserApi.as_view(), name='feishu-qr-unbind'), + path('feishu/qr/unbind//', api.FeiShuQRUnBindForAdminApi.as_view(), name='feishu-qr-unbind-for-admin'), + path('feishu/event/subscription/callback/', api.FeiShuEventSubscriptionCallback.as_view(), name='feishu-event-subscription-callback'), + 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'), diff --git a/apps/authentication/urls/view_urls.py b/apps/authentication/urls/view_urls.py index 8e754340c..0bac07e25 100644 --- a/apps/authentication/urls/view_urls.py +++ b/apps/authentication/urls/view_urls.py @@ -37,6 +37,14 @@ urlpatterns = [ 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'), + path('feishu/bind/success-flash-msg/', views.FlashDingTalkBindSucceedMsgView.as_view(), name='feishu-bind-success-flash-msg'), + path('feishu/bind/failed-flash-msg/', views.FlashDingTalkBindFailedMsgView.as_view(), name='feishu-bind-failed-flash-msg'), + path('feishu/bind/start/', views.FeiShuEnableStartView.as_view(), name='feishu-bind-start'), + path('feishu/qr/bind/', views.FeiShuQRBindView.as_view(), name='feishu-qr-bind'), + path('feishu/qr/login/', views.FeiShuQRLoginView.as_view(), name='feishu-qr-login'), + path('feishu/qr/bind/callback/', views.FeiShuQRBindCallbackView.as_view(), name='feishu-qr-bind-callback'), + path('feishu/qr/login/callback/', views.FeiShuQRLoginCallbackView.as_view(), name='feishu-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 0467e321a..38cf114e1 100644 --- a/apps/authentication/views/__init__.py +++ b/apps/authentication/views/__init__.py @@ -4,3 +4,4 @@ from .login import * from .mfa import * from .wecom import * from .dingtalk import * +from .feishu import * diff --git a/apps/authentication/views/feishu.py b/apps/authentication/views/feishu.py new file mode 100644 index 000000000..2db1404d7 --- /dev/null +++ b/apps/authentication/views/feishu.py @@ -0,0 +1,253 @@ +import urllib + +from django.http.response import HttpResponseRedirect, HttpResponse +from django.utils.decorators import method_decorator +from django.utils.translation import ugettext_lazy 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 django.db.utils import IntegrityError +from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.exceptions import APIException + +from users.utils import is_auth_password_time_valid +from users.views import UserVerifyPasswordView +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.mixins.views import PermissionsMixin +from common.message.backends.feishu import FeiShu, URL +from authentication import errors +from authentication.mixins import AuthMixin + +logger = get_logger(__file__) + + +FEISHU_STATE_SESSION_KEY = '_feishu_state' + + +class FeiShuQRMixin(PermissionsMixin, View): + def dispatch(self, request, *args, **kwargs): + try: + return super().dispatch(request, *args, **kwargs) + except APIException as e: + msg = str(e.detail) + return self.get_failed_reponse( + '/', + _('FeiShu Error'), + msg + ) + + def verify_state(self): + state = self.request.GET.get('state') + session_state = self.request.session.get(FEISHU_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[FEISHU_STATE_SESSION_KEY] = state + + params = { + 'app_id': settings.FEISHU_APP_ID, + 'state': state, + 'redirect_uri': redirect_uri, + } + url = URL.AUTHEN + '?' + urllib.parse.urlencode(params) + return url + + def get_success_reponse(self, redirect_url, title, msg): + ok_flash_msg_url = reverse('authentication:feishu-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:feishu-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 = _('FeiShu is already bound') + response = self.get_failed_reponse(redirect_url, msg, msg) + return response + + +class FeiShuQRBindView(FeiShuQRMixin, 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:feishu-qr-bind-callback', external=True) + redirect_uri += '?' + urllib.parse.urlencode({'redirect_url': redirect_url}) + + url = self.get_qr_url(redirect_uri) + return HttpResponseRedirect(url) + + +class FeiShuQRBindCallbackView(FeiShuQRMixin, View): + permission_classes = (IsAuthenticated,) + + def get(self, request: HttpRequest): + 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 = request.user + + if user.feishu_id: + response = self.get_already_bound_response(redirect_url) + return response + + feishu = FeiShu( + app_id=settings.FEISHU_APP_ID, + app_secret=settings.FEISHU_APP_SECRET + ) + user_id = feishu.get_user_id_by_code(code) + + if not user_id: + msg = _('FeiShu query user failed') + response = self.get_failed_reponse(redirect_url, msg, msg) + return response + + try: + user.feishu_id = user_id + user.save() + except IntegrityError as e: + if e.args[0] == 1062: + msg = _('The FeiShu is already bound to another user') + response = self.get_failed_reponse(redirect_url, msg, msg) + return response + raise e + + msg = _('Binding FeiShu successfully') + response = self.get_success_reponse(redirect_url, msg, msg) + return response + + +class FeiShuEnableStartView(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:feishu-qr-bind') + + success_url += '?' + urllib.parse.urlencode({ + 'redirect_url': redirect_url or referer + }) + + return success_url + + +class FeiShuQRLoginView(FeiShuQRMixin, View): + permission_classes = (AllowAny,) + + def get(self, request: HttpRequest): + redirect_url = request.GET.get('redirect_url') + + redirect_uri = reverse('authentication:feishu-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 FeiShuQRLoginCallbackView(AuthMixin, FeiShuQRMixin, 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) + + feishu = FeiShu( + app_id=settings.FEISHU_APP_ID, + app_secret=settings.FEISHU_APP_SECRET + ) + user_id = feishu.get_user_id_by_code(code) + if not user_id: + # 正常流程不会出这个错误,hack 行为 + msg = _('Failed to get user from FeiShu') + response = self.get_failed_reponse(login_url, title=msg, msg=msg) + return response + + user = get_object_or_none(User, feishu_id=user_id) + if user is None: + title = _('FeiShu is not bound') + msg = _('Please login with a password and then bind the WeCom') + response = self.get_failed_reponse(login_url, title=title, msg=msg) + return response + + try: + self.check_oauth2_auth(user, settings.AUTH_BACKEND_FEISHU) + 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 FlashFeiShuBindSucceedMsgView(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 FeiShu successfully'), + 'messages': msg or _('Binding FeiShu 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 FlashFeiShuBindFailedMsgView(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 FeiShu failed'), + 'messages': msg or _('Binding FeiShu 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 0eef00579..6a2481d20 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -154,6 +154,7 @@ class UserLoginView(mixins.AuthMixin, FormView): 'AUTH_CAS': settings.AUTH_CAS, 'AUTH_WECOM': settings.AUTH_WECOM, 'AUTH_DINGTALK': settings.AUTH_DINGTALK, + 'AUTH_FEISHU': settings.AUTH_FEISHU, 'rsa_public_key': rsa_public_key, 'forgot_password_url': forgot_password_url } diff --git a/apps/common/message/backends/dingtalk/__init__.py b/apps/common/message/backends/dingtalk/__init__.py index 0ca9d5dc5..e98bdee04 100644 --- a/apps/common/message/backends/dingtalk/__init__.py +++ b/apps/common/message/backends/dingtalk/__init__.py @@ -2,8 +2,7 @@ import time import hmac import base64 -from common.message.backends.utils import request -from common.message.backends.utils import digest +from common.message.backends.utils import digest, as_request from common.message.backends.mixin import BaseRequest @@ -34,7 +33,7 @@ class URL: class DingTalkRequests(BaseRequest): - invalid_token_errcode = ErrorCode.INVALID_TOKEN + invalid_token_errcodes = (ErrorCode.INVALID_TOKEN,) def __init__(self, appid, appsecret, agentid, timeout=None): self._appid = appid @@ -55,21 +54,33 @@ class DingTalkRequests(BaseRequest): expires_in = data['expires_in'] return access_token, expires_in - @request + def add_token(self, kwargs: dict): + params = kwargs.get('params') + if params is None: + params = {} + kwargs['params'] = params + params['access_token'] = self.access_token + def get(self, url, params=None, with_token=False, with_sign=False, check_errcode_is_0=True, **kwargs): pass + get = as_request(get) - @request def post(self, url, json=None, params=None, with_token=False, with_sign=False, check_errcode_is_0=True, **kwargs): pass + post = as_request(post) + + def _add_sign(self, kwargs: dict): + params = kwargs.get('params') + if params is None: + params = {} + kwargs['params'] = params - def _add_sign(self, params: dict): timestamp = str(int(time.time() * 1000)) signature = sign(self._appsecret, timestamp) accessKey = self._appid @@ -78,23 +89,17 @@ class DingTalkRequests(BaseRequest): params['signature'] = signature params['accessKey'] = accessKey - def request(self, method, url, params=None, + def request(self, method, url, 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) + self._add_sign(kwargs) + data = super().request( + method, url, with_token=with_token, + check_errcode_is_0=check_errcode_is_0, **kwargs) return data diff --git a/apps/common/message/backends/feishu/__init__.py b/apps/common/message/backends/feishu/__init__.py new file mode 100644 index 000000000..7f70fd35d --- /dev/null +++ b/apps/common/message/backends/feishu/__init__.py @@ -0,0 +1,114 @@ +import json + +from django.utils.translation import ugettext_lazy as _ +from rest_framework.exceptions import APIException + +from common.utils.common import get_logger +from common.message.backends.utils import digest +from common.message.backends.mixin import RequestMixin, BaseRequest + +logger = get_logger(__name__) + + +class URL: + AUTHEN = 'https://open.feishu.cn/open-apis/authen/v1/index' + + GET_TOKEN = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/' + + # https://open.feishu.cn/document/ukTMukTMukTM/uEDO4UjLxgDO14SM4gTN + GET_USER_INFO_BY_CODE = 'https://open.feishu.cn/open-apis/authen/v1/access_token' + + SEND_MESSAGE = 'https://open.feishu.cn/open-apis/im/v1/messages' + + +class ErrorCode: + INVALID_APP_ACCESS_TOKEN = 99991664 + INVALID_USER_ACCESS_TOKEN = 99991668 + INVALID_TENANT_ACCESS_TOKEN = 99991663 + + +class FeishuRequests(BaseRequest): + """ + 处理系统级错误,抛出 API 异常,直接生成 HTTP 响应,业务代码无需关心这些错误 + - 确保 status_code == 200 + - 确保 access_token 无效时重试 + """ + invalid_token_errcodes = ( + ErrorCode.INVALID_USER_ACCESS_TOKEN, ErrorCode.INVALID_TENANT_ACCESS_TOKEN, + ErrorCode.INVALID_APP_ACCESS_TOKEN + ) + code_key = 'code' + msg_key = 'msg' + + def __init__(self, app_id, app_secret, timeout=None): + self._app_id = app_id + self._app_secret = app_secret + + super().__init__(timeout=timeout) + + def get_access_token_cache_key(self): + return digest(self._app_id, self._app_secret) + + def request_access_token(self): + data = {'app_id': self._app_id, 'app_secret': self._app_secret} + response = self.raw_request('post', url=URL.GET_TOKEN, data=data) + self.check_errcode_is_0(response) + + access_token = response['tenant_access_token'] + expires_in = response['expire'] + return access_token, expires_in + + def add_token(self, kwargs: dict): + headers = kwargs.setdefault('headers', {}) + headers['Authorization'] = f'Bearer {self.access_token}' + + +class FeiShu(RequestMixin): + """ + 非业务数据导致的错误直接抛异常,说明是系统配置错误,业务代码不用理会 + """ + + def __init__(self, app_id, app_secret, timeout=None): + self._app_id = app_id + self._app_secret = app_secret + + self._requests = FeishuRequests( + app_id=app_id, + app_secret=app_secret, + timeout=timeout + ) + + def get_user_id_by_code(self, code): + # https://open.feishu.cn/document/ukTMukTMukTM/uEDO4UjLxgDO14SM4gTN + + body = { + 'grant_type': 'authorization_code', + 'code': code + } + + data = self._requests.post(URL.GET_USER_INFO_BY_CODE, json=body, check_errcode_is_0=False) + + self._requests.check_errcode_is_0(data) + return data['data']['user_id'] + + def send_text(self, user_ids, msg): + params = { + 'receive_id_type': 'user_id' + } + + body = { + 'msg_type': 'text', + 'content': json.dumps({'text': msg}) + } + + invalid_users = [] + for user_id in user_ids: + body['receive_id'] = user_id + + try: + self._requests.post(URL.SEND_MESSAGE, params=params, json=body) + except APIException as e: + # 只处理可预知的错误 + logger.exception(e) + invalid_users.append(user_id) + return invalid_users diff --git a/apps/common/message/backends/mixin.py b/apps/common/message/backends/mixin.py index 5652a1520..af151a536 100644 --- a/apps/common/message/backends/mixin.py +++ b/apps/common/message/backends/mixin.py @@ -6,7 +6,7 @@ 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 common.message.backends.utils import set_default, as_request from . import exceptions as exce @@ -14,17 +14,37 @@ logger = get_logger(__name__) class RequestMixin: - def check_errcode_is_0(self, data: DictWrapper): - errcode = data['errcode'] + code_key: str + msg_key: str + + +class BaseRequest(RequestMixin): + """ + 定义了 `access_token` 的过期刷新框架 + """ + invalid_token_errcodes = () + code_key = 'errcode' + msg_key = 'err_msg' + + def __init__(self, timeout=None): + self._request_kwargs = { + 'timeout': timeout + } + self.init_access_token() + + @classmethod + def check_errcode_is_0(cls, data: DictWrapper): + errcode = data[cls.code_key] if errcode != 0: # 如果代码写的对,配置没问题,这里不该出错,系统性错误,直接抛异常 - errmsg = data['errmsg'] + errmsg = data[cls.msg_key] logger.error(f'Response 200 but errcode is not 0: ' f'errcode={errcode} ' f'errmsg={errmsg} ') raise exce.ErrCodeNot0(detail=data.raw_data) - def check_http_is_200(self, response): + @staticmethod + def check_http_is_200(response): if response.status_code != 200: # 正常情况下不会返回非 200 响应码 logger.error(f'Response error: ' @@ -33,25 +53,28 @@ class RequestMixin: f'\ncontent={response.content}') raise exce.HTTPNot200(detail=response.json()) - -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): + """ + 获取新的 `access_token` 的方法,子类需要实现 + """ raise NotImplementedError def get_access_token_cache_key(self): + """ + 获取 `access_token` 的缓存 key, 子类需要实现 + """ + raise NotImplementedError + + def add_token(self, kwargs: dict): + """ + 添加 token ,子类需要实现 + """ raise NotImplementedError def is_token_invalid(self, data): - errcode = data['errcode'] - if errcode == self.invalid_token_errcode: + code = data[self.code_key] + if code in self.invalid_token_errcodes: + logger.error(f'OAuth token invalid: {data}') return True return False @@ -69,26 +92,58 @@ class BaseRequest(RequestMixin): 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) + cache.set(self.access_token_cache_key, access_token, expires_in - 10) def raw_request(self, method, url, **kwargs): set_default(kwargs, self._request_kwargs) - raw_data = '' + try: + response = getattr(requests, method)(url, **kwargs) + self.check_http_is_200(response) + raw_data = response.json() + data = DictWrapper(raw_data) + + return data + except req_exce.ReadTimeout as e: + logger.exception(e) + raise exce.NetError + + def token_request(self, method, url, **kwargs): 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) + self.add_token(kwargs) + data = self.raw_request(method, url, **kwargs) - if self.is_token_invalid(data): - self.refresh_access_token() - continue + 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) + return data + logger.error(f'Get access_token error, check config: url={url} data={data.raw_data}') + raise PermissionDenied(data.raw_data) + + def get(self, url, params=None, with_token=True, + check_errcode_is_0=True, **kwargs): + # self.request ... + pass + get = as_request(get) + + def post(self, url, params=None, json=None, + with_token=True, check_errcode_is_0=True, + **kwargs): + # self.request ... + pass + post = as_request(post) + + def request(self, method, url, + with_token=True, + check_errcode_is_0=True, + **kwargs): + + if with_token: + data = self.token_request(method, url, **kwargs) + else: + data = self.raw_request(method, url, **kwargs) + + if check_errcode_is_0: + self.check_errcode_is_0(data) + return data diff --git a/apps/common/message/backends/utils.py b/apps/common/message/backends/utils.py index 5a2f90355..1a1a3fe8c 100644 --- a/apps/common/message/backends/utils.py +++ b/apps/common/message/backends/utils.py @@ -54,7 +54,7 @@ class DictWrapper: return str(self.raw_data) -def request(func): +def as_request(func): def inner(*args, **kwargs): signature = inspect.signature(func) bound_args = signature.bind(*args, **kwargs) diff --git a/apps/common/message/backends/wecom/__init__.py b/apps/common/message/backends/wecom/__init__.py index dd3e34c8a..661a8276c 100644 --- a/apps/common/message/backends/wecom/__init__.py +++ b/apps/common/message/backends/wecom/__init__.py @@ -2,13 +2,9 @@ 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__) @@ -48,7 +44,7 @@ class WeComRequests(BaseRequest): - 确保 status_code == 200 - 确保 access_token 无效时重试 """ - invalid_token_errcode = ErrorCode.INVALID_TOKEN + invalid_token_errcodes = (ErrorCode.INVALID_TOKEN,) def __init__(self, corpid, corpsecret, agentid, timeout=None): self._corpid = corpid @@ -68,35 +64,13 @@ class WeComRequests(BaseRequest): 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): + def add_token(self, kwargs: dict): + params = kwargs.get('params') + if params is None: params = {} + kwargs['params'] = 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 + params['access_token'] = self.access_token class WeCom(RequestMixin): @@ -147,7 +121,7 @@ class WeCom(RequestMixin): if errcode in (ErrorCode.RECIPIENTS_INVALID, ErrorCode.RECIPIENTS_EMPTY): # 全部接收人无权限或不存在 return users - self.check_errcode_is_0(data) + self._requests.check_errcode_is_0(data) invaliduser = data['invaliduser'] if not invaliduser: @@ -173,7 +147,7 @@ class WeCom(RequestMixin): logger.warn(f'WeCom get_user_id_by_code invalid code: code={code}') return None, None - self.check_errcode_is_0(data) + self._requests.check_errcode_is_0(data) USER_ID = 'UserId' OPEN_ID = 'OpenId' diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index b24b1f460..44f7711b7 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -228,6 +228,10 @@ class Config(dict): 'DINGTALK_APPKEY': '', 'DINGTALK_APPSECRET': '', + 'AUTH_FEISHU': False, + 'FEISHU_APP_ID': '', + 'FEISHU_APP_SECRET': '', + '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 79b1e450f..4245abd48 100644 --- a/apps/jumpserver/context_processor.py +++ b/apps/jumpserver/context_processor.py @@ -16,6 +16,7 @@ def jumpserver_processor(request): 'LOGIN_CAS_LOGO_URL': static('img/login_cas_logo.png'), 'LOGIN_WECOM_LOGO_URL': static('img/login_wecom_logo.png'), 'LOGIN_DINGTALK_LOGO_URL': static('img/login_dingtalk_logo.png'), + 'LOGIN_FEISHU_LOGO_URL': static('img/login_feishu_logo.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 264566620..d8e96e673 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -117,6 +117,10 @@ DINGTALK_AGENTID = CONFIG.DINGTALK_AGENTID DINGTALK_APPKEY = CONFIG.DINGTALK_APPKEY DINGTALK_APPSECRET = CONFIG.DINGTALK_APPSECRET +# FeiShu auth +AUTH_FEISHU = CONFIG.AUTH_FEISHU +FEISHU_APP_ID = CONFIG.FEISHU_APP_ID +FEISHU_APP_SECRET = CONFIG.FEISHU_APP_SECRET # Other setting TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION @@ -134,12 +138,13 @@ 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' +AUTH_BACKEND_FEISHU = 'authentication.backends.api.FeiShuAuthentication' AUTH_BACKEND_AUTH_TOKEN = 'authentication.backends.api.AuthorizationTokenAuthentication' AUTHENTICATION_BACKENDS = [ AUTH_BACKEND_MODEL, AUTH_BACKEND_PUBKEY, AUTH_BACKEND_WECOM, - AUTH_BACKEND_DINGTALK, AUTH_BACKEND_AUTH_TOKEN + AUTH_BACKEND_DINGTALK, AUTH_BACKEND_FEISHU, AUTH_BACKEND_AUTH_TOKEN, ] if AUTH_CAS: diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 64246b900..7fca22dce 100644 Binary files a/apps/locale/zh/LC_MESSAGES/django.mo and b/apps/locale/zh/LC_MESSAGES/django.mo differ diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index a9d942468..6736d2761 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-08-02 14:17+0800\n" +"POT-Creation-Date: 2021-08-12 10:27+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -20,7 +20,7 @@ msgstr "" #: acls/models/base.py:25 acls/serializers/login_asset_acl.py:47 #: applications/models/application.py:166 assets/models/asset.py:139 #: assets/models/base.py:175 assets/models/cluster.py:18 -#: assets/models/cmd_filter.py:21 assets/models/domain.py:21 +#: assets/models/cmd_filter.py:21 assets/models/domain.py:24 #: assets/models/group.py:20 assets/models/label.py:18 ops/mixin.py:24 #: orgs/models.py:24 perms/models/base.py:49 settings/models.py:29 #: terminal/models/storage.py:23 terminal/models/task.py:16 @@ -56,12 +56,12 @@ msgstr "激活中" #: assets/models/asset.py:144 assets/models/asset.py:220 #: assets/models/base.py:180 assets/models/cluster.py:29 #: assets/models/cmd_filter.py:23 assets/models/cmd_filter.py:64 -#: assets/models/domain.py:22 assets/models/domain.py:53 +#: assets/models/domain.py:25 assets/models/domain.py:65 #: assets/models/group.py:23 assets/models/label.py:23 ops/models/adhoc.py:37 #: orgs/models.py:27 perms/models/base.py:57 settings/models.py:34 #: terminal/models/storage.py:26 terminal/models/terminal.py:114 #: tickets/models/ticket.py:73 users/models/group.py:16 -#: users/models/user.py:589 xpack/plugins/change_auth_plan/models.py:87 +#: users/models/user.py:589 xpack/plugins/change_auth_plan/models.py:77 #: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:108 #: xpack/plugins/gathered_user/models.py:26 msgid "Comment" @@ -92,14 +92,14 @@ 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/models.py:36 audits/models.py:56 audits/models.py:74 #: audits/serializers.py:93 authentication/models.py:44 #: authentication/models.py:97 orgs/models.py:19 orgs/models.py:433 #: 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:176 -#: users/models/user.py:752 users/models/user.py:778 +#: users/models/user.py:757 users/models/user.py:783 #: users/serializers/group.py:19 #: users/templates/users/user_asset_permission.html:38 #: users/templates/users/user_asset_permission.html:64 @@ -118,7 +118,7 @@ msgid "System User" msgstr "系统用户" #: acls/models/login_asset_acl.py:22 -#: applications/serializers/attrs/application_category/remote_app.py:33 +#: applications/serializers/attrs/application_category/remote_app.py:37 #: assets/models/asset.py:357 assets/models/authbook.py:15 #: assets/models/gathered_user.py:14 assets/serializers/system_user.py:200 #: audits/models.py:38 perms/models/asset_permission.py:99 @@ -126,7 +126,7 @@ msgstr "系统用户" #: terminal/backends/command/serializers.py:13 terminal/models/session.py:40 #: users/templates/users/user_asset_permission.html:40 #: users/templates/users/user_asset_permission.html:70 -#: xpack/plugins/change_auth_plan/models.py:320 +#: xpack/plugins/change_auth_plan/models.py:282 #: xpack/plugins/cloud/models.py:212 msgid "Asset" msgstr "资产" @@ -155,8 +155,8 @@ 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:180 assets/models/domain.py:49 -#: assets/serializers/account.py:12 settings/serializers/settings.py:113 +#: assets/models/asset.py:180 assets/models/domain.py:61 +#: assets/serializers/account.py:12 settings/serializers/settings.py:114 #: users/templates/users/_granted_assets.html:26 #: users/templates/users/user_asset_permission.html:156 msgid "IP" @@ -178,11 +178,11 @@ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. " #: applications/serializers/attrs/application_type/mysql_workbench.py:30 #: applications/serializers/attrs/application_type/vmware_client.py:26 #: assets/models/base.py:176 assets/models/gathered_user.py:15 -#: audits/models.py:100 authentication/forms.py:15 authentication/forms.py:17 +#: audits/models.py:105 authentication/forms.py:15 authentication/forms.py:17 #: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:554 #: users/templates/users/_select_user_modal.html:14 -#: xpack/plugins/change_auth_plan/models.py:50 -#: xpack/plugins/change_auth_plan/models.py:316 +#: xpack/plugins/change_auth_plan/models.py:47 +#: xpack/plugins/change_auth_plan/models.py:278 #: xpack/plugins/cloud/serializers.py:67 msgid "Username" msgstr "用户名" @@ -198,7 +198,7 @@ msgstr "" #: acls/serializers/login_asset_acl.py:35 assets/models/asset.py:181 #: assets/serializers/account.py:13 assets/serializers/gathered_user.py:23 -#: settings/serializers/settings.py:112 +#: settings/serializers/settings.py:113 #: users/templates/users/_granted_assets.html:25 #: users/templates/users/user_asset_permission.html:157 msgid "Hostname" @@ -211,7 +211,7 @@ msgid "" msgstr "格式为逗号分隔的字符串, * 表示匹配所有. 可选的协议有: {}" #: acls/serializers/login_asset_acl.py:55 assets/models/asset.py:184 -#: assets/models/domain.py:51 assets/models/user.py:204 +#: assets/models/domain.py:63 assets/models/user.py:204 #: terminal/serializers/session.py:30 terminal/serializers/storage.py:69 msgid "Protocol" msgstr "协议" @@ -270,7 +270,7 @@ msgid "Type" msgstr "类型" #: applications/models/application.py:175 assets/models/asset.py:188 -#: assets/models/domain.py:27 assets/models/domain.py:52 +#: assets/models/domain.py:30 assets/models/domain.py:64 msgid "Domain" msgstr "网域" @@ -303,13 +303,13 @@ msgstr "类型名称" #: assets/models/base.py:177 audits/signals_handler.py:63 #: authentication/forms.py:22 #: authentication/templates/authentication/login.html:164 -#: settings/serializers/settings.py:94 users/forms/profile.py:21 +#: settings/serializers/settings.py:95 users/forms/profile.py:21 #: users/templates/users/user_otp_check_password.html:13 #: users/templates/users/user_password_update.html:43 #: users/templates/users/user_password_verify.html:18 -#: xpack/plugins/change_auth_plan/models.py:71 -#: xpack/plugins/change_auth_plan/models.py:212 -#: xpack/plugins/change_auth_plan/models.py:323 +#: xpack/plugins/change_auth_plan/models.py:68 +#: xpack/plugins/change_auth_plan/models.py:190 +#: xpack/plugins/change_auth_plan/models.py:285 #: xpack/plugins/cloud/serializers.py:69 msgid "Password" msgstr "密码" @@ -360,12 +360,12 @@ msgstr "主机" #: applications/serializers/attrs/application_type/mysql_workbench.py:22 #: applications/serializers/attrs/application_type/oracle.py:11 #: applications/serializers/attrs/application_type/pgsql.py:11 -#: assets/models/asset.py:185 assets/models/domain.py:50 +#: assets/models/asset.py:185 assets/models/domain.py:62 #: xpack/plugins/cloud/serializers.py:66 msgid "Port" msgstr "端口" -#: applications/serializers/attrs/application_category/remote_app.py:36 +#: applications/serializers/attrs/application_category/remote_app.py:40 #: applications/serializers/attrs/application_type/chrome.py:14 #: applications/serializers/attrs/application_type/mysql_workbench.py:14 #: applications/serializers/attrs/application_type/vmware_client.py:18 @@ -431,13 +431,13 @@ msgstr "协议组" #: assets/models/asset.py:189 assets/models/user.py:194 #: perms/models/asset_permission.py:100 -#: xpack/plugins/change_auth_plan/models.py:59 +#: xpack/plugins/change_auth_plan/models.py:56 #: xpack/plugins/gathered_user/models.py:24 msgid "Nodes" msgstr "节点" #: assets/models/asset.py:190 assets/models/cmd_filter.py:22 -#: assets/models/domain.py:54 assets/models/label.py:22 +#: assets/models/domain.py:66 assets/models/label.py:22 #: authentication/models.py:46 msgid "Is active" msgstr "激活" @@ -521,7 +521,7 @@ msgstr "标签管理" #: assets/models/cmd_filter.py:67 assets/models/group.py:21 #: common/db/models.py:70 common/mixins/models.py:49 orgs/models.py:25 #: orgs/models.py:437 perms/models/base.py:55 users/models/user.py:597 -#: users/serializers/group.py:33 xpack/plugins/change_auth_plan/models.py:91 +#: users/serializers/group.py:33 xpack/plugins/change_auth_plan/models.py:81 #: xpack/plugins/cloud/models.py:114 xpack/plugins/gathered_user/models.py:30 msgid "Created by" msgstr "创建者" @@ -529,12 +529,12 @@ msgstr "创建者" # msgid "Created by" # msgstr "创建者" #: assets/models/asset.py:219 assets/models/base.py:181 -#: assets/models/cluster.py:26 assets/models/domain.py:24 +#: assets/models/cluster.py:26 assets/models/domain.py:27 #: assets/models/gathered_user.py:19 assets/models/group.py:22 #: 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:26 #: orgs/models.py:435 perms/models/base.py:56 users/models/group.py:18 -#: users/models/user.py:779 xpack/plugins/cloud/models.py:117 +#: users/models/user.py:784 xpack/plugins/cloud/models.py:117 msgid "Date created" msgstr "创建日期" @@ -554,7 +554,8 @@ msgstr "未知" msgid "Ok" msgstr "成功" -#: assets/models/base.py:32 audits/models.py:97 xpack/plugins/cloud/const.py:27 +#: assets/models/base.py:32 audits/models.py:102 +#: xpack/plugins/cloud/const.py:27 msgid "Failed" msgstr "失败" @@ -566,15 +567,15 @@ msgstr "可连接性" msgid "Date verified" msgstr "校验日期" -#: assets/models/base.py:178 xpack/plugins/change_auth_plan/models.py:81 -#: xpack/plugins/change_auth_plan/models.py:219 -#: xpack/plugins/change_auth_plan/models.py:330 +#: assets/models/base.py:178 xpack/plugins/change_auth_plan/models.py:72 +#: xpack/plugins/change_auth_plan/models.py:197 +#: xpack/plugins/change_auth_plan/models.py:292 msgid "SSH private key" msgstr "SSH密钥" -#: assets/models/base.py:179 xpack/plugins/change_auth_plan/models.py:84 -#: xpack/plugins/change_auth_plan/models.py:215 -#: xpack/plugins/change_auth_plan/models.py:326 +#: assets/models/base.py:179 xpack/plugins/change_auth_plan/models.py:75 +#: xpack/plugins/change_auth_plan/models.py:193 +#: xpack/plugins/change_auth_plan/models.py:288 msgid "SSH public key" msgstr "SSH公钥" @@ -618,7 +619,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:764 +#: users/models/user.py:769 msgid "System" msgstr "系统" @@ -667,16 +668,16 @@ msgstr "命令过滤规则" msgid "Command confirm" msgstr "命令复核" -#: assets/models/domain.py:61 +#: assets/models/domain.py:73 msgid "Gateway" msgstr "网关" -#: assets/models/domain.py:84 +#: assets/models/domain.py:127 #, python-brace-format msgid "Unable to connect to port {port} on {ip}" msgstr "无法连接到 {ip} 上的端口 {port}" -#: assets/models/domain.py:87 +#: assets/models/domain.py:130 msgid "Authentication failed" msgstr "认证失败" @@ -753,7 +754,7 @@ msgid "Username same with user" msgstr "用户名与用户相同" #: assets/models/user.py:196 assets/serializers/domain.py:28 -#: templates/_nav.html:39 xpack/plugins/change_auth_plan/models.py:55 +#: templates/_nav.html:39 xpack/plugins/change_auth_plan/models.py:52 msgid "Assets" msgstr "资产" @@ -956,7 +957,7 @@ msgstr "更新资产硬件信息: {}" msgid "Update node asset hardware information: {}" msgstr "更新节点资产硬件信息: {}" -#: assets/tasks/gather_asset_users.py:108 +#: assets/tasks/gather_asset_users.py:111 msgid "Gather assets users" msgstr "收集资产上的用户" @@ -1053,7 +1054,7 @@ msgstr "创建目录" msgid "Symlink" msgstr "建立软链接" -#: audits/models.py:37 audits/models.py:60 audits/models.py:71 +#: audits/models.py:37 audits/models.py:60 audits/models.py:76 #: terminal/models/session.py:45 msgid "Remote addr" msgstr "远端地址" @@ -1066,7 +1067,7 @@ msgstr "操作" msgid "Filename" msgstr "文件名" -#: audits/models.py:42 audits/models.py:96 +#: audits/models.py:42 audits/models.py:101 msgid "Success" msgstr "成功" @@ -1076,8 +1077,8 @@ msgstr "成功" #: tickets/serializers/ticket/meta/ticket_type/apply_application.py:74 #: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:40 #: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:78 -#: xpack/plugins/change_auth_plan/models.py:199 -#: xpack/plugins/change_auth_plan/models.py:345 +#: xpack/plugins/change_auth_plan/models.py:177 +#: xpack/plugins/change_auth_plan/models.py:307 #: xpack/plugins/gathered_user/models.py:76 msgid "Date start" msgstr "开始日期" @@ -1102,45 +1103,45 @@ msgstr "资源类型" msgid "Resource" msgstr "资源" -#: audits/models.py:61 audits/models.py:72 +#: audits/models.py:61 audits/models.py:77 msgid "Datetime" msgstr "日期" -#: audits/models.py:70 +#: audits/models.py:75 msgid "Change by" msgstr "修改者" -#: audits/models.py:90 +#: audits/models.py:95 msgid "Disabled" msgstr "禁用" -#: audits/models.py:91 settings/models.py:33 +#: audits/models.py:96 settings/models.py:33 msgid "Enabled" msgstr "启用" -#: audits/models.py:92 +#: audits/models.py:97 msgid "-" msgstr "" -#: audits/models.py:101 +#: audits/models.py:106 msgid "Login type" msgstr "登录方式" -#: audits/models.py:102 +#: audits/models.py:107 #: tickets/serializers/ticket/meta/ticket_type/login_confirm.py:14 msgid "Login ip" msgstr "登录IP" -#: audits/models.py:103 +#: audits/models.py:108 #: tickets/serializers/ticket/meta/ticket_type/login_confirm.py:17 msgid "Login city" msgstr "登录城市" -#: audits/models.py:104 audits/serializers.py:44 +#: audits/models.py:109 audits/serializers.py:44 msgid "User agent" msgstr "用户代理" -#: audits/models.py:105 +#: audits/models.py:110 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 #: authentication/templates/authentication/login_otp.html:6 #: users/forms/profile.py:64 users/models/user.py:578 @@ -1148,21 +1149,21 @@ msgstr "用户代理" msgid "MFA" msgstr "多因子认证" -#: audits/models.py:106 xpack/plugins/change_auth_plan/models.py:341 +#: audits/models.py:111 xpack/plugins/change_auth_plan/models.py:303 #: xpack/plugins/cloud/models.py:171 msgid "Reason" msgstr "原因" -#: audits/models.py:107 tickets/models/ticket.py:47 +#: audits/models.py:112 tickets/models/ticket.py:47 #: xpack/plugins/cloud/models.py:167 xpack/plugins/cloud/models.py:216 msgid "Status" msgstr "状态" -#: audits/models.py:108 +#: audits/models.py:113 msgid "Date login" msgstr "登录日期" -#: audits/models.py:109 +#: audits/models.py:114 msgid "Authentication backend" msgstr "认证方式" @@ -1222,13 +1223,13 @@ msgstr "认证令牌" #: audits/signals_handler.py:66 #: authentication/templates/authentication/login.html:210 -#: notifications/backends/__init__.py:12 +#: notifications/backends/__init__.py:13 msgid "WeCom" msgstr "企业微信" #: audits/signals_handler.py:67 #: authentication/templates/authentication/login.html:215 -#: notifications/backends/__init__.py:13 +#: notifications/backends/__init__.py:14 msgid "DingTalk" msgstr "钉钉" @@ -1401,7 +1402,7 @@ msgstr "{ApplicationPermission} *添加了* {SystemUser}" msgid "{ApplicationPermission} *REMOVE* {SystemUser}" msgstr "{ApplicationPermission} *移除了* {SystemUser}" -#: authentication/api/connection_token.py:268 +#: authentication/api/connection_token.py:222 msgid "Invalid token" msgstr "无效的令牌" @@ -1577,7 +1578,7 @@ msgstr "登录完成前,请先修改密码" msgid "Your password has expired, please reset before logging in" msgstr "您的密码已过期,先修改再登录" -#: authentication/errors.py:320 +#: authentication/errors.py:325 msgid "Your password is invalid" msgstr "您的密码无效" @@ -1633,7 +1634,7 @@ msgid "Show" msgstr "显示" #: authentication/templates/authentication/_access_key_modal.html:66 -#: settings/serializers/settings.py:148 users/models/user.py:463 +#: settings/serializers/settings.py:149 users/models/user.py:463 #: users/serializers/profile.py:99 #: users/templates/users/user_verify_mfa.html:32 msgid "Disable" @@ -1707,6 +1708,11 @@ msgstr "OpenID" msgid "CAS" msgstr "CAS" +#: authentication/templates/authentication/login.html:220 +#: notifications/backends/__init__.py:16 +msgid "FeiShu" +msgstr "" + #: authentication/templates/authentication/login_otp.html:17 msgid "One-time password" msgstr "一次性密码" @@ -1752,7 +1758,8 @@ msgstr "钉钉错误,请联系系统管理员" msgid "DingTalk Error" msgstr "钉钉错误" -#: authentication/views/dingtalk.py:56 authentication/views/wecom.py:56 +#: authentication/views/dingtalk.py:56 authentication/views/feishu.py:55 +#: authentication/views/wecom.py:56 msgid "You've been hacked" msgstr "你被攻击了" @@ -1760,7 +1767,8 @@ msgstr "你被攻击了" msgid "DingTalk is already bound" msgstr "钉钉已经绑定" -#: authentication/views/dingtalk.py:105 authentication/views/wecom.py:104 +#: authentication/views/dingtalk.py:105 authentication/views/feishu.py:102 +#: authentication/views/wecom.py:104 msgid "Please verify your password first" msgstr "请检查密码" @@ -1789,7 +1797,8 @@ msgstr "从钉钉获取用户失败" msgid "DingTalk is not bound" msgstr "钉钉没有绑定" -#: authentication/views/dingtalk.py:218 authentication/views/wecom.py:216 +#: authentication/views/dingtalk.py:218 authentication/views/feishu.py:208 +#: authentication/views/wecom.py:216 msgid "Please login with a password and then bind the WeCom" msgstr "请使用密码登录,然后绑定企业微信" @@ -1797,6 +1806,43 @@ msgstr "请使用密码登录,然后绑定企业微信" msgid "Binding DingTalk failed" msgstr "绑定钉钉失败" +#: authentication/views/feishu.py:40 +msgid "FeiShu Error, Please contact your system administrator" +msgstr "飞书错误,请联系系统管理员" + +#: authentication/views/feishu.py:43 +msgid "FeiShu Error" +msgstr "飞书错误" + +#: authentication/views/feishu.py:89 +msgid "FeiShu is already bound" +msgstr "飞书已经绑定" + +#: authentication/views/feishu.py:136 +msgid "FeiShu query user failed" +msgstr "飞书查询用户失败" + +#: authentication/views/feishu.py:145 +msgid "The FeiShu is already bound to another user" +msgstr "该飞书已经绑定其他用户" + +#: authentication/views/feishu.py:150 authentication/views/feishu.py:232 +#: authentication/views/feishu.py:233 +msgid "Binding FeiShu successfully" +msgstr "绑定 飞书 成功" + +#: authentication/views/feishu.py:201 +msgid "Failed to get user from FeiShu" +msgstr "从飞书获取用户失败" + +#: authentication/views/feishu.py:207 +msgid "FeiShu is not bound" +msgstr "没有绑定飞书" + +#: authentication/views/feishu.py:250 authentication/views/feishu.py:251 +msgid "Binding FeiShu failed" +msgstr "绑定飞书失败" + #: authentication/views/login.py:78 msgid "Redirecting" msgstr "跳转中" @@ -1809,7 +1855,7 @@ msgstr "正在跳转到 {} 认证" msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: authentication/views/login.py:223 +#: authentication/views/login.py:224 msgid "" "Wait for {} confirm, You also can copy link to her/him
\n" " Don't close this page" @@ -1817,15 +1863,15 @@ msgstr "" "等待 {} 确认, 你也可以复制链接发给他/她
\n" " 不要关闭本页面" -#: authentication/views/login.py:228 +#: authentication/views/login.py:229 msgid "No ticket found" msgstr "没有发现工单" -#: authentication/views/login.py:260 +#: authentication/views/login.py:261 msgid "Logout success" msgstr "退出登录成功" -#: authentication/views/login.py:261 +#: authentication/views/login.py:262 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" @@ -1949,7 +1995,7 @@ msgstr "加密的字段" msgid "Network error, please contact system administrator" msgstr "网络错误,请联系系统管理员" -#: common/message/backends/wecom/__init__.py:19 +#: common/message/backends/wecom/__init__.py:15 msgid "WeCom error, please contact system administrator" msgstr "企业微信错误,请联系系统管理员" @@ -1981,7 +2027,7 @@ msgstr "字段必须唯一" msgid "Should not contains special characters" msgstr "不能包含特殊字符" -#: jumpserver/context_processor.py:19 +#: jumpserver/context_processor.py:20 msgid "JumpServer Open Source Bastion Host" msgstr "JumpServer 开源堡垒机" @@ -2012,12 +2058,12 @@ msgstr "" "div>
如果你看到了这个页面,证明你访问的不是nginx监听的端口,祝你好运" -#: notifications/backends/__init__.py:11 users/forms/profile.py:101 +#: notifications/backends/__init__.py:12 users/forms/profile.py:101 #: users/models/user.py:558 msgid "Email" msgstr "邮件" -#: notifications/backends/__init__.py:14 +#: notifications/backends/__init__.py:15 msgid "Site message" msgstr "站内信" @@ -2029,7 +2075,7 @@ msgstr "等待任务开始" msgid "Not has host {} permission" msgstr "没有该主机 {} 权限" -#: ops/apps.py:9 ops/notifications.py:16 +#: ops/apps.py:9 ops/notifications.py:14 msgid "Operations" msgstr "运维" @@ -2042,7 +2088,7 @@ msgid "Regularly perform" msgstr "定期执行" #: ops/mixin.py:106 ops/mixin.py:147 -#: xpack/plugins/change_auth_plan/serializers.py:59 +#: xpack/plugins/change_auth_plan/serializers.py:55 msgid "Periodic perform" msgstr "定时执行" @@ -2121,8 +2167,8 @@ msgstr "开始时间" msgid "End time" msgstr "完成时间" -#: ops/models/adhoc.py:246 xpack/plugins/change_auth_plan/models.py:202 -#: xpack/plugins/change_auth_plan/models.py:348 +#: ops/models/adhoc.py:246 xpack/plugins/change_auth_plan/models.py:180 +#: xpack/plugins/change_auth_plan/models.py:310 #: xpack/plugins/gathered_user/models.py:79 msgid "Time" msgstr "时间" @@ -2156,31 +2202,26 @@ msgstr "命令 `{}` 不允许被执行 ......." msgid "Task end" msgstr "任务结束" -#: ops/notifications.py:17 +#: ops/notifications.py:15 msgid "Server performance" msgstr "监控告警" -#: ops/notifications.py:86 -msgid "The terminal is offline: {}" +#: ops/notifications.py:36 +#, python-brace-format +msgid "[Alive] The terminal is offline: {name}" msgstr "" -#: ops/notifications.py:103 -#, fuzzy -#| msgid "Disk used more than 80%: {} => {}" -msgid "Disk used more than {}%: {} => {} ({})" -msgstr "磁盘使用率超过 80%: {} => {}" +#: ops/notifications.py:42 +msgid "[Disk] Disk used more than {max_threshold}%: => {value} ({name})" +msgstr "[Disk] 硬盘使用率超过 {max_threshold}%: => {value} ({name})" -#: ops/notifications.py:128 -#, fuzzy -#| msgid "Disk used more than 80%: {} => {}" -msgid "CPU load more than {}: => {} ({})" -msgstr "磁盘使用率超过 80%: {} => {}" +#: ops/notifications.py:49 +msgid "[Memory] Memory used more than {max_threshold}%: => {value} ({name})" +msgstr "[Memory] 内存使用率超过 {max_threshold}%: => {value} ({name})" -#: ops/notifications.py:142 -#, fuzzy -#| msgid "Disk used more than 80%: {} => {}" -msgid "Memory used more than {}%: => {} ({})" -msgstr "磁盘使用率超过 80%: {} => {}" +#: ops/notifications.py:56 +msgid "[CPU] CPU load more than {max_threshold}: => {value} ({name})" +msgstr "[CPU] CPU 使用率超过 {max_threshold}: => {value} ({name})" #: ops/tasks.py:71 msgid "Clean task history period" @@ -2241,7 +2282,7 @@ msgstr "该授权暂时不能撤销" msgid "Application" msgstr "应用程序" -#: perms/models/asset_permission.py:37 settings/serializers/settings.py:117 +#: perms/models/asset_permission.py:37 settings/serializers/settings.py:118 msgid "All" msgstr "全部" @@ -2371,7 +2412,8 @@ msgstr "邮件已经发送{}, 请检查" msgid "Welcome to the JumpServer open source Bastion Host" msgstr "欢迎使用JumpServer开源堡垒机" -#: settings/api/dingtalk.py:36 settings/api/wecom.py:36 +#: settings/api/dingtalk.py:36 settings/api/feishu.py:35 +#: settings/api/wecom.py:36 msgid "Test success" msgstr "测试成功" @@ -2387,27 +2429,27 @@ msgstr "成功导入 {} 个用户 ( 组织: {} )" msgid "Setting" msgstr "设置" -#: settings/serializers/settings.py:15 +#: settings/serializers/settings.py:16 msgid "Site url" msgstr "当前站点URL" -#: settings/serializers/settings.py:16 +#: settings/serializers/settings.py:17 msgid "eg: http://dev.jumpserver.org:8080" msgstr "如: http://dev.jumpserver.org:8080" -#: settings/serializers/settings.py:20 +#: settings/serializers/settings.py:21 msgid "User guide url" msgstr "用户向导URL" -#: settings/serializers/settings.py:21 +#: settings/serializers/settings.py:22 msgid "User first login update profile done redirect to it" msgstr "用户第一次登录,修改profile后重定向到地址, 可以是 wiki 或 其他说明文档" -#: settings/serializers/settings.py:24 +#: settings/serializers/settings.py:25 msgid "Forgot password url" msgstr "忘记密码URL" -#: settings/serializers/settings.py:25 +#: settings/serializers/settings.py:26 msgid "" "The forgot password url on login page, If you use ldap or cas external " "authentication, you can set it" @@ -2415,138 +2457,138 @@ msgstr "" "登录页面忘记密码URL, 如果使用了 LDAP, OPENID 等外部认证系统,可以自定义用户重" "置密码访问的地址" -#: settings/serializers/settings.py:29 +#: settings/serializers/settings.py:30 msgid "Global organization name" msgstr "全局组织名" -#: settings/serializers/settings.py:30 +#: settings/serializers/settings.py:31 msgid "The name of global organization to display" msgstr "全局组织的显示名称,默认为 全局组织" -#: settings/serializers/settings.py:37 +#: settings/serializers/settings.py:38 msgid "SMTP host" msgstr "SMTP 主机" -#: settings/serializers/settings.py:38 +#: settings/serializers/settings.py:39 msgid "SMTP port" msgstr "SMTP 端口" -#: settings/serializers/settings.py:39 +#: settings/serializers/settings.py:40 msgid "SMTP account" msgstr "SMTP 账号" -#: settings/serializers/settings.py:41 +#: settings/serializers/settings.py:42 msgid "SMTP password" msgstr "SMTP 密码" -#: settings/serializers/settings.py:42 +#: settings/serializers/settings.py:43 msgid "Tips: Some provider use token except password" msgstr "提示:一些邮件提供商需要输入的是授权码" -#: settings/serializers/settings.py:45 +#: settings/serializers/settings.py:46 msgid "Send user" msgstr "发件人" -#: settings/serializers/settings.py:46 +#: settings/serializers/settings.py:47 msgid "Tips: Send mail account, default SMTP account as the send account" msgstr "提示:发送邮件账号,默认使用 SMTP 账号作为发送账号" -#: settings/serializers/settings.py:49 +#: settings/serializers/settings.py:50 msgid "Test recipient" msgstr "测试收件人" -#: settings/serializers/settings.py:50 +#: settings/serializers/settings.py:51 msgid "Tips: Used only as a test mail recipient" msgstr "提示:仅用来作为测试邮件收件人" -#: settings/serializers/settings.py:53 +#: settings/serializers/settings.py:54 msgid "Use SSL" msgstr "使用 SSL" -#: settings/serializers/settings.py:54 +#: settings/serializers/settings.py:55 msgid "If SMTP port is 465, may be select" msgstr "如果SMTP端口是465,通常需要启用 SSL" -#: settings/serializers/settings.py:57 +#: settings/serializers/settings.py:58 msgid "Use TLS" msgstr "使用 TLS" -#: settings/serializers/settings.py:58 +#: settings/serializers/settings.py:59 msgid "If SMTP port is 587, may be select" msgstr "如果SMTP端口是587,通常需要启用 TLS" -#: settings/serializers/settings.py:61 +#: settings/serializers/settings.py:62 msgid "Subject prefix" msgstr "主题前缀" -#: settings/serializers/settings.py:68 +#: settings/serializers/settings.py:69 msgid "Create user email subject" msgstr "邮件主题" -#: settings/serializers/settings.py:69 +#: settings/serializers/settings.py:70 msgid "" "Tips: When creating a user, send the subject of the email (eg:Create account " "successfully)" msgstr "提示: 创建用户时,发送设置密码邮件的主题 (例如: 创建用户成功)" -#: settings/serializers/settings.py:73 +#: settings/serializers/settings.py:74 msgid "Create user honorific" msgstr "邮件的敬语" -#: settings/serializers/settings.py:74 +#: settings/serializers/settings.py:75 msgid "Tips: When creating a user, send the honorific of the email (eg:Hello)" msgstr "提示: 创建用户时,发送设置密码邮件的敬语 (例如: 您好)" -#: settings/serializers/settings.py:78 +#: settings/serializers/settings.py:79 msgid "Create user email content" msgstr "邮件的内容" -#: settings/serializers/settings.py:79 +#: settings/serializers/settings.py:80 msgid "Tips:When creating a user, send the content of the email" msgstr "提示: 创建用户时,发送设置密码邮件的内容" -#: settings/serializers/settings.py:82 +#: settings/serializers/settings.py:83 msgid "Signature" msgstr "署名" -#: settings/serializers/settings.py:83 +#: settings/serializers/settings.py:84 msgid "Tips: Email signature (eg:jumpserver)" msgstr "邮件署名 (如:jumpserver)" -#: settings/serializers/settings.py:91 +#: settings/serializers/settings.py:92 msgid "LDAP server" msgstr "LDAP 地址" -#: settings/serializers/settings.py:91 +#: settings/serializers/settings.py:92 msgid "eg: ldap://localhost:389" msgstr "如: ldap://localhost:389" -#: settings/serializers/settings.py:93 +#: settings/serializers/settings.py:94 msgid "Bind DN" msgstr "绑定 DN" -#: settings/serializers/settings.py:96 +#: settings/serializers/settings.py:97 msgid "User OU" msgstr "用户 OU" -#: settings/serializers/settings.py:97 +#: settings/serializers/settings.py:98 msgid "Use | split multi OUs" msgstr "多个 OU 使用 | 分割" -#: settings/serializers/settings.py:100 +#: settings/serializers/settings.py:101 msgid "User search filter" msgstr "用户过滤器" -#: settings/serializers/settings.py:101 +#: settings/serializers/settings.py:102 #, python-format msgid "Choice may be (cn|uid|sAMAccountName)=%(user)s)" msgstr "可能的选项是(cn或uid或sAMAccountName=%(user)s)" -#: settings/serializers/settings.py:104 +#: settings/serializers/settings.py:105 msgid "User attr map" msgstr "用户属性映射" -#: settings/serializers/settings.py:105 +#: settings/serializers/settings.py:106 msgid "" "User attr map present how to map LDAP user attr to jumpserver, username,name," "email is jumpserver attr" @@ -2554,23 +2596,23 @@ msgstr "" "用户属性映射代表怎样将LDAP中用户属性映射到jumpserver用户上,username, name," "email 是jumpserver的用户需要属性" -#: settings/serializers/settings.py:107 +#: settings/serializers/settings.py:108 msgid "Enable LDAP auth" msgstr "启用 LDAP 认证" -#: settings/serializers/settings.py:118 +#: settings/serializers/settings.py:119 msgid "Auto" msgstr "自动" -#: settings/serializers/settings.py:124 +#: settings/serializers/settings.py:125 msgid "Password auth" msgstr "密码认证" -#: settings/serializers/settings.py:126 +#: settings/serializers/settings.py:127 msgid "Public key auth" msgstr "密钥认证" -#: settings/serializers/settings.py:127 +#: settings/serializers/settings.py:128 msgid "" "Tips: If use other auth method, like AD/LDAP, you should disable this to " "avoid being able to log in after deleting" @@ -2578,19 +2620,19 @@ msgstr "" "提示:如果你使用其它认证方式,如 AD/LDAP,你应该禁用此项,以避免第三方系统删" "除后,还可以登录" -#: settings/serializers/settings.py:130 +#: settings/serializers/settings.py:131 msgid "List sort by" msgstr "资产列表排序" -#: settings/serializers/settings.py:131 +#: settings/serializers/settings.py:132 msgid "List page size" msgstr "资产列表每页数量" -#: settings/serializers/settings.py:133 +#: settings/serializers/settings.py:134 msgid "Session keep duration" msgstr "会话日志保存时间" -#: settings/serializers/settings.py:134 +#: settings/serializers/settings.py:135 msgid "" "Units: days, Session, record, command will be delete if more than duration, " "only in database" @@ -2598,76 +2640,76 @@ msgstr "" "单位:天。 会话、录像、命令记录超过该时长将会被删除(仅影响数据库存储, oss等不" "受影响)" -#: settings/serializers/settings.py:136 +#: settings/serializers/settings.py:137 msgid "Telnet login regex" msgstr "Telnet 成功正则表达式" -#: settings/serializers/settings.py:138 +#: settings/serializers/settings.py:139 msgid "RDP address" msgstr "RDP 地址" -#: settings/serializers/settings.py:141 +#: settings/serializers/settings.py:142 msgid "RDP visit address, eg: dev.jumpserver.org:3389" msgstr "RDP 访问地址, 如: dev.jumpserver.org:3389" -#: settings/serializers/settings.py:149 +#: settings/serializers/settings.py:150 msgid "All users" msgstr "所有用户" -#: settings/serializers/settings.py:150 +#: settings/serializers/settings.py:151 msgid "Only admin users" msgstr "仅管理员" -#: settings/serializers/settings.py:152 +#: settings/serializers/settings.py:153 msgid "Global MFA auth" msgstr "全局启用 MFA 认证" -#: settings/serializers/settings.py:155 +#: settings/serializers/settings.py:156 msgid "Batch command execution" msgstr "批量命令执行" -#: settings/serializers/settings.py:156 +#: settings/serializers/settings.py:157 msgid "Allow user run batch command or not using ansible" msgstr "是否允许用户使用 ansible 执行批量命令" -#: settings/serializers/settings.py:159 +#: settings/serializers/settings.py:160 msgid "Enable terminal register" msgstr "终端注册" -#: settings/serializers/settings.py:160 +#: settings/serializers/settings.py:161 msgid "" "Allow terminal register, after all terminal setup, you should disable this " "for security" msgstr "是否允许终端注册,当所有终端启动后,为了安全应该关闭" -#: settings/serializers/settings.py:164 +#: settings/serializers/settings.py:165 msgid "Limit the number of login failures" msgstr "限制登录失败次数" -#: settings/serializers/settings.py:168 +#: settings/serializers/settings.py:169 msgid "Block logon interval" msgstr "禁止登录时间间隔" -#: settings/serializers/settings.py:169 +#: settings/serializers/settings.py:170 msgid "" "Tip: (unit/minute) if the user has failed to log in for a limited number of " "times, no login is allowed during this time interval." msgstr "" "提示:(单位:分)当用户登录失败次数达到限制后,那么在此时间间隔内禁止登录" -#: settings/serializers/settings.py:173 +#: settings/serializers/settings.py:174 msgid "Connection max idle time" msgstr "连接最大空闲时间" -#: settings/serializers/settings.py:174 +#: settings/serializers/settings.py:175 msgid "If idle time more than it, disconnect connection Unit: minute" msgstr "提示:如果超过该配置没有操作,连接会被断开 (单位:分)" -#: settings/serializers/settings.py:178 +#: settings/serializers/settings.py:179 msgid "User password expiration" msgstr "用户密码过期时间" -#: settings/serializers/settings.py:179 +#: settings/serializers/settings.py:180 msgid "" "Tip: (unit: day) If the user does not update the password during the time, " "the user password will expire failure;The password expiration reminder mail " @@ -2677,60 +2719,64 @@ msgstr "" "提示:(单位:天)如果用户在此期间没有更新密码,用户密码将过期失效; 密码过期" "提醒邮件将在密码过期前5天内由系统(每天)自动发送给用户" -#: settings/serializers/settings.py:183 +#: settings/serializers/settings.py:184 msgid "Number of repeated historical passwords" msgstr "不能设置近几次密码" -#: settings/serializers/settings.py:184 +#: settings/serializers/settings.py:185 msgid "" "Tip: When the user resets the password, it cannot be the previous n " "historical passwords of the user" msgstr "提示:用户重置密码时,不能为该用户前几次使用过的密码" -#: settings/serializers/settings.py:188 +#: settings/serializers/settings.py:189 msgid "Password minimum length" msgstr "密码最小长度" -#: settings/serializers/settings.py:192 +#: settings/serializers/settings.py:193 msgid "Admin user password minimum length" msgstr "管理员密码最小长度" -#: settings/serializers/settings.py:195 +#: settings/serializers/settings.py:196 msgid "Must contain capital" msgstr "必须包含大写字符" -#: settings/serializers/settings.py:197 +#: settings/serializers/settings.py:198 msgid "Must contain lowercase" msgstr "必须包含小写字符" -#: settings/serializers/settings.py:198 +#: settings/serializers/settings.py:199 msgid "Must contain numeric" msgstr "必须包含数字" -#: settings/serializers/settings.py:199 +#: settings/serializers/settings.py:200 msgid "Must contain special" msgstr "必须包含特殊字符" -#: settings/serializers/settings.py:200 +#: settings/serializers/settings.py:201 msgid "Insecure command alert" msgstr "危险命令告警" -#: settings/serializers/settings.py:202 +#: settings/serializers/settings.py:203 msgid "Email recipient" msgstr "邮件收件人" -#: settings/serializers/settings.py:203 +#: settings/serializers/settings.py:204 msgid "Multiple user using , split" msgstr "多个用户,使用 , 分割" -#: settings/serializers/settings.py:211 +#: settings/serializers/settings.py:212 msgid "Enable WeCom Auth" msgstr "启用企业微信认证" -#: settings/serializers/settings.py:218 +#: settings/serializers/settings.py:219 msgid "Enable DingTalk Auth" msgstr "启用钉钉认证" +#: settings/serializers/settings.py:225 +msgid "Enable FeiShu Auth" +msgstr "启用飞书认证" + #: settings/utils/ldap.py:412 msgid "ldap:// or ldaps:// protocol is used." msgstr "使用 ldap:// 或 ldaps:// 协议" @@ -4027,7 +4073,7 @@ msgstr "工单已处理 - {} ({})" msgid "Your ticket has been processed, processor - {}" msgstr "你的工单已被处理, 处理人 - {}" -#: users/api/user.py:215 +#: users/api/user.py:214 msgid "Could not reset self otp, use profile reset instead" msgstr "不能在该页面重置多因子认证, 请去个人信息页面重置" @@ -4151,11 +4197,11 @@ msgstr "最后更新密码日期" msgid "Need update password" msgstr "需要更新密码" -#: users/models/user.py:760 +#: users/models/user.py:765 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:763 +#: users/models/user.py:768 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" @@ -4187,8 +4233,8 @@ msgstr "生成重置密码链接,通过邮件发送给用户" msgid "Set password" msgstr "设置密码" -#: users/serializers/user.py:27 xpack/plugins/change_auth_plan/models.py:64 -#: xpack/plugins/change_auth_plan/serializers.py:31 +#: users/serializers/user.py:27 xpack/plugins/change_auth_plan/models.py:61 +#: xpack/plugins/change_auth_plan/serializers.py:30 msgid "Password strategy" msgstr "密码策略" @@ -4815,150 +4861,93 @@ msgid "Reset password success, return to login page" msgstr "重置密码成功,返回到登录页面" #: xpack/plugins/change_auth_plan/meta.py:9 -#: xpack/plugins/change_auth_plan/models.py:99 -#: xpack/plugins/change_auth_plan/models.py:206 +#: xpack/plugins/change_auth_plan/models.py:89 +#: xpack/plugins/change_auth_plan/models.py:184 msgid "Change auth plan" msgstr "改密计划" -#: xpack/plugins/change_auth_plan/models.py:39 +#: xpack/plugins/change_auth_plan/models.py:41 msgid "Custom password" msgstr "自定义密码" -#: xpack/plugins/change_auth_plan/models.py:40 +#: xpack/plugins/change_auth_plan/models.py:42 msgid "All assets use the same random password" msgstr "所有资产使用相同的随机密码" -#: xpack/plugins/change_auth_plan/models.py:41 +#: xpack/plugins/change_auth_plan/models.py:43 msgid "All assets use different random password" msgstr "所有资产使用不同的随机密码" -#: xpack/plugins/change_auth_plan/models.py:45 -msgid "Append SSH KEY" -msgstr "追加新密钥" - -#: xpack/plugins/change_auth_plan/models.py:46 -msgid "Empty and append SSH KEY" -msgstr "清空所有密钥再追加新密钥" - -#: xpack/plugins/change_auth_plan/models.py:47 -msgid "Empty current user and append SSH KEY" -msgstr "清空当前账号密钥再追加新密钥" - -#: xpack/plugins/change_auth_plan/models.py:68 +#: xpack/plugins/change_auth_plan/models.py:65 msgid "Password rules" msgstr "密码规则" -#: xpack/plugins/change_auth_plan/models.py:77 -#, fuzzy -#| msgid "Hostname strategy" -msgid "SSH key strategy" -msgstr "主机名策略" - -#: xpack/plugins/change_auth_plan/models.py:194 -msgid "Manual trigger" -msgstr "手动触发" - -#: xpack/plugins/change_auth_plan/models.py:195 -msgid "Timing trigger" -msgstr "定时触发" - -#: xpack/plugins/change_auth_plan/models.py:209 +#: xpack/plugins/change_auth_plan/models.py:187 msgid "Change auth plan snapshot" msgstr "改密计划快照" -#: xpack/plugins/change_auth_plan/models.py:223 -#: xpack/plugins/change_auth_plan/serializers.py:163 -msgid "Trigger mode" -msgstr "触发模式" - -#: xpack/plugins/change_auth_plan/models.py:228 -#: xpack/plugins/change_auth_plan/models.py:334 +#: xpack/plugins/change_auth_plan/models.py:202 +#: xpack/plugins/change_auth_plan/models.py:296 msgid "Change auth plan execution" msgstr "改密计划执行" -#: xpack/plugins/change_auth_plan/models.py:307 +#: xpack/plugins/change_auth_plan/models.py:269 msgid "Ready" msgstr "" -#: xpack/plugins/change_auth_plan/models.py:308 +#: xpack/plugins/change_auth_plan/models.py:270 msgid "Preflight check" msgstr "" -#: xpack/plugins/change_auth_plan/models.py:309 +#: xpack/plugins/change_auth_plan/models.py:271 msgid "Change auth" msgstr "" -#: xpack/plugins/change_auth_plan/models.py:310 +#: xpack/plugins/change_auth_plan/models.py:272 msgid "Verify auth" msgstr "" -#: xpack/plugins/change_auth_plan/models.py:311 +#: xpack/plugins/change_auth_plan/models.py:273 msgid "Keep auth" msgstr "" -#: xpack/plugins/change_auth_plan/models.py:312 +#: xpack/plugins/change_auth_plan/models.py:274 msgid "Finished" msgstr "结束" -#: xpack/plugins/change_auth_plan/models.py:338 +#: xpack/plugins/change_auth_plan/models.py:300 msgid "Step" msgstr "步骤" -#: xpack/plugins/change_auth_plan/models.py:355 +#: xpack/plugins/change_auth_plan/models.py:317 msgid "Change auth plan task" msgstr "改密计划任务" -#: xpack/plugins/change_auth_plan/serializers.py:27 -msgid "Change Password" -msgstr "修改密码" - -#: xpack/plugins/change_auth_plan/serializers.py:28 -msgid "Change SSH Key" -msgstr "修改密钥" - -#: xpack/plugins/change_auth_plan/serializers.py:33 -#, fuzzy -#| msgid "SSH Key Reset" -msgid "SSH Key strategy" -msgstr "重置SSH密钥" - -#: xpack/plugins/change_auth_plan/serializers.py:60 +#: xpack/plugins/change_auth_plan/serializers.py:56 msgid "Run times" msgstr "执行次数" -#: xpack/plugins/change_auth_plan/serializers.py:78 -msgid "Require password strategy perform setting" -msgstr "需要密码策略执行设置" +#: xpack/plugins/change_auth_plan/serializers.py:72 +msgid "* Please enter custom password" +msgstr "* 请输入自定义密码" -#: xpack/plugins/change_auth_plan/serializers.py:81 -msgid "Require password perform setting" -msgstr "需要密码执行设置" - -#: xpack/plugins/change_auth_plan/serializers.py:84 -msgid "Require password rule perform setting" -msgstr "需要密码规则执行设置" - -#: xpack/plugins/change_auth_plan/serializers.py:96 +#: xpack/plugins/change_auth_plan/serializers.py:82 msgid "* Please enter the correct password length" msgstr "* 请输入正确的密码长度" -#: xpack/plugins/change_auth_plan/serializers.py:99 +#: xpack/plugins/change_auth_plan/serializers.py:85 msgid "* Password length range 6-30 bits" msgstr "* 密码长度范围 6-30 位" -#: xpack/plugins/change_auth_plan/serializers.py:118 -msgid "Require ssh key strategy or ssh key perform setting" -msgstr "需要ssh密钥策略或ssh密钥执行设置" - -#: xpack/plugins/change_auth_plan/utils.py:485 +#: xpack/plugins/change_auth_plan/utils.py:442 msgid "Invalid/incorrect password" msgstr "无效/错误 密码" -#: xpack/plugins/change_auth_plan/utils.py:487 +#: xpack/plugins/change_auth_plan/utils.py:444 msgid "Failed to connect to the host" msgstr "连接主机失败" -#: xpack/plugins/change_auth_plan/utils.py:489 +#: xpack/plugins/change_auth_plan/utils.py:446 msgid "Data could not be sent to remote" msgstr "无法将数据发送到远程" @@ -5380,8 +5369,51 @@ msgstr "旗舰版" msgid "Community edition" msgstr "社区版" -#~ msgid "* Please enter custom password" -#~ msgstr "* 请输入自定义密码" +#~ msgid "Append SSH KEY" +#~ msgstr "追加新密钥" + +#~ msgid "Empty and append SSH KEY" +#~ msgstr "清空所有密钥再追加新密钥" + +#~ msgid "Empty current user and append SSH KEY" +#~ msgstr "清空当前账号密钥再追加新密钥" + +#, fuzzy +#~| msgid "Hostname strategy" +#~ msgid "SSH key strategy" +#~ msgstr "主机名策略" + +#~ msgid "Manual trigger" +#~ msgstr "手动触发" + +#~ msgid "Timing trigger" +#~ msgstr "定时触发" + +#~ msgid "Trigger mode" +#~ msgstr "触发模式" + +#~ msgid "Change Password" +#~ msgstr "修改密码" + +#~ msgid "Change SSH Key" +#~ msgstr "修改密钥" + +#, fuzzy +#~| msgid "SSH Key Reset" +#~ msgid "SSH Key strategy" +#~ msgstr "重置SSH密钥" + +#~ msgid "Require password strategy perform setting" +#~ msgstr "需要密码策略执行设置" + +#~ msgid "Require password perform setting" +#~ msgstr "需要密码执行设置" + +#~ msgid "Require password rule perform setting" +#~ msgstr "需要密码规则执行设置" + +#~ msgid "Require ssh key strategy or ssh key perform setting" +#~ msgstr "需要ssh密钥策略或ssh密钥执行设置" #~ msgid "Category(Display)" #~ msgstr "类别 (显示名称)" diff --git a/apps/notifications/backends/__init__.py b/apps/notifications/backends/__init__.py index 4e2633072..11a95cf40 100644 --- a/apps/notifications/backends/__init__.py +++ b/apps/notifications/backends/__init__.py @@ -5,6 +5,7 @@ from .dingtalk import DingTalk from .email import Email from .site_msg import SiteMessage from .wecom import WeCom +from .feishu import FeiShu class BACKEND(models.TextChoices): @@ -12,6 +13,7 @@ class BACKEND(models.TextChoices): WECOM = 'wecom', _('WeCom') DINGTALK = 'dingtalk', _('DingTalk') SITE_MSG = 'site_msg', _('Site message') + FEISHU = 'feishu', _('FeiShu') @property def client(self): @@ -19,7 +21,8 @@ class BACKEND(models.TextChoices): self.EMAIL: Email, self.WECOM: WeCom, self.DINGTALK: DingTalk, - self.SITE_MSG: SiteMessage + self.SITE_MSG: SiteMessage, + self.FEISHU: FeiShu, }[self] return client diff --git a/apps/notifications/backends/dingtalk.py b/apps/notifications/backends/dingtalk.py index ef5e9a9c6..83add673e 100644 --- a/apps/notifications/backends/dingtalk.py +++ b/apps/notifications/backends/dingtalk.py @@ -1,5 +1,4 @@ from django.conf import settings - from common.message.backends.dingtalk import DingTalk as Client from .base import BackendBase diff --git a/apps/notifications/backends/feishu.py b/apps/notifications/backends/feishu.py new file mode 100644 index 000000000..90547299c --- /dev/null +++ b/apps/notifications/backends/feishu.py @@ -0,0 +1,19 @@ +from django.conf import settings + +from common.message.backends.feishu import FeiShu as Client +from .base import BackendBase + + +class FeiShu(BackendBase): + account_field = 'feishu_id' + is_enable_field_in_settings = 'AUTH_FEISHU' + + def __init__(self): + self.client = Client( + app_id=settings.FEISHU_APP_ID, + app_secret=settings.FEISHU_APP_SECRET + ) + + def send_msg(self, users, msg): + accounts, __, __ = self.get_accounts(users) + return self.client.send_text(accounts, msg) diff --git a/apps/settings/api/__init__.py b/apps/settings/api/__init__.py index 39e009ed5..d7cfa4cec 100644 --- a/apps/settings/api/__init__.py +++ b/apps/settings/api/__init__.py @@ -2,3 +2,4 @@ from .common import * from .ldap import * from .wecom import * from .dingtalk import * +from .feishu import * diff --git a/apps/settings/api/common.py b/apps/settings/api/common.py index 27e3eff54..5f0e6f89c 100644 --- a/apps/settings/api/common.py +++ b/apps/settings/api/common.py @@ -130,6 +130,7 @@ class PublicSettingApi(generics.RetrieveAPIView): }, "AUTH_WECOM": settings.AUTH_WECOM, "AUTH_DINGTALK": settings.AUTH_DINGTALK, + "AUTH_FEISHU": settings.AUTH_FEISHU, 'SECURITY_WATERMARK_ENABLED': settings.SECURITY_WATERMARK_ENABLED } } @@ -148,6 +149,7 @@ class SettingsApi(generics.RetrieveUpdateAPIView): 'email_content': serializers.EmailContentSettingSerializer, 'wecom': serializers.WeComSettingSerializer, 'dingtalk': serializers.DingTalkSettingSerializer, + 'feishu': serializers.FeiShuSettingSerializer, } def get_serializer_class(self): diff --git a/apps/settings/api/feishu.py b/apps/settings/api/feishu.py new file mode 100644 index 000000000..3e3d720b1 --- /dev/null +++ b/apps/settings/api/feishu.py @@ -0,0 +1,41 @@ +from rest_framework.views import Response +from rest_framework.generics import GenericAPIView +from rest_framework.exceptions import APIException +from rest_framework import status +from django.utils.translation import gettext_lazy as _ + +from settings.models import Setting +from common.permissions import IsSuperUser +from common.message.backends.feishu import FeiShu + +from .. import serializers + + +class FeiShuTestingAPI(GenericAPIView): + permission_classes = (IsSuperUser,) + serializer_class = serializers.FeiShuSettingSerializer + + def post(self, request): + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + + app_id = serializer.validated_data['FEISHU_APP_ID'] + app_secret = serializer.validated_data.get('FEISHU_APP_SECRET') + + if not app_secret: + secret = Setting.objects.filter(name='FEISHU_APP_SECRET').first() + if secret: + app_secret = secret.cleaned_value + + app_secret = app_secret or '' + + try: + feishu = FeiShu(app_id=app_id, app_secret=app_secret) + feishu.send_text(['test'], 'test') + return Response(status=status.HTTP_200_OK, data={'msg': _('Test success')}) + except APIException as e: + try: + error = e.detail['errmsg'] + except: + error = e.detail + return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': error}) diff --git a/apps/settings/serializers/settings.py b/apps/settings/serializers/settings.py index 1039fd557..b9159c2cd 100644 --- a/apps/settings/serializers/settings.py +++ b/apps/settings/serializers/settings.py @@ -7,6 +7,7 @@ __all__ = [ 'BasicSettingSerializer', 'EmailSettingSerializer', 'EmailContentSettingSerializer', 'LDAPSettingSerializer', 'TerminalSettingSerializer', 'SecuritySettingSerializer', 'SettingsSerializer', 'WeComSettingSerializer', 'DingTalkSettingSerializer', + 'FeiShuSettingSerializer', ] @@ -218,6 +219,12 @@ class DingTalkSettingSerializer(serializers.Serializer): AUTH_DINGTALK = serializers.BooleanField(default=False, label=_('Enable DingTalk Auth')) +class FeiShuSettingSerializer(serializers.Serializer): + FEISHU_APP_ID = serializers.CharField(max_length=256, required=True, label='App ID') + FEISHU_APP_SECRET = serializers.CharField(max_length=256, required=False, label='App Secret', write_only=True) + AUTH_FEISHU = serializers.BooleanField(default=False, label=_('Enable FeiShu Auth')) + + class SettingsSerializer( BasicSettingSerializer, EmailSettingSerializer, @@ -227,6 +234,7 @@ class SettingsSerializer( SecuritySettingSerializer, WeComSettingSerializer, DingTalkSettingSerializer, + FeiShuSettingSerializer, ): # encrypt_fields 现在使用 write_only 来判断了 diff --git a/apps/settings/urls/api_urls.py b/apps/settings/urls/api_urls.py index 86dfc6847..bd423611f 100644 --- a/apps/settings/urls/api_urls.py +++ b/apps/settings/urls/api_urls.py @@ -15,6 +15,7 @@ urlpatterns = [ 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('feishu/testing/', api.FeiShuTestingAPI.as_view(), name='feishu-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_feishu_logo.png b/apps/static/img/login_feishu_logo.png new file mode 100644 index 000000000..054f350f5 Binary files /dev/null and b/apps/static/img/login_feishu_logo.png differ diff --git a/apps/users/migrations/0036_user_feishu_id.py b/apps/users/migrations/0036_user_feishu_id.py new file mode 100644 index 000000000..3e5882c70 --- /dev/null +++ b/apps/users/migrations/0036_user_feishu_id.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.6 on 2021-08-06 02:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0035_auto_20210526_1100'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='feishu_id', + field=models.CharField(default=None, max_length=128, null=True, unique=True), + ), + ] diff --git a/apps/users/models/user.py b/apps/users/models/user.py index f921058fa..c6a689c7e 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -610,6 +610,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): ) wecom_id = models.CharField(null=True, default=None, unique=True, max_length=128) dingtalk_id = models.CharField(null=True, default=None, unique=True, max_length=128) + feishu_id = models.CharField(null=True, default=None, unique=True, max_length=128) def __str__(self): return '{0.name}({0.username})'.format(self) @@ -628,6 +629,10 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): def is_dingtalk_bound(self): return bool(self.dingtalk_id) + @property + def is_feishu_bound(self): + return bool(self.feishu_id) + def get_absolute_url(self): return reverse('users:user-detail', args=(self.id,)) diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index c718f29ec..e8760a3ee 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -54,7 +54,7 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): 'mfa_enabled', 'is_valid', 'is_expired', 'is_active', # 布尔字段 'date_expired', 'date_joined', 'last_login', # 日期字段 'created_by', 'comment', # 通用字段 - 'is_wecom_bound', 'is_dingtalk_bound', + 'is_wecom_bound', 'is_dingtalk_bound', 'is_feishu_bound', ] # 包含不太常用的字段,可以没有 fields_verbose = fields_small + [