feat: 拆分 feishu lark

pull/12877/head
feng 2024-03-22 18:05:43 +08:00 committed by Bryan
parent ccd4f3ada4
commit 470a088a9f
34 changed files with 329 additions and 75 deletions

View File

@ -36,6 +36,7 @@ class AuthBackendLabelMapping(LazyObject):
backend_label_mapping[settings.AUTH_BACKEND_AUTH_TOKEN] = _("Auth Token") backend_label_mapping[settings.AUTH_BACKEND_AUTH_TOKEN] = _("Auth Token")
backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _("WeCom") backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _("WeCom")
backend_label_mapping[settings.AUTH_BACKEND_FEISHU] = _("FeiShu") 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_SLACK] = _("Slack")
backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _("DingTalk") backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _("DingTalk")
backend_label_mapping[settings.AUTH_BACKEND_TEMP_TOKEN] = _("Temporary token") backend_label_mapping[settings.AUTH_BACKEND_TEMP_TOKEN] = _("Temporary token")

View File

@ -6,6 +6,7 @@ from .common import *
from .confirm import * from .confirm import *
from .connection_token import * from .connection_token import *
from .feishu import * from .feishu import *
from .lark import *
from .login_confirm import * from .login_confirm import *
from .mfa import * from .mfa import *
from .password import * from .password import *

View File

@ -12,7 +12,6 @@ from common.permissions import IsValidUser, OnlySuperUser
from common.utils import get_logger from common.utils import get_logger
from users.models import User from users.models import User
logger = get_logger(__file__) logger = get_logger(__file__)
@ -24,6 +23,7 @@ class QRUnBindBase(APIView):
'wecom': {'user_field': 'wecom_id', 'not_bind_err': errors.WeComNotBound}, 'wecom': {'user_field': 'wecom_id', 'not_bind_err': errors.WeComNotBound},
'dingtalk': {'user_field': 'dingtalk_id', 'not_bind_err': errors.DingTalkNotBound}, 'dingtalk': {'user_field': 'dingtalk_id', 'not_bind_err': errors.DingTalkNotBound},
'feishu': {'user_field': 'feishu_id', 'not_bind_err': errors.FeiShuNotBound}, '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}, 'slack': {'user_field': 'slack_id', 'not_bind_err': errors.SlackNotBound},
} }
user = self.user user = self.user

View File

@ -0,0 +1,8 @@
from common.utils import get_logger
from .feishu import FeiShuEventSubscriptionCallback
logger = get_logger(__name__)
class LarkEventSubscriptionCallback(FeiShuEventSubscriptionCallback):
pass

View File

@ -55,6 +55,12 @@ class FeiShuAuthentication(JMSModelBackend):
pass pass
class LarkAuthentication(FeiShuAuthentication):
@staticmethod
def is_enabled():
return settings.AUTH_LARK
class SlackAuthentication(JMSModelBackend): class SlackAuthentication(JMSModelBackend):
""" """
什么也不做呀😺 什么也不做呀😺
@ -72,5 +78,6 @@ class AuthorizationTokenAuthentication(JMSModelBackend):
""" """
什么也不做呀😺 什么也不做呀😺
""" """
def authenticate(self, request, **kwargs): def authenticate(self, request, **kwargs):
pass pass

View File

@ -33,6 +33,11 @@ class FeiShuNotBound(JMSException):
default_detail = _('FeiShu is not bound') default_detail = _('FeiShu is not bound')
class LarkNotBound(JMSException):
default_code = 'lark_not_bound'
default_detail = _('Lark is not bound')
class SlackNotBound(JMSException): class SlackNotBound(JMSException):
default_code = 'slack_not_bound' default_code = 'slack_not_bound'
default_detail = _('Slack is not bound') default_detail = _('Slack is not bound')

View File

