From 470a088a9f4fa00b6b299d06cef7c3c5621f0fe6 Mon Sep 17 00:00:00 2001 From: feng <1304903146@qq.com> Date: Fri, 22 Mar 2024 18:05:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8B=86=E5=88=86=20feishu=20lark?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/audits/signal_handlers/login_log.py | 1 + apps/authentication/api/__init__.py | 1 + apps/authentication/api/common.py | 2 +- apps/authentication/api/lark.py | 8 ++ apps/authentication/backends/sso.py | 7 ++ apps/authentication/errors/mfa.py | 5 ++ apps/authentication/urls/api_urls.py | 3 + apps/authentication/urls/view_urls.py | 6 ++ apps/authentication/views/__init__.py | 7 +- apps/authentication/views/feishu.py | 81 ++++++++++++------- apps/authentication/views/lark.py | 49 +++++++++++ apps/authentication/views/login.py | 6 ++ apps/common/sdk/im/feishu/__init__.py | 21 ++--- apps/common/sdk/im/lark/__init__.py | 16 ++++ apps/jumpserver/conf.py | 6 +- apps/jumpserver/settings/auth.py | 8 +- apps/notifications/backends/__init__.py | 4 +- apps/notifications/backends/lark.py | 23 ++++++ apps/notifications/notifications.py | 3 + apps/settings/api/__init__.py | 1 + apps/settings/api/feishu.py | 23 +++--- apps/settings/api/lark.py | 7 ++ apps/settings/api/settings.py | 1 + .../migrations/0013_auto_20240326_1531.py | 42 ++++++++++ apps/settings/serializers/auth/__init__.py | 3 +- apps/settings/serializers/auth/base.py | 1 + apps/settings/serializers/auth/feishu.py | 7 -- apps/settings/serializers/auth/lark.py | 14 ++++ apps/settings/serializers/public.py | 1 + apps/settings/serializers/settings.py | 7 +- apps/settings/urls/api_urls.py | 1 + .../0050_user_lark_id_alter_user_source.py | 27 +++++++ apps/users/models/user.py | 6 ++ apps/users/serializers/user.py | 6 +- 34 files changed, 329 insertions(+), 75 deletions(-) create mode 100644 apps/authentication/api/lark.py create mode 100644 apps/authentication/views/lark.py create mode 100644 apps/common/sdk/im/lark/__init__.py create mode 100644 apps/notifications/backends/lark.py create mode 100644 apps/settings/api/lark.py create mode 100644 apps/settings/migrations/0013_auto_20240326_1531.py create mode 100644 apps/settings/serializers/auth/lark.py create mode 100644 apps/users/migrations/0050_user_lark_id_alter_user_source.py diff --git a/apps/audits/signal_handlers/login_log.py b/apps/audits/signal_handlers/login_log.py index ea53716b4..cdcf244cb 100644 --- a/apps/audits/signal_handlers/login_log.py +++ b/apps/audits/signal_handlers/login_log.py @@ -36,6 +36,7 @@ class AuthBackendLabelMapping(LazyObject): backend_label_mapping[settings.AUTH_BACKEND_AUTH_TOKEN] = _("Auth Token") backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _("WeCom") backend_label_mapping[settings.AUTH_BACKEND_FEISHU] = _("FeiShu") + backend_label_mapping[settings.AUTH_BACKEND_LARK] = 'Lark' backend_label_mapping[settings.AUTH_BACKEND_SLACK] = _("Slack") backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _("DingTalk") backend_label_mapping[settings.AUTH_BACKEND_TEMP_TOKEN] = _("Temporary token") diff --git a/apps/authentication/api/__init__.py b/apps/authentication/api/__init__.py index af0542a33..4cfc6cc4a 100644 --- a/apps/authentication/api/__init__.py +++ b/apps/authentication/api/__init__.py @@ -6,6 +6,7 @@ from .common import * from .confirm import * from .connection_token import * from .feishu import * +from .lark import * from .login_confirm import * from .mfa import * from .password import * diff --git a/apps/authentication/api/common.py b/apps/authentication/api/common.py index 6624078a7..59e0e3eb2 100644 --- a/apps/authentication/api/common.py +++ b/apps/authentication/api/common.py @@ -12,7 +12,6 @@ from common.permissions import IsValidUser, OnlySuperUser from common.utils import get_logger from users.models import User - logger = get_logger(__file__) @@ -24,6 +23,7 @@ class QRUnBindBase(APIView): 'wecom': {'user_field': 'wecom_id', 'not_bind_err': errors.WeComNotBound}, 'dingtalk': {'user_field': 'dingtalk_id', 'not_bind_err': errors.DingTalkNotBound}, 'feishu': {'user_field': 'feishu_id', 'not_bind_err': errors.FeiShuNotBound}, + 'lark': {'user_field': 'lark_id', 'not_bind_err': errors.LarkNotBound}, 'slack': {'user_field': 'slack_id', 'not_bind_err': errors.SlackNotBound}, } user = self.user diff --git a/apps/authentication/api/lark.py b/apps/authentication/api/lark.py new file mode 100644 index 000000000..5c81fdfa6 --- /dev/null +++ b/apps/authentication/api/lark.py @@ -0,0 +1,8 @@ +from common.utils import get_logger +from .feishu import FeiShuEventSubscriptionCallback + +logger = get_logger(__name__) + + +class LarkEventSubscriptionCallback(FeiShuEventSubscriptionCallback): + pass diff --git a/apps/authentication/backends/sso.py b/apps/authentication/backends/sso.py index 66f15a3a4..5ee17a4ca 100644 --- a/apps/authentication/backends/sso.py +++ b/apps/authentication/backends/sso.py @@ -55,6 +55,12 @@ class FeiShuAuthentication(JMSModelBackend): pass +class LarkAuthentication(FeiShuAuthentication): + @staticmethod + def is_enabled(): + return settings.AUTH_LARK + + class SlackAuthentication(JMSModelBackend): """ 什么也不做呀😺 @@ -72,5 +78,6 @@ class AuthorizationTokenAuthentication(JMSModelBackend): """ 什么也不做呀😺 """ + def authenticate(self, request, **kwargs): pass diff --git a/apps/authentication/errors/mfa.py b/apps/authentication/errors/mfa.py index b1ace594d..ae314ada6 100644 --- a/apps/authentication/errors/mfa.py +++ b/apps/authentication/errors/mfa.py @@ -33,6 +33,11 @@ class FeiShuNotBound(JMSException): default_detail = _('FeiShu is not bound') +class LarkNotBound(JMSException): + default_code = 'lark_not_bound' + default_detail = _('Lark is not bound') + + class SlackNotBound(JMSException): default_code = 'slack_not_bound' default_detail = _('Slack is not bound') diff --git a/apps/authentication/urls/api_urls.py b/apps/authentication/urls/api_urls.py index 8474f5a30..62f52ac9f 100644 --- a/apps/authentication/urls/api_urls.py +++ b/apps/authentication/urls/api_urls.py @@ -22,6 +22,9 @@ urlpatterns = [ path('feishu/event/subscription/callback/', api.FeiShuEventSubscriptionCallback.as_view(), name='feishu-event-subscription-callback'), + path('lark/event/subscription/callback/', api.LarkEventSubscriptionCallback.as_view(), + name='lark-event-subscription-callback'), + path('auth/', api.TokenCreateApi.as_view(), name='user-auth'), path('confirm-oauth/', api.ConfirmBindORUNBindOAuth.as_view(), name='confirm-oauth'), path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'), diff --git a/apps/authentication/urls/view_urls.py b/apps/authentication/urls/view_urls.py index a648ded51..94c15047f 100644 --- a/apps/authentication/urls/view_urls.py +++ b/apps/authentication/urls/view_urls.py @@ -49,6 +49,12 @@ urlpatterns = [ 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'), + path('lark/bind/start/', views.LarkEnableStartView.as_view(), name='lark-bind-start'), + path('lark/qr/bind/', views.LarkQRBindView.as_view(), name='lark-qr-bind'), + path('lark/qr/login/', views.LarkQRLoginView.as_view(), name='lark-qr-login'), + path('lark/qr/bind/callback/', views.LarkQRBindCallbackView.as_view(), name='lark-qr-bind-callback'), + path('lark/qr/login/callback/', views.LarkQRLoginCallbackView.as_view(), name='lark-qr-login-callback'), + path('slack/bind/start/', views.SlackEnableStartView.as_view(), name='slack-bind-start'), path('slack/qr/bind/', views.SlackQRBindView.as_view(), name='slack-qr-bind'), path('slack/qr/login/', views.SlackQRLoginView.as_view(), name='slack-qr-login'), diff --git a/apps/authentication/views/__init__.py b/apps/authentication/views/__init__.py index 214c8943a..3c51a1c85 100644 --- a/apps/authentication/views/__init__.py +++ b/apps/authentication/views/__init__.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- # -from .login import * -from .mfa import * -from .wecom import * from .dingtalk import * from .feishu import * +from .lark import * +from .login import * +from .mfa import * from .slack import * +from .wecom import * diff --git a/apps/authentication/views/feishu.py b/apps/authentication/views/feishu.py index 0427c2b1b..c2cd07914 100644 --- a/apps/authentication/views/feishu.py +++ b/apps/authentication/views/feishu.py @@ -21,24 +21,45 @@ from .mixins import FlashMessageMixin logger = get_logger(__file__) -FEISHU_STATE_SESSION_KEY = '_feishu_state' + +class FeiShuEnableStartView(UserVerifyPasswordView): + category = 'feishu' + + def get_success_url(self): + referer = self.request.META.get('HTTP_REFERER') + redirect_url = self.request.GET.get("redirect_url") + + success_url = reverse(f'authentication:{self.category}-qr-bind') + + success_url += '?' + urlencode({ + 'redirect_url': redirect_url or referer + }) + + return success_url class FeiShuQRMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashMessageMixin, View): + category = 'feishu' + error = _('FeiShu Error') + error_msg = _('FeiShu is already bound') + state_session_key = f'_{category}_state' + + @property + def url_object(self): + return URL() + 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_response( - '/', - _('FeiShu Error'), - msg + '/', self.error, msg ) def verify_state(self): state = self.request.GET.get('state') - session_state = self.request.session.get(FEISHU_STATE_SESSION_KEY) + session_state = self.request.session.get(self.state_session_key) if state != session_state: return False return True @@ -49,19 +70,18 @@ class FeiShuQRMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashMe def get_qr_url(self, redirect_uri): state = random_string(16) - self.request.session[FEISHU_STATE_SESSION_KEY] = state + self.request.session[self.state_session_key] = state params = { - 'app_id': settings.FEISHU_APP_ID, + 'app_id': getattr(settings, f'{self.category}_APP_ID'.upper()), 'state': state, 'redirect_uri': redirect_uri, } - url = URL().authen + '?' + urlencode(params) + url = self.url_object.authen + '?' + urlencode(params) return url def get_already_bound_response(self, redirect_url): - msg = _('FeiShu is already bound') - response = self.get_failed_response(redirect_url, msg, msg) + response = self.get_failed_response(redirect_url, self.error_msg, self.error_msg) return response @@ -71,7 +91,7 @@ class FeiShuQRBindView(FeiShuQRMixin, View): def get(self, request: HttpRequest): redirect_url = request.GET.get('redirect_url') - redirect_uri = reverse('authentication:feishu-qr-bind-callback', external=True) + redirect_uri = reverse(f'authentication:{self.category}-qr-bind-callback', external=True) redirect_uri += '?' + urlencode({'redirect_url': redirect_url}) url = self.get_qr_url(redirect_uri) @@ -81,25 +101,16 @@ class FeiShuQRBindView(FeiShuQRMixin, View): class FeiShuQRBindCallbackView(FeiShuQRMixin, BaseBindCallbackView): permission_classes = (IsAuthenticated,) - client_type_path = 'common.sdk.im.feishu.FeiShu' - client_auth_params = {'app_id': 'FEISHU_APP_ID', 'app_secret': 'FEISHU_APP_SECRET'} auth_type = 'feishu' auth_type_label = _('FeiShu') + client_type_path = f'common.sdk.im.{auth_type}.FeiShu' - -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 += '?' + urlencode({ - 'redirect_url': redirect_url or referer - }) - - return success_url + @property + def client_auth_params(self): + return { + 'app_id': f'{self.auth_type}_APP_ID'.upper(), + 'app_secret': f'{self.auth_type}_APP_SECRET'.upper() + } class FeiShuQRLoginView(FeiShuQRMixin, View): @@ -107,7 +118,7 @@ class FeiShuQRLoginView(FeiShuQRMixin, View): def get(self, request: HttpRequest): redirect_url = request.GET.get('redirect_url') or reverse('index') - redirect_uri = reverse('authentication:feishu-qr-login-callback', external=True) + redirect_uri = reverse(f'authentication:{self.category}-qr-login-callback', external=True) redirect_uri += '?' + urlencode({ 'redirect_url': redirect_url, }) @@ -119,11 +130,19 @@ class FeiShuQRLoginView(FeiShuQRMixin, View): class FeiShuQRLoginCallbackView(FeiShuQRMixin, BaseLoginCallbackView): permission_classes = (AllowAny,) - client_type_path = 'common.sdk.im.feishu.FeiShu' - client_auth_params = {'app_id': 'FEISHU_APP_ID', 'app_secret': 'FEISHU_APP_SECRET'} user_type = 'feishu' - auth_backend = 'AUTH_BACKEND_FEISHU' + auth_type = user_type + client_type_path = f'common.sdk.im.{auth_type}.FeiShu' msg_client_err = _('FeiShu Error') msg_user_not_bound_err = _('FeiShu is not bound') msg_not_found_user_from_client_err = _('Failed to get user from FeiShu') + + auth_backend = f'AUTH_BACKEND_{auth_type}'.upper() + + @property + def client_auth_params(self): + return { + 'app_id': f'{self.auth_type}_APP_ID'.upper(), + 'app_secret': f'{self.auth_type}_APP_SECRET'.upper() + } diff --git a/apps/authentication/views/lark.py b/apps/authentication/views/lark.py new file mode 100644 index 000000000..3337a95a1 --- /dev/null +++ b/apps/authentication/views/lark.py @@ -0,0 +1,49 @@ +from django.utils.translation import gettext_lazy as _ + +from common.sdk.im.lark import URL +from common.utils import get_logger +from .feishu import ( + FeiShuEnableStartView, FeiShuQRBindView, FeiShuQRBindCallbackView, + FeiShuQRLoginView, FeiShuQRLoginCallbackView +) + +logger = get_logger(__file__) + + +class LarkEnableStartView(FeiShuEnableStartView): + category = 'lark' + + +class BaseLarkQRMixin: + category = 'lark' + error = _('Lark Error') + error_msg = _('Lark is already bound') + state_session_key = f'_{category}_state' + + @property + def url_object(self): + return URL() + + +class LarkQRBindView(BaseLarkQRMixin, FeiShuQRBindView): + pass + + +class LarkQRBindCallbackView(BaseLarkQRMixin, FeiShuQRBindCallbackView): + auth_type = 'lark' + auth_type_label = auth_type.capitalize() + client_type_path = f'common.sdk.im.{auth_type}.Lark' + + +class LarkQRLoginView(BaseLarkQRMixin, FeiShuQRLoginView): + pass + + +class LarkQRLoginCallbackView(BaseLarkQRMixin, FeiShuQRLoginCallbackView): + user_type = 'lark' + auth_type = user_type + client_type_path = f'common.sdk.im.{auth_type}.Lark' + + msg_client_err = _('Lark Error') + msg_user_not_bound_err = _('Lark is not bound') + msg_not_found_user_from_client_err = _('Failed to get user from Lark') diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index 23ad4d814..666dee778 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -91,6 +91,12 @@ class UserLoginContextMixin: 'url': reverse('authentication:feishu-qr-login'), 'logo': static('img/login_feishu_logo.png') }, + { + 'name': 'Lark', + 'enabled': settings.AUTH_LARK, + 'url': reverse('authentication:lark-qr-login'), + 'logo': static('img/login_feishu_logo.png') + }, { 'name': _('Slack'), 'enabled': settings.AUTH_SLACK, diff --git a/apps/common/sdk/im/feishu/__init__.py b/apps/common/sdk/im/feishu/__init__.py index 99194e5c1..c7bf03a06 100644 --- a/apps/common/sdk/im/feishu/__init__.py +++ b/apps/common/sdk/im/feishu/__init__.py @@ -2,24 +2,18 @@ import json from rest_framework.exceptions import APIException -from django.conf import settings -from users.utils import construct_user_email -from common.utils.common import get_logger -from common.sdk.im.utils import digest from common.sdk.im.mixin import RequestMixin, BaseRequest +from common.sdk.im.utils import digest +from common.utils.common import get_logger +from users.utils import construct_user_email logger = get_logger(__name__) class URL: # https://open.feishu.cn/document/ukTMukTMukTM/uEDO4UjLxgDO14SM4gTN - @property - def host(self): - if settings.FEISHU_VERSION == 'feishu': - h = 'https://open.feishu.cn' - else: - h = 'https://open.larksuite.com' - return h + + host = 'https://open.feishu.cn' @property def authen(self): @@ -87,12 +81,13 @@ class FeiShu(RequestMixin): """ 非业务数据导致的错误直接抛异常,说明是系统配置错误,业务代码不用理会 """ + requests_cls = FeishuRequests def __init__(self, app_id, app_secret, timeout=None): self._app_id = app_id or '' self._app_secret = app_secret or '' - self._requests = FeishuRequests( + self._requests = self.requests_cls( app_id=app_id, app_secret=app_secret, timeout=timeout @@ -130,7 +125,7 @@ class FeiShu(RequestMixin): body['receive_id'] = user_id try: - logger.info(f'Feishu send text: user_ids={user_ids} msg={msg}') + logger.info(f'{self.__class__.__name__} send text: user_ids={user_ids} msg={msg}') self._requests.post(URL().send_message, params=params, json=body) except APIException as e: # 只处理可预知的错误 diff --git a/apps/common/sdk/im/lark/__init__.py b/apps/common/sdk/im/lark/__init__.py new file mode 100644 index 000000000..dcfaa838a --- /dev/null +++ b/apps/common/sdk/im/lark/__init__.py @@ -0,0 +1,16 @@ +from common.utils.common import get_logger +from ..feishu import URL as FeiShuURL, FeishuRequests, FeiShu + +logger = get_logger(__name__) + + +class URL(FeiShuURL): + host = 'https://open.larksuite.com' + + +class LarkRequests(FeishuRequests): + pass + + +class Lark(FeiShu): + requests_cls = LarkRequests diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index d1caba0cd..26d813b90 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -407,7 +407,11 @@ class Config(dict): 'AUTH_FEISHU': False, 'FEISHU_APP_ID': '', 'FEISHU_APP_SECRET': '', - 'FEISHU_VERSION': 'feishu', + + # Lark + 'AUTH_LARK': False, + 'LARK_APP_ID': '', + 'LARK_APP_SECRET': '', # Slack 'AUTH_SLACK': False, diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index 5e44a22a4..4e4badcfc 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -141,7 +141,10 @@ DINGTALK_APPSECRET = CONFIG.DINGTALK_APPSECRET AUTH_FEISHU = CONFIG.AUTH_FEISHU FEISHU_APP_ID = CONFIG.FEISHU_APP_ID FEISHU_APP_SECRET = CONFIG.FEISHU_APP_SECRET -FEISHU_VERSION = CONFIG.FEISHU_VERSION + +AUTH_LARK = CONFIG.AUTH_LARK +LARK_APP_ID = CONFIG.LARK_APP_ID +LARK_APP_SECRET = CONFIG.LARK_APP_SECRET # Slack auth AUTH_SLACK = CONFIG.AUTH_SLACK @@ -212,6 +215,7 @@ AUTH_BACKEND_SSO = 'authentication.backends.sso.SSOAuthentication' AUTH_BACKEND_WECOM = 'authentication.backends.sso.WeComAuthentication' AUTH_BACKEND_DINGTALK = 'authentication.backends.sso.DingTalkAuthentication' AUTH_BACKEND_FEISHU = 'authentication.backends.sso.FeiShuAuthentication' +AUTH_BACKEND_LARK = 'authentication.backends.sso.LarkAuthentication' AUTH_BACKEND_SLACK = 'authentication.backends.sso.SlackAuthentication' AUTH_BACKEND_AUTH_TOKEN = 'authentication.backends.sso.AuthorizationTokenAuthentication' AUTH_BACKEND_SAML2 = 'authentication.backends.saml2.SAML2Backend' @@ -228,7 +232,7 @@ AUTHENTICATION_BACKENDS = [ AUTH_BACKEND_CAS, AUTH_BACKEND_OIDC_PASSWORD, AUTH_BACKEND_OIDC_CODE, AUTH_BACKEND_SAML2, AUTH_BACKEND_OAUTH2, # 扫码模式 - AUTH_BACKEND_WECOM, AUTH_BACKEND_DINGTALK, AUTH_BACKEND_FEISHU, AUTH_BACKEND_SLACK, + AUTH_BACKEND_WECOM, AUTH_BACKEND_DINGTALK, AUTH_BACKEND_FEISHU, AUTH_BACKEND_LARK, AUTH_BACKEND_SLACK, # Token模式 AUTH_BACKEND_AUTH_TOKEN, AUTH_BACKEND_SSO, AUTH_BACKEND_TEMP_TOKEN, AUTH_BACKEND_PASSKEY diff --git a/apps/notifications/backends/__init__.py b/apps/notifications/backends/__init__.py index 047123e57..5baecf35a 100644 --- a/apps/notifications/backends/__init__.py +++ b/apps/notifications/backends/__init__.py @@ -1,7 +1,7 @@ import importlib -from django.utils.translation import gettext_lazy as _ from django.db import models +from django.utils.translation import gettext_lazy as _ client_name_mapper = {} @@ -12,7 +12,9 @@ class BACKEND(models.TextChoices): DINGTALK = 'dingtalk', _('DingTalk') SITE_MSG = 'site_msg', _('Site message') FEISHU = 'feishu', _('FeiShu') + LARK = 'lark', 'Lark' SLACK = 'slack', _('Slack') + # SMS = 'sms', _('SMS') @property diff --git a/apps/notifications/backends/lark.py b/apps/notifications/backends/lark.py new file mode 100644 index 000000000..e6e37218a --- /dev/null +++ b/apps/notifications/backends/lark.py @@ -0,0 +1,23 @@ +from django.conf import settings + +from common.sdk.im.lark import Lark as Client +from .base import BackendBase + + +class Lark(BackendBase): + account_field = 'lark_id' + is_enable_field_in_settings = 'AUTH_LARK' + + def __init__(self): + self.client = Client( + app_id=settings.LARK_APP_ID, + app_secret=settings.LARK_APP_SECRET + ) + + def send_msg(self, users, message, subject=None): + accounts, __, __ = self.get_accounts(users) + print('lark', message) + return self.client.send_text(accounts, message) + + +backend = Lark diff --git a/apps/notifications/notifications.py b/apps/notifications/notifications.py index 5ceb629c3..ba2b69343 100644 --- a/apps/notifications/notifications.py +++ b/apps/notifications/notifications.py @@ -196,6 +196,9 @@ class Message(metaclass=MessageType): def get_feishu_msg(self) -> dict: return self.markdown_msg + def get_lark_msg(self) -> dict: + return self.markdown_msg + def get_email_msg(self) -> dict: return self.html_msg_with_sign diff --git a/apps/settings/api/__init__.py b/apps/settings/api/__init__.py index 686a52f3b..b30c60c59 100644 --- a/apps/settings/api/__init__.py +++ b/apps/settings/api/__init__.py @@ -2,6 +2,7 @@ from .chat import * from .dingtalk import * from .email import * from .feishu import * +from .lark import * from .ldap import * from .public import * from .security import * diff --git a/apps/settings/api/feishu.py b/apps/settings/api/feishu.py index ed3b51f9e..1f639f66c 100644 --- a/apps/settings/api/feishu.py +++ b/apps/settings/api/feishu.py @@ -1,16 +1,16 @@ -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 rest_framework import status +from rest_framework.exceptions import APIException +from rest_framework.generics import GenericAPIView +from rest_framework.views import Response -from settings.models import Setting from common.sdk.im.feishu import FeiShu - +from settings.models import Setting from .. import serializers class FeiShuTestingAPI(GenericAPIView): + category = 'FEISHU' serializer_class = serializers.FeiShuSettingSerializer rbac_perms = { 'POST': 'settings.change_auth' @@ -20,11 +20,11 @@ class FeiShuTestingAPI(GenericAPIView): 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') + app_id = serializer.validated_data[f'{self.category}_APP_ID'] + app_secret = serializer.validated_data.get(f'{self.category}_APP_SECRET') if not app_secret: - secret = Setting.objects.filter(name='FEISHU_APP_SECRET').first() + secret = Setting.objects.filter(name=f'{self.category}_APP_SECRET').first() if secret: app_secret = secret.cleaned_value @@ -40,3 +40,8 @@ class FeiShuTestingAPI(GenericAPIView): except: error = e.detail return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': error}) + + +class LarkTestingAPI(FeiShuTestingAPI): + category = 'LARK' + serializer_class = serializers.LarkSettingSerializer diff --git a/apps/settings/api/lark.py b/apps/settings/api/lark.py new file mode 100644 index 000000000..6a7fb2cc7 --- /dev/null +++ b/apps/settings/api/lark.py @@ -0,0 +1,7 @@ +from .feishu import FeiShuTestingAPI +from .. import serializers + + +class LarkTestingAPI(FeiShuTestingAPI): + category = 'LARK' + serializer_class = serializers.LarkSettingSerializer diff --git a/apps/settings/api/settings.py b/apps/settings/api/settings.py index c03e99b6c..a801ab2f8 100644 --- a/apps/settings/api/settings.py +++ b/apps/settings/api/settings.py @@ -39,6 +39,7 @@ class SettingsApi(generics.RetrieveUpdateAPIView): 'wecom': serializers.WeComSettingSerializer, 'dingtalk': serializers.DingTalkSettingSerializer, 'feishu': serializers.FeiShuSettingSerializer, + 'lark': serializers.LarkSettingSerializer, 'slack': serializers.SlackSettingSerializer, 'auth': serializers.AuthSettingSerializer, 'oidc': serializers.OIDCSettingSerializer, diff --git a/apps/settings/migrations/0013_auto_20240326_1531.py b/apps/settings/migrations/0013_auto_20240326_1531.py new file mode 100644 index 000000000..59e6a7f34 --- /dev/null +++ b/apps/settings/migrations/0013_auto_20240326_1531.py @@ -0,0 +1,42 @@ +# Generated by Django 4.1.13 on 2024-03-26 07:31 +import json + +from django.db import migrations +from django.db.models import F + + +def migrate_feishu_to_lark(apps, schema_editor): + setting_model = apps.get_model("settings", "Setting") + user_model = apps.get_model("users", "User") + db_alias = schema_editor.connection.alias + + feishu_version_instance = setting_model.objects.using(db_alias).filter(name='FEISHU_VERSION').first() + if not feishu_version_instance or json.loads(feishu_version_instance.value) == 'feishu': + return + feishu_version_instance.delete() + + user_model.objects.using(db_alias).filter(feishu_id__isnull=False).update(lark_id=F('feishu_id')) + user_model.objects.filter(feishu_id__isnull=False).update(lark_id=F('feishu_id')) + user_model.objects.filter(feishu_id__isnull=False).update(feishu_id=None) + + settings_to_update = [ + ('AUTH_FEISHU', 'AUTH_LARK'), + ('FEISHU_APP_ID', 'LARK_APP_ID'), + ('FEISHU_APP_SECRET', 'LARK_APP_SECRET'), + ] + + for old_name, new_name in settings_to_update: + setting_model.objects.using(db_alias).filter( + name=old_name + ).update(name=new_name) + + +class Migration(migrations.Migration): + dependencies = [ + ('settings', '0012_alter_setting_options'), + ('users', '0050_user_lark_id_alter_user_source'), + ] + + operations = [ + migrations.RunPython(migrate_feishu_to_lark), + ] diff --git a/apps/settings/serializers/auth/__init__.py b/apps/settings/serializers/auth/__init__.py index 2b641114c..b5f567618 100644 --- a/apps/settings/serializers/auth/__init__.py +++ b/apps/settings/serializers/auth/__init__.py @@ -2,13 +2,14 @@ from .base import * from .cas import * from .dingtalk import * from .feishu import * +from .lark import * from .ldap import * from .oauth2 import * from .oidc import * from .passkey import * from .radius import * from .saml2 import * +from .slack import * from .sms import * from .sso import * from .wecom import * -from .slack import * diff --git a/apps/settings/serializers/auth/base.py b/apps/settings/serializers/auth/base.py index fbc833124..bde6e781d 100644 --- a/apps/settings/serializers/auth/base.py +++ b/apps/settings/serializers/auth/base.py @@ -17,6 +17,7 @@ class AuthSettingSerializer(serializers.Serializer): AUTH_RADIUS = serializers.BooleanField(required=False, label=_('RADIUS Auth')) AUTH_DINGTALK = serializers.BooleanField(default=False, label=_('DingTalk Auth')) AUTH_FEISHU = serializers.BooleanField(default=False, label=_('FeiShu Auth')) + AUTH_LARK = serializers.BooleanField(default=False, label=_('Lark Auth')) AUTH_WECOM = serializers.BooleanField(default=False, label=_('Slack Auth')) AUTH_SLACK = serializers.BooleanField(default=False, label=_('WeCom Auth')) AUTH_SSO = serializers.BooleanField(default=False, label=_("SSO Auth")) diff --git a/apps/settings/serializers/auth/feishu.py b/apps/settings/serializers/auth/feishu.py index d3c1edcad..187f2642f 100644 --- a/apps/settings/serializers/auth/feishu.py +++ b/apps/settings/serializers/auth/feishu.py @@ -9,13 +9,6 @@ __all__ = ['FeiShuSettingSerializer'] class FeiShuSettingSerializer(serializers.Serializer): PREFIX_TITLE = _('FeiShu') - VERSION_CHOICES = ( - ('feishu', _('FeiShu')), - ('lark', 'Lark') - ) AUTH_FEISHU = serializers.BooleanField(default=False, label=_('Enable FeiShu Auth')) FEISHU_APP_ID = serializers.CharField(max_length=256, required=True, label='App ID') FEISHU_APP_SECRET = EncryptedField(max_length=256, required=False, label='App Secret') - FEISHU_VERSION = serializers.ChoiceField( - choices=VERSION_CHOICES, default='feishu', label=_('Version') - ) diff --git a/apps/settings/serializers/auth/lark.py b/apps/settings/serializers/auth/lark.py new file mode 100644 index 000000000..4b2143fe1 --- /dev/null +++ b/apps/settings/serializers/auth/lark.py @@ -0,0 +1,14 @@ +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from common.serializers.fields import EncryptedField + +__all__ = ['LarkSettingSerializer'] + + +class LarkSettingSerializer(serializers.Serializer): + PREFIX_TITLE = 'Lark' + + AUTH_LARK = serializers.BooleanField(default=False, label=_('Enable Lark Auth')) + LARK_APP_ID = serializers.CharField(max_length=256, required=True, label='App ID') + LARK_APP_SECRET = EncryptedField(max_length=256, required=False, label='App Secret') diff --git a/apps/settings/serializers/public.py b/apps/settings/serializers/public.py index b4d66671e..ebefcb717 100644 --- a/apps/settings/serializers/public.py +++ b/apps/settings/serializers/public.py @@ -40,6 +40,7 @@ class PrivateSettingSerializer(PublicSettingSerializer): AUTH_WECOM = serializers.BooleanField() AUTH_DINGTALK = serializers.BooleanField() AUTH_FEISHU = serializers.BooleanField() + AUTH_LARK = serializers.BooleanField() AUTH_TEMP_TOKEN = serializers.BooleanField() TERMINAL_RAZOR_ENABLED = serializers.BooleanField() diff --git a/apps/settings/serializers/settings.py b/apps/settings/serializers/settings.py index 7349e3195..4706f9740 100644 --- a/apps/settings/serializers/settings.py +++ b/apps/settings/serializers/settings.py @@ -7,9 +7,9 @@ from common.utils import i18n_fmt from .auth import ( LDAPSettingSerializer, OIDCSettingSerializer, KeycloakSettingSerializer, CASSettingSerializer, RadiusSettingSerializer, FeiShuSettingSerializer, - WeComSettingSerializer, DingTalkSettingSerializer, AlibabaSMSSettingSerializer, - TencentSMSSettingSerializer, CMPP2SMSSettingSerializer, AuthSettingSerializer, - SAML2SettingSerializer, OAuth2SettingSerializer, PasskeySettingSerializer, + LarkSettingSerializer, WeComSettingSerializer, DingTalkSettingSerializer, + AlibabaSMSSettingSerializer, TencentSMSSettingSerializer, CMPP2SMSSettingSerializer, + AuthSettingSerializer, SAML2SettingSerializer, OAuth2SettingSerializer, PasskeySettingSerializer, CustomSMSSettingSerializer, ) from .basic import BasicSettingSerializer @@ -75,6 +75,7 @@ class SettingsSerializer( WeComSettingSerializer, DingTalkSettingSerializer, FeiShuSettingSerializer, + LarkSettingSerializer, EmailSettingSerializer, EmailContentSettingSerializer, OtherSettingSerializer, diff --git a/apps/settings/urls/api_urls.py b/apps/settings/urls/api_urls.py index 15b97c82c..03f3a4390 100644 --- a/apps/settings/urls/api_urls.py +++ b/apps/settings/urls/api_urls.py @@ -15,6 +15,7 @@ urlpatterns = [ 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('lark/testing/', api.LarkTestingAPI.as_view(), name='lark-testing'), path('slack/testing/', api.SlackTestingAPI.as_view(), name='slack-testing'), path('sms//testing/', api.SMSTestingAPI.as_view(), name='sms-testing'), path('sms/backend/', api.SMSBackendAPI.as_view(), name='sms-backend'), diff --git a/apps/users/migrations/0050_user_lark_id_alter_user_source.py b/apps/users/migrations/0050_user_lark_id_alter_user_source.py new file mode 100644 index 000000000..762b8ed7e --- /dev/null +++ b/apps/users/migrations/0050_user_lark_id_alter_user_source.py @@ -0,0 +1,27 @@ +# Generated by Django 4.1.13 on 2024-03-25 08:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0049_alter_user_unique_together_user_slack_id_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='lark_id', + field=models.CharField(default=None, max_length=128, null=True, verbose_name='Lark'), + ), + migrations.AlterField( + model_name='user', + name='source', + field=models.CharField(choices=[('local', 'Local'), ('ldap', 'LDAP/AD'), ('openid', 'OpenID'), ('radius', 'Radius'), ('cas', 'CAS'), ('saml2', 'SAML2'), ('oauth2', 'OAuth2'), ('wecom', 'WeCom'), ('dingtalk', 'DingTalk'), ('feishu', 'FeiShu'), ('lark', 'Lark'), ('slack', 'Slack'), ('custom', 'Custom')], default='local', max_length=30, verbose_name='Source'), + ), + migrations.AlterUniqueTogether( + name='user', + unique_together={('wecom_id',), ('slack_id',), ('dingtalk_id',), ('lark_id',), ('feishu_id',)}, + ), + ] diff --git a/apps/users/models/user.py b/apps/users/models/user.py index f31ec6690..0d05da844 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -749,6 +749,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, LabeledMixin, JSONFilterM wecom = 'wecom', _('WeCom') dingtalk = 'dingtalk', _('DingTalk') feishu = 'feishu', _('FeiShu') + lark = 'lark', _('Lark') slack = 'slack', _('Slack') custom = 'custom', 'Custom' @@ -782,6 +783,9 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, LabeledMixin, JSONFilterM Source.feishu: [ settings.AUTH_BACKEND_FEISHU ], + Source.lark: [ + settings.AUTH_BACKEND_LARK + ], Source.slack: [ settings.AUTH_BACKEND_SLACK ], @@ -855,6 +859,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, LabeledMixin, JSONFilterM wecom_id = models.CharField(null=True, default=None, max_length=128, verbose_name=_('WeCom')) dingtalk_id = models.CharField(null=True, default=None, max_length=128, verbose_name=_('DingTalk')) feishu_id = models.CharField(null=True, default=None, max_length=128, verbose_name=_('FeiShu')) + lark_id = models.CharField(null=True, default=None, max_length=128, verbose_name='Lark') slack_id = models.CharField(null=True, default=None, max_length=128, verbose_name=_('Slack')) DATE_EXPIRED_WARNING_DAYS = 5 @@ -1006,6 +1011,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, LabeledMixin, JSONFilterM ('dingtalk_id',), ('wecom_id',), ('feishu_id',), + ('lark_id',), ('slack_id',), ) permissions = [ diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index aaeda080c..db7efb4f9 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -123,8 +123,8 @@ class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, ResourceLa # small 指的是 不需要计算的直接能从一张表中获取到的数据 fields_small = fields_mini + fields_write_only + [ "email", "wechat", "phone", "mfa_level", "source", - "wecom_id", "dingtalk_id", "feishu_id", "slack_id", - "created_by", "updated_by", "comment", # 通用字段 + "wecom_id", "dingtalk_id", "feishu_id", "lark_id", + "slack_id", "created_by", "updated_by", "comment", # 通用字段 ] fields_date = [ "date_expired", "date_joined", "last_login", @@ -154,7 +154,7 @@ class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, ResourceLa read_only_fields = [ "date_joined", "last_login", "created_by", "is_first_login", "wecom_id", "dingtalk_id", - "feishu_id", "date_api_key_last_used", + "feishu_id", "lark_id", "date_api_key_last_used", ] disallow_self_update_fields = ["is_active", "system_roles", "org_roles"] extra_kwargs = {