@ -22,6 +22,9 @@ urlpatterns = [
path('feishu/event/subscription/callback/', api.FeiShuEventSubscriptionCallback.as_view(), path('feishu/event/subscription/callback/', api.FeiShuEventSubscriptionCallback.as_view(),
name='feishu-event-subscription-callback'), 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('auth/', api.TokenCreateApi.as_view(), name='user-auth'),
path('confirm-oauth/', api.ConfirmBindORUNBindOAuth.as_view(), name='confirm-oauth'), path('confirm-oauth/', api.ConfirmBindORUNBindOAuth.as_view(), name='confirm-oauth'),
path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'), path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'),

View File

@ -49,6 +49,12 @@ urlpatterns = [
path('feishu/qr/bind/callback/', views.FeiShuQRBindCallbackView.as_view(), name='feishu-qr-bind-callback'), 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('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/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/bind/', views.SlackQRBindView.as_view(), name='slack-qr-bind'),
path('slack/qr/login/', views.SlackQRLoginView.as_view(), name='slack-qr-login'), path('slack/qr/login/', views.SlackQRLoginView.as_view(), name='slack-qr-login'),

View File

@ -1,8 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from .login import *
from .mfa import *
from .wecom import *
from .dingtalk import * from .dingtalk import *
from .feishu import * from .feishu import *
from .lark import *
from .login import *
from .mfa import *
from .slack import * from .slack import *
from .wecom import *

View File

@ -21,24 +21,45 @@ from .mixins import FlashMessageMixin
logger = get_logger(__file__) 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): 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): def dispatch(self, request, *args, **kwargs):
try: try:
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
except APIException as e: except APIException as e:
msg = str(e.detail) msg = str(e.detail)
return self.get_failed_response( return self.get_failed_response(
'/', '/', self.error, msg
_('FeiShu Error'),
msg
) )
def verify_state(self): def verify_state(self):
state = self.request.GET.get('state') 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: if state != session_state:
return False return False
return True return True
@ -49,19 +70,18 @@ class FeiShuQRMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashMe
def get_qr_url(self, redirect_uri): def get_qr_url(self, redirect_uri):
state = random_string(16) state = random_string(16)
self.request.session[FEISHU_STATE_SESSION_KEY] = state self.request.session[self.state_session_key] = state
params = { params = {
'app_id': settings.FEISHU_APP_ID, 'app_id': getattr(settings, f'{self.category}_APP_ID'.upper()),
'state': state, 'state': state,
'redirect_uri': redirect_uri, 'redirect_uri': redirect_uri,
} }
url = URL().authen + '?' + urlencode(params) url = self.url_object.authen + '?' + urlencode(params)
return url return url
def get_already_bound_response(self, redirect_url): def get_already_bound_response(self, redirect_url):
msg = _('FeiShu is already bound') response = self.get_failed_response(redirect_url, self.error_msg, self.error_msg)
response = self.get_failed_response(redirect_url, msg, msg)
return response return response
@ -71,7 +91,7 @@ class FeiShuQRBindView(FeiShuQRMixin, View):
def get(self, request: HttpRequest): def get(self, request: HttpRequest):
redirect_url = request.GET.get('redirect_url') 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}) redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
url = self.get_qr_url(redirect_uri) url = self.get_qr_url(redirect_uri)
@ -81,25 +101,16 @@ class FeiShuQRBindView(FeiShuQRMixin, View):
class FeiShuQRBindCallbackView(FeiShuQRMixin, BaseBindCallbackView): class FeiShuQRBindCallbackView(FeiShuQRMixin, BaseBindCallbackView):
permission_classes = (IsAuthenticated,) 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 = 'feishu'
auth_type_label = _('FeiShu') auth_type_label = _('FeiShu')
client_type_path = f'common.sdk.im.{auth_type}.FeiShu'
@property
class FeiShuEnableStartView(UserVerifyPasswordView): def client_auth_params(self):
return {
def get_success_url(self): 'app_id': f'{self.auth_type}_APP_ID'.upper(),
referer = self.request.META.get('HTTP_REFERER') 'app_secret': f'{self.auth_type}_APP_SECRET'.upper()
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
class FeiShuQRLoginView(FeiShuQRMixin, View): class FeiShuQRLoginView(FeiShuQRMixin, View):
@ -107,7 +118,7 @@ class FeiShuQRLoginView(FeiShuQRMixin, View):
def get(self, request: HttpRequest): def get(self, request: HttpRequest):
redirect_url = request.GET.get('redirect_url') or reverse('index') 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_uri += '?' + urlencode({
'redirect_url': redirect_url, 'redirect_url': redirect_url,
}) })
@ -119,11 +130,19 @@ class FeiShuQRLoginView(FeiShuQRMixin, View):
class FeiShuQRLoginCallbackView(FeiShuQRMixin, BaseLoginCallbackView): class FeiShuQRLoginCallbackView(FeiShuQRMixin, BaseLoginCallbackView):
permission_classes = (AllowAny,) 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' 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_client_err = _('FeiShu Error')
msg_user_not_bound_err = _('FeiShu is not bound') msg_user_not_bound_err = _('FeiShu is not bound')
msg_not_found_user_from_client_err = _('Failed to get user from FeiShu') 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()
}

View File

@ -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')

View File

@ -91,6 +91,12 @@ class UserLoginContextMixin:
'url': reverse('authentication:feishu-qr-login'), 'url': reverse('authentication:feishu-qr-login'),
'logo': static('img/login_feishu_logo.png') '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'), 'name': _('Slack'),
'enabled': settings.AUTH_SLACK, 'enabled': settings.AUTH_SLACK,

View File

@ -2,24 +2,18 @@ import json
from rest_framework.exceptions import APIException 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.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__) logger = get_logger(__name__)
class URL: class URL:
# https://open.feishu.cn/document/ukTMukTMukTM/uEDO4UjLxgDO14SM4gTN # https://open.feishu.cn/document/ukTMukTMukTM/uEDO4UjLxgDO14SM4gTN
@property
def host(self): host = 'https://open.feishu.cn'
if settings.FEISHU_VERSION == 'feishu':
h = 'https://open.feishu.cn'
else:
h = 'https://open.larksuite.com'
return h
@property @property
def authen(self): def authen(self):
@ -87,12 +81,13 @@ class FeiShu(RequestMixin):
""" """
非业务数据导致的错误直接抛异常说明是系统配置错误业务代码不用理会 非业务数据导致的错误直接抛异常说明是系统配置错误业务代码不用理会
""" """
requests_cls = FeishuRequests
def __init__(self, app_id, app_secret, timeout=None): def __init__(self, app_id, app_secret, timeout=None):
self._app_id = app_id or '' self._app_id = app_id or ''
self._app_secret = app_secret or '' self._app_secret = app_secret or ''
self._requests = FeishuRequests( self._requests = self.requests_cls(
app_id=app_id, app_id=app_id,
app_secret=app_secret, app_secret=app_secret,
timeout=timeout timeout=timeout
@ -130,7 +125,7 @@ class FeiShu(RequestMixin):
body['receive_id'] = user_id body['receive_id'] = user_id
try: 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) self._requests.post(URL().send_message, params=params, json=body)
except APIException as e: except APIException as e:
# 只处理可预知的错误 # 只处理可预知的错误

View File

@ -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

View File

@ -407,7 +407,11 @@ class Config(dict):
'AUTH_FEISHU': False, 'AUTH_FEISHU': False,
'FEISHU_APP_ID': '', 'FEISHU_APP_ID': '',
'FEISHU_APP_SECRET': '', 'FEISHU_APP_SECRET': '',
'FEISHU_VERSION': 'feishu',
# Lark
'AUTH_LARK': False,
'LARK_APP_ID': '',
'LARK_APP_SECRET': '',
# Slack # Slack
'AUTH_SLACK': False, 'AUTH_SLACK': False,

View File

@ -141,7 +141,10 @@ DINGTALK_APPSECRET = CONFIG.DINGTALK_APPSECRET
AUTH_FEISHU = CONFIG.AUTH_FEISHU AUTH_FEISHU = CONFIG.AUTH_FEISHU
FEISHU_APP_ID = CONFIG.FEISHU_APP_ID FEISHU_APP_ID = CONFIG.FEISHU_APP_ID
FEISHU_APP_SECRET = CONFIG.FEISHU_APP_SECRET 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 # Slack auth
AUTH_SLACK = CONFIG.AUTH_SLACK 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_WECOM = 'authentication.backends.sso.WeComAuthentication'
AUTH_BACKEND_DINGTALK = 'authentication.backends.sso.DingTalkAuthentication' AUTH_BACKEND_DINGTALK = 'authentication.backends.sso.DingTalkAuthentication'
AUTH_BACKEND_FEISHU = 'authentication.backends.sso.FeiShuAuthentication' AUTH_BACKEND_FEISHU = 'authentication.backends.sso.FeiShuAuthentication'
AUTH_BACKEND_LARK = 'authentication.backends.sso.LarkAuthentication'
AUTH_BACKEND_SLACK = 'authentication.backends.sso.SlackAuthentication' AUTH_BACKEND_SLACK = 'authentication.backends.sso.SlackAuthentication'
AUTH_BACKEND_AUTH_TOKEN = 'authentication.backends.sso.AuthorizationTokenAuthentication' AUTH_BACKEND_AUTH_TOKEN = 'authentication.backends.sso.AuthorizationTokenAuthentication'
AUTH_BACKEND_SAML2 = 'authentication.backends.saml2.SAML2Backend' 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_CAS, AUTH_BACKEND_OIDC_PASSWORD, AUTH_BACKEND_OIDC_CODE, AUTH_BACKEND_SAML2,
AUTH_BACKEND_OAUTH2, 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模式 # Token模式
AUTH_BACKEND_AUTH_TOKEN, AUTH_BACKEND_SSO, AUTH_BACKEND_TEMP_TOKEN, AUTH_BACKEND_AUTH_TOKEN, AUTH_BACKEND_SSO, AUTH_BACKEND_TEMP_TOKEN,
AUTH_BACKEND_PASSKEY AUTH_BACKEND_PASSKEY

View File

@ -1,7 +1,7 @@
import importlib import importlib
from django.utils.translation import gettext_lazy as _
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _
client_name_mapper = {} client_name_mapper = {}
@ -12,7 +12,9 @@ class BACKEND(models.TextChoices):
DINGTALK = 'dingtalk', _('DingTalk') DINGTALK = 'dingtalk', _('DingTalk')
SITE_MSG = 'site_msg', _('Site message') SITE_MSG = 'site_msg', _('Site message')
FEISHU = 'feishu', _('FeiShu') FEISHU = 'feishu', _('FeiShu')
LARK = 'lark', 'Lark'
SLACK = 'slack', _('Slack') SLACK = 'slack', _('Slack')
# SMS = 'sms', _('SMS') # SMS = 'sms', _('SMS')
@property @property

View File

@ -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

View File

@ -196,6 +196,9 @@ class Message(metaclass=MessageType):
def get_feishu_msg(self) -> dict: def get_feishu_msg(self) -> dict:
return self.markdown_msg return self.markdown_msg
def get_lark_msg(self) -> dict:
return self.markdown_msg
def get_email_msg(self) -> dict: def get_email_msg(self) -> dict:
return self.html_msg_with_sign return self.html_msg_with_sign

View File

@ -2,6 +2,7 @@ from .chat import *
from .dingtalk import * from .dingtalk import *
from .email import * from .email import *
from .feishu import * from .feishu import *
from .lark import *
from .ldap import * from .ldap import *
from .public import * from .public import *
from .security import * from .security import *

View File

@ -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 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 common.sdk.im.feishu import FeiShu
from settings.models import Setting
from .. import serializers from .. import serializers
class FeiShuTestingAPI(GenericAPIView): class FeiShuTestingAPI(GenericAPIView):
category = 'FEISHU'
serializer_class = serializers.FeiShuSettingSerializer serializer_class = serializers.FeiShuSettingSerializer
rbac_perms = { rbac_perms = {
'POST': 'settings.change_auth' 'POST': 'settings.change_auth'
@ -20,11 +20,11 @@ class FeiShuTestingAPI(GenericAPIView):
serializer = self.serializer_class(data=request.data) serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
app_id = serializer.validated_data['FEISHU_APP_ID'] app_id = serializer.validated_data[f'{self.category}_APP_ID']
app_secret = serializer.validated_data.get('FEISHU_APP_SECRET') app_secret = serializer.validated_data.get(f'{self.category}_APP_SECRET')
if not 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: if secret:
app_secret = secret.cleaned_value app_secret = secret.cleaned_value
@ -40,3 +40,8 @@ class FeiShuTestingAPI(GenericAPIView):
except: except:
error = e.detail error = e.detail
return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': error}) return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': error})
class LarkTestingAPI(FeiShuTestingAPI):
category = 'LARK'
serializer_class = serializers.LarkSettingSerializer

View File

@ -0,0 +1,7 @@
from .feishu import FeiShuTestingAPI
from .. import serializers
class LarkTestingAPI(FeiShuTestingAPI):
category = 'LARK'
serializer_class = serializers.LarkSettingSerializer

View File

@ -39,6 +39,7 @@ class SettingsApi(generics.RetrieveUpdateAPIView):
'wecom': serializers.WeComSettingSerializer, 'wecom': serializers.WeComSettingSerializer,
'dingtalk': serializers.DingTalkSettingSerializer, 'dingtalk': serializers.DingTalkSettingSerializer,
'feishu': serializers.FeiShuSettingSerializer, 'feishu': serializers.FeiShuSettingSerializer,
'lark': serializers.LarkSettingSerializer,
'slack': serializers.SlackSettingSerializer, 'slack': serializers.SlackSettingSerializer,
'auth': serializers.AuthSettingSerializer, 'auth': serializers.AuthSettingSerializer,
'oidc': serializers.OIDCSettingSerializer, 'oidc': serializers.OIDCSettingSerializer,

View File

@ -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),
]

View File

@ -2,13 +2,14 @@ from .base import *
from .cas import * from .cas import *
from .dingtalk import * from .dingtalk import *
from .feishu import * from .feishu import *
from .lark import *
from .ldap import * from .ldap import *
from .oauth2 import * from .oauth2 import *
from .oidc import * from .oidc import *
from .passkey import * from .passkey import *
from .radius import * from .radius import *
from .saml2 import * from .saml2 import *
from .slack import *
from .sms import * from .sms import *
from .sso import * from .sso import *
from .wecom import * from .wecom import *
from .slack import *

View File

@ -17,6 +17,7 @@ class AuthSettingSerializer(serializers.Serializer):
AUTH_RADIUS = serializers.BooleanField(required=False, label=_('RADIUS Auth')) AUTH_RADIUS = serializers.BooleanField(required=False, label=_('RADIUS Auth'))
AUTH_DINGTALK = serializers.BooleanField(default=False, label=_('DingTalk Auth')) AUTH_DINGTALK = serializers.BooleanField(default=False, label=_('DingTalk Auth'))
AUTH_FEISHU = serializers.BooleanField(default=False, label=_('FeiShu 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_WECOM = serializers.BooleanField(default=False, label=_('Slack Auth'))
AUTH_SLACK = serializers.BooleanField(default=False, label=_('WeCom Auth')) AUTH_SLACK = serializers.BooleanField(default=False, label=_('WeCom Auth'))
AUTH_SSO = serializers.BooleanField(default=False, label=_("SSO Auth")) AUTH_SSO = serializers.BooleanField(default=False, label=_("SSO Auth"))

View File

@ -9,13 +9,6 @@ __all__ = ['FeiShuSettingSerializer']
class FeiShuSettingSerializer(serializers.Serializer): class FeiShuSettingSerializer(serializers.Serializer):
PREFIX_TITLE = _('FeiShu') PREFIX_TITLE = _('FeiShu')
VERSION_CHOICES = (
('feishu', _('FeiShu')),
('lark', 'Lark')
)
AUTH_FEISHU = serializers.BooleanField(default=False, label=_('Enable FeiShu Auth')) 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_ID = serializers.CharField(max_length=256, required=True, label='App ID')
FEISHU_APP_SECRET = EncryptedField(max_length=256, required=False, label='App Secret') FEISHU_APP_SECRET = EncryptedField(max_length=256, required=False, label='App Secret')
FEISHU_VERSION = serializers.ChoiceField(
choices=VERSION_CHOICES, default='feishu', label=_('Version')
)

View File

@ -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')

View File

@ -40,6 +40,7 @@ class PrivateSettingSerializer(PublicSettingSerializer):
AUTH_WECOM = serializers.BooleanField() AUTH_WECOM = serializers.BooleanField()
AUTH_DINGTALK = serializers.BooleanField() AUTH_DINGTALK = serializers.BooleanField()
AUTH_FEISHU = serializers.BooleanField() AUTH_FEISHU = serializers.BooleanField()
AUTH_LARK = serializers.BooleanField()
AUTH_TEMP_TOKEN = serializers.BooleanField() AUTH_TEMP_TOKEN = serializers.BooleanField()
TERMINAL_RAZOR_ENABLED = serializers.BooleanField() TERMINAL_RAZOR_ENABLED = serializers.BooleanField()

View File

@ -7,9 +7,9 @@ from common.utils import i18n_fmt
from .auth import ( from .auth import (
LDAPSettingSerializer, OIDCSettingSerializer, KeycloakSettingSerializer, LDAPSettingSerializer, OIDCSettingSerializer, KeycloakSettingSerializer,
CASSettingSerializer, RadiusSettingSerializer, FeiShuSettingSerializer, CASSettingSerializer, RadiusSettingSerializer, FeiShuSettingSerializer,
WeComSettingSerializer, DingTalkSettingSerializer, AlibabaSMSSettingSerializer, LarkSettingSerializer, WeComSettingSerializer, DingTalkSettingSerializer,
TencentSMSSettingSerializer, CMPP2SMSSettingSerializer, AuthSettingSerializer, AlibabaSMSSettingSerializer, TencentSMSSettingSerializer, CMPP2SMSSettingSerializer,
SAML2SettingSerializer, OAuth2SettingSerializer, PasskeySettingSerializer, AuthSettingSerializer, SAML2SettingSerializer, OAuth2SettingSerializer, PasskeySettingSerializer,
CustomSMSSettingSerializer, CustomSMSSettingSerializer,
) )
from .basic import BasicSettingSerializer from .basic import BasicSettingSerializer
@ -75,6 +75,7 @@ class SettingsSerializer(
WeComSettingSerializer, WeComSettingSerializer,
DingTalkSettingSerializer, DingTalkSettingSerializer,
FeiShuSettingSerializer, FeiShuSettingSerializer,
LarkSettingSerializer,
EmailSettingSerializer, EmailSettingSerializer,
EmailContentSettingSerializer, EmailContentSettingSerializer,
OtherSettingSerializer, OtherSettingSerializer,

View File

@ -15,6 +15,7 @@ urlpatterns = [
path('wecom/testing/', api.WeComTestingAPI.as_view(), name='wecom-testing'), path('wecom/testing/', api.WeComTestingAPI.as_view(), name='wecom-testing'),
path('dingtalk/testing/', api.DingTalkTestingAPI.as_view(), name='dingtalk-testing'), path('dingtalk/testing/', api.DingTalkTestingAPI.as_view(), name='dingtalk-testing'),
path('feishu/testing/', api.FeiShuTestingAPI.as_view(), name='feishu-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('slack/testing/', api.SlackTestingAPI.as_view(), name='slack-testing'),
path('sms/<str:backend>/testing/', api.SMSTestingAPI.as_view(), name='sms-testing'), path('sms/<str:backend>/testing/', api.SMSTestingAPI.as_view(), name='sms-testing'),
path('sms/backend/', api.SMSBackendAPI.as_view(), name='sms-backend'), path('sms/backend/', api.SMSBackendAPI.as_view(), name='sms-backend'),

View File

@ -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',)},
),
]

View File

@ -749,6 +749,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, LabeledMixin, JSONFilterM
wecom = 'wecom', _('WeCom') wecom = 'wecom', _('WeCom')
dingtalk = 'dingtalk', _('DingTalk') dingtalk = 'dingtalk', _('DingTalk')
feishu = 'feishu', _('FeiShu') feishu = 'feishu', _('FeiShu')
lark = 'lark', _('Lark')
slack = 'slack', _('Slack') slack = 'slack', _('Slack')
custom = 'custom', 'Custom' custom = 'custom', 'Custom'
@ -782,6 +783,9 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, LabeledMixin, JSONFilterM
Source.feishu: [ Source.feishu: [
settings.AUTH_BACKEND_FEISHU settings.AUTH_BACKEND_FEISHU
], ],
Source.lark: [
settings.AUTH_BACKEND_LARK
],
Source.slack: [ Source.slack: [
settings.AUTH_BACKEND_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')) 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')) 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')) 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')) slack_id = models.CharField(null=True, default=None, max_length=128, verbose_name=_('Slack'))
DATE_EXPIRED_WARNING_DAYS = 5 DATE_EXPIRED_WARNING_DAYS = 5
@ -1006,6 +1011,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, LabeledMixin, JSONFilterM
('dingtalk_id',), ('dingtalk_id',),
('wecom_id',), ('wecom_id',),
('feishu_id',), ('feishu_id',),
('lark_id',),
('slack_id',), ('slack_id',),
) )
permissions = [ permissions = [

View File

@ -123,8 +123,8 @@ class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, ResourceLa
# small 指的是 不需要计算的直接能从一张表中获取到的数据 # small 指的是 不需要计算的直接能从一张表中获取到的数据
fields_small = fields_mini + fields_write_only + [ fields_small = fields_mini + fields_write_only + [
"email", "wechat", "phone", "mfa_level", "source", "email", "wechat", "phone", "mfa_level", "source",
"wecom_id", "dingtalk_id", "feishu_id", "slack_id", "wecom_id", "dingtalk_id", "feishu_id", "lark_id",
"created_by", "updated_by", "comment", # 通用字段 "slack_id", "created_by", "updated_by", "comment", # 通用字段
] ]
fields_date = [ fields_date = [
"date_expired", "date_joined", "last_login", "date_expired", "date_joined", "last_login",
@ -154,7 +154,7 @@ class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, ResourceLa
read_only_fields = [ read_only_fields = [
"date_joined", "last_login", "created_by", "date_joined", "last_login", "created_by",
"is_first_login", "wecom_id", "dingtalk_id", "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"] disallow_self_update_fields = ["is_active", "system_roles", "org_roles"]
extra_kwargs = { extra_kwargs = {