diff --git a/apps/authentication/views/dingtalk.py b/apps/authentication/views/dingtalk.py index e0e76f966..7b421851a 100644 --- a/apps/authentication/views/dingtalk.py +++ b/apps/authentication/views/dingtalk.py @@ -23,7 +23,7 @@ from common.utils.random import random_string from users.models import User from users.views import UserVerifyPasswordView -from .mixins import METAMixin +from .mixins import METAMixin, QRLoginCallbackMixin logger = get_logger(__file__) @@ -158,7 +158,7 @@ class DingTalkQRBindCallbackView(DingTalkQRMixin, View): appsecret=settings.DINGTALK_APPSECRET, agentid=settings.DINGTALK_AGENTID ) - userid = dingtalk.get_userid_by_code(code) + userid, __ = dingtalk.get_user_id_by_code(code) if not userid: msg = _('DingTalk query user failed') @@ -214,45 +214,20 @@ class DingTalkQRLoginView(DingTalkQRMixin, METAMixin, View): return HttpResponseRedirect(url) -class DingTalkQRLoginCallbackView(AuthMixin, DingTalkQRMixin, View): +class DingTalkQRLoginCallbackView(QRLoginCallbackMixin, AuthMixin, DingTalkQRMixin, View): permission_classes = (AllowAny,) - def get(self, request: HttpRequest): - code = request.GET.get('code') - redirect_url = request.GET.get('redirect_url') - login_url = reverse('authentication:login') + CLIENT_INFO = ( + DingTalk, {'appid': 'DINGTALK_APPKEY', 'appsecret': 'DINGTALK_APPSECRET', 'agentid': 'DINGTALK_AGENTID'} + ) + USER_TYPE = 'dingtalk' + AUTH_BACKEND = 'AUTH_BACKEND_DINGTALK' + CREATE_USER_IF_NOT_EXIST = 'DINGTALK_CREATE_USER_IF_NOT_EXIST' - if not self.verify_state(): - return self.get_verify_state_failed_response(redirect_url) - - dingtalk = DingTalk( - appid=settings.DINGTALK_APPKEY, - appsecret=settings.DINGTALK_APPSECRET, - agentid=settings.DINGTALK_AGENTID - ) - userid = dingtalk.get_userid_by_code(code) - if not userid: - # 正常流程不会出这个错误,hack 行为 - msg = _('Failed to get user from DingTalk') - response = self.get_failed_response(login_url, title=msg, msg=msg) - return response - - user = get_object_or_none(User, dingtalk_id=userid) - if user is None: - title = _('DingTalk is not bound') - msg = _('Please login with a password and then bind the DingTalk') - response = self.get_failed_response(login_url, title=title, msg=msg) - return response - - try: - self.check_oauth2_auth(user, settings.AUTH_BACKEND_DINGTALK) - except errors.AuthFailedError as e: - self.set_login_failed_mark() - msg = e.msg - response = self.get_failed_response(login_url, title=msg, msg=msg) - return response - - return self.redirect_to_guard_view() + MSG_CLIENT_ERR = _('DingTalk Error') + MSG_USER_NOT_BOUND_ERR = _('DingTalk is not bound') + MSG_USER_NEED_BOUND_WARNING = _('Please login with a password and then bind the DingTalk') + MSG_NOT_FOUND_USER_FROM_CLIENT_ERR = _('Failed to get user from DingTalk') class DingTalkOAuthLoginView(DingTalkOAuthMixin, View): @@ -284,7 +259,7 @@ class DingTalkOAuthLoginCallbackView(AuthMixin, DingTalkOAuthMixin, View): appsecret=settings.DINGTALK_APPSECRET, agentid=settings.DINGTALK_AGENTID ) - userid = dingtalk.get_userid_by_code(code) + userid, __ = dingtalk.get_user_id_by_code(code) if not userid: # 正常流程不会出这个错误,hack 行为 msg = _('Failed to get user from DingTalk') diff --git a/apps/authentication/views/feishu.py b/apps/authentication/views/feishu.py index e9734b170..4ba777097 100644 --- a/apps/authentication/views/feishu.py +++ b/apps/authentication/views/feishu.py @@ -9,7 +9,6 @@ from django.views import View from rest_framework.exceptions import APIException from rest_framework.permissions import AllowAny, IsAuthenticated -from authentication import errors from authentication.const import ConfirmType from authentication.mixins import AuthMixin from authentication.notifications import OAuthBindMessage @@ -18,11 +17,12 @@ from common.permissions import UserConfirmation from common.sdk.im.feishu import URL, FeiShu from common.utils import FlashMessageUtil, get_logger from common.utils.common import get_request_ip -from common.utils.django import get_object_or_none, reverse +from common.utils.django import reverse from common.utils.random import random_string -from users.models import User from users.views import UserVerifyPasswordView +from .mixins import QRLoginCallbackMixin + logger = get_logger(__file__) FEISHU_STATE_SESSION_KEY = '_feishu_state' @@ -123,7 +123,7 @@ class FeiShuQRBindCallbackView(FeiShuQRMixin, View): app_id=settings.FEISHU_APP_ID, app_secret=settings.FEISHU_APP_SECRET ) - user_id = feishu.get_user_id_by_code(code) + user_id, __ = feishu.get_user_id_by_code(code) if not user_id: msg = _('FeiShu query user failed') @@ -176,41 +176,17 @@ class FeiShuQRLoginView(FeiShuQRMixin, View): return HttpResponseRedirect(url) -class FeiShuQRLoginCallbackView(AuthMixin, FeiShuQRMixin, View): +class FeiShuQRLoginCallbackView(QRLoginCallbackMixin, 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') + CLIENT_INFO = ( + FeiShu, {'app_id': 'FEISHU_APP_ID', 'app_secret': 'FEISHU_APP_SECRET'} + ) + USER_TYPE = 'feishu' + AUTH_BACKEND = 'AUTH_BACKEND_FEISHU' + CREATE_USER_IF_NOT_EXIST = 'FEISHU_CREATE_USER_IF_NOT_EXIST' - 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_response(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 FeiShu') - response = self.get_failed_response(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_response(login_url, title=msg, msg=msg) - return response - - return self.redirect_to_guard_view() + MSG_CLIENT_ERR = _('FeiShu Error') + MSG_USER_NOT_BOUND_ERR = _('FeiShu is not bound') + MSG_USER_NEED_BOUND_WARNING = _('Please login with a password and then bind the FeiShu') + MSG_NOT_FOUND_USER_FROM_CLIENT_ERR = _('Failed to get user from FeiShu') diff --git a/apps/authentication/views/mixins.py b/apps/authentication/views/mixins.py index 3571dfac7..aa79eef1c 100644 --- a/apps/authentication/views/mixins.py +++ b/apps/authentication/views/mixins.py @@ -1,5 +1,20 @@ # -*- coding: utf-8 -*- # +from functools import lru_cache + +from rest_framework.request import Request +from django.utils.translation import ugettext_lazy as _ +from django.conf import settings +from django.db.utils import IntegrityError + +from authentication import errors +from users.models import User +from common.utils.django import reverse, get_object_or_none +from common.utils import get_logger + + +logger = get_logger(__file__) + class METAMixin: def get_next_url_from_meta(self): @@ -10,3 +25,92 @@ class METAMixin: if len(next_url_item) > 1: next_url = next_url_item[-1] return next_url + + +class Client: + get_user_id_by_code: callable + get_user_detail: callable + + +class QRLoginCallbackMixin: + verify_state: callable + get_verify_state_failed_response: callable + get_failed_response: callable + check_oauth2_auth: callable + set_login_failed_mark: callable + redirect_to_guard_view: callable + # 属性 + _client: Client + CLIENT_INFO: tuple + USER_TYPE: str + AUTH_BACKEND: str + CREATE_USER_IF_NOT_EXIST: str + # 提示信息 + MSG_CLIENT_ERR: str + MSG_USER_NOT_BOUND_ERR: str + MSG_USER_NEED_BOUND_WARNING: str + MSG_NOT_FOUND_USER_FROM_CLIENT_ERR: str + + @property + @lru_cache(maxsize=1) + def client(self): + client_type, client_init = self.CLIENT_INFO + client_init = {k: getattr(settings, v) for k, v in client_init.items()} + return client_type(**client_init) + + def create_user_if_not_exist(self, user_id, **kwargs): + user = None + if not getattr(settings, self.CREATE_USER_IF_NOT_EXIST): + title = self.MSG_CLIENT_ERR + msg = self.MSG_USER_NEED_BOUND_WARNING + return user, (title, msg) + + user_attr = self.client.get_user_detail(user_id, **kwargs) + try: + user, create = User.objects.get_or_create( + username=user_attr['username'], defaults=user_attr + ) + setattr(user, f'{self.USER_TYPE}_id', user_id) + if create: + setattr(user, 'source', self.USER_TYPE) + user.save() + except IntegrityError as err: + logger.error(f'{self.MSG_CLIENT_ERR}: create user error: {err}') + + if user is None: + title = self.MSG_CLIENT_ERR + msg = _('If you have any question, please contact the administrator') + return user, (title, msg) + + return user, None + + def get(self, request: Request): + 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) + + user_id, other_info = self.client.get_user_id_by_code(code) + if not user_id: + # 正常流程不会出这个错误,hack 行为 + err = self.MSG_NOT_FOUND_USER_FROM_CLIENT_ERR + response = self.get_failed_response(login_url, title=err, msg=err) + return response + + user = get_object_or_none(User, **{f'{self.USER_TYPE}_id': user_id}) + if user is None: + user, err = self.create_user_if_not_exist(user_id, other_info=other_info) + if err is not None: + response = self.get_failed_response(login_url, title=err[0], msg=err[1]) + return response + + try: + self.check_oauth2_auth(user, getattr(settings, self.AUTH_BACKEND)) + except errors.AuthFailedError as e: + self.set_login_failed_mark() + msg = e.msg + response = self.get_failed_response(login_url, title=msg, msg=msg) + return response + return self.redirect_to_guard_view() diff --git a/apps/authentication/views/wecom.py b/apps/authentication/views/wecom.py index c764c2138..ba128834f 100644 --- a/apps/authentication/views/wecom.py +++ b/apps/authentication/views/wecom.py @@ -22,7 +22,8 @@ from authentication import errors from authentication.mixins import AuthMixin from authentication.const import ConfirmType from authentication.notifications import OAuthBindMessage -from .mixins import METAMixin + +from .mixins import METAMixin, QRLoginCallbackMixin logger = get_logger(__file__) @@ -208,45 +209,20 @@ class WeComQRLoginView(WeComQRMixin, METAMixin, View): return HttpResponseRedirect(url) -class WeComQRLoginCallbackView(AuthMixin, WeComQRMixin, View): +class WeComQRLoginCallbackView(QRLoginCallbackMixin, AuthMixin, WeComQRMixin, View): permission_classes = (AllowAny,) - def get(self, request: HttpRequest): - code = request.GET.get('code') - redirect_url = request.GET.get('redirect_url') - login_url = reverse('authentication:login') + CLIENT_INFO = ( + WeCom, {'corpid': 'WECOM_CORPID', 'corpsecret': 'WECOM_SECRET', 'agentid': 'WECOM_AGENTID'} + ) + USER_TYPE = 'wecom' + AUTH_BACKEND = 'AUTH_BACKEND_WECOM' + CREATE_USER_IF_NOT_EXIST = 'WECOM_CREATE_USER_IF_NOT_EXIST' - if not self.verify_state(): - return self.get_verify_state_failed_response(redirect_url) - - wecom = WeCom( - corpid=settings.WECOM_CORPID, - corpsecret=settings.WECOM_SECRET, - agentid=settings.WECOM_AGENTID - ) - wecom_userid, __ = wecom.get_user_id_by_code(code) - if not wecom_userid: - # 正常流程不会出这个错误,hack 行为 - msg = _('Failed to get user from WeCom') - response = self.get_failed_response(login_url, title=msg, msg=msg) - return response - - user = get_object_or_none(User, wecom_id=wecom_userid) - if user is None: - title = _('WeCom is not bound') - msg = _('Please login with a password and then bind the WeCom') - response = self.get_failed_response(login_url, title=title, msg=msg) - return response - - try: - self.check_oauth2_auth(user, settings.AUTH_BACKEND_WECOM) - except errors.AuthFailedError as e: - self.set_login_failed_mark() - msg = e.msg - response = self.get_failed_response(login_url, title=msg, msg=msg) - return response - - return self.redirect_to_guard_view() + MSG_CLIENT_ERR = _('WeCom Error') + MSG_USER_NOT_BOUND_ERR = _('WeCom is not bound') + MSG_USER_NEED_BOUND_WARNING = _('Please login with a password and then bind the WeCom') + MSG_NOT_FOUND_USER_FROM_CLIENT_ERR = _('Failed to get user from WeCom') class WeComOAuthLoginView(WeComOAuthMixin, View): diff --git a/apps/common/sdk/im/dingtalk/__init__.py b/apps/common/sdk/im/dingtalk/__init__.py index d41e73221..4f54e1ea2 100644 --- a/apps/common/sdk/im/dingtalk/__init__.py +++ b/apps/common/sdk/im/dingtalk/__init__.py @@ -5,6 +5,7 @@ import base64 from common.utils import get_logger from common.sdk.im.utils import digest, as_request from common.sdk.im.mixin import BaseRequest +from users.utils import construct_user_email logger = get_logger(__file__) @@ -35,6 +36,7 @@ class URL: SEND_MESSAGE = 'https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2' GET_SEND_MSG_PROGRESS = 'https://oapi.dingtalk.com/topapi/message/corpconversation/getsendprogress' GET_USERID_BY_UNIONID = 'https://oapi.dingtalk.com/topapi/user/getbyunionid' + GET_USER_INFO_BY_USER_ID = 'https://oapi.dingtalk.com/topapi/v2/user/get' class DingTalkRequests(BaseRequest): @@ -129,11 +131,11 @@ class DingTalk: data = self._request.post(URL.GET_USER_INFO_BY_CODE, json=body, with_sign=True) return data['user_info'] - def get_userid_by_code(self, code): + def get_user_id_by_code(self, code): user_info = self.get_userinfo_bycode(code) unionid = user_info['unionid'] userid = self.get_userid_by_unionid(unionid) - return userid + return userid, None def get_userid_by_unionid(self, unionid): body = { @@ -195,3 +197,18 @@ class DingTalk: data = self._request.post(URL.GET_SEND_MSG_PROGRESS, json=body, with_token=True) return data + + def get_user_detail(self, user_id, **kwargs): + # https://open.dingtalk.com/document/orgapp/query-user-details + body = {'userid': user_id} + data = self._request.post( + URL.GET_USER_INFO_BY_USER_ID, json=body, with_token=True + ) + data = data['result'] + username = user_id + name = data.get('name', username) + email = data.get('email') or data.get('org_email') + email = construct_user_email(username, email) + return { + 'username': username, 'name': name, 'email': email + } diff --git a/apps/common/sdk/im/feishu/__init__.py b/apps/common/sdk/im/feishu/__init__.py index cb01b66da..ef3a419b6 100644 --- a/apps/common/sdk/im/feishu/__init__.py +++ b/apps/common/sdk/im/feishu/__init__.py @@ -1,9 +1,9 @@ import json -from django.utils.translation import ugettext_lazy as _ 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 @@ -30,13 +30,16 @@ class URL: return f'{self.host}/open-apis/auth/v3/tenant_access_token/internal/' @property - def get_user_info_by_code(self): + def get_userinfo_by_code(self): return f'{self.host}/open-apis/authen/v1/access_token' @property def send_message(self): return f'{self.host}/open-apis/im/v1/messages' + def get_user_detail(self, user_id): + return f'{self.host}/open-apis/contact/v3/users/{user_id}' + class ErrorCode: INVALID_APP_ACCESS_TOKEN = 99991664 @@ -103,10 +106,10 @@ class FeiShu(RequestMixin): 'code': code } - data = self._requests.post(URL().get_user_info_by_code, json=body, check_errcode_is_0=False) + data = self._requests.post(URL().get_userinfo_by_code, json=body, check_errcode_is_0=False) self._requests.check_errcode_is_0(data) - return data['data']['user_id'] + return data['data']['user_id'], data['data'] def send_text(self, user_ids, msg): params = { @@ -130,3 +133,15 @@ class FeiShu(RequestMixin): logger.exception(e) invalid_users.append(user_id) return invalid_users + + @staticmethod + def get_user_detail(user_id, **kwargs): + # get_user_id_by_code 已经返回个人信息,这里直接解析 + data = kwargs['other_info'] + username = user_id + name = data.get('name', username) + email = data.get('email') or data.get('enterprise_email') + email = construct_user_email(username, email) + return { + 'username': username, 'name': name, 'email': email + } diff --git a/apps/common/sdk/im/wecom/__init__.py b/apps/common/sdk/im/wecom/__init__.py index bc925508e..5d2d6a4c1 100644 --- a/apps/common/sdk/im/wecom/__init__.py +++ b/apps/common/sdk/im/wecom/__init__.py @@ -3,8 +3,9 @@ from typing import Iterable, AnyStr from django.utils.translation import ugettext_lazy as _ from rest_framework.exceptions import APIException +from users.utils import construct_user_email from common.utils.common import get_logger -from common.sdk.im.utils import digest, DictWrapper, update_values, set_default +from common.sdk.im.utils import digest, update_values from common.sdk.im.mixin import RequestMixin, BaseRequest logger = get_logger(__name__) @@ -151,10 +152,7 @@ class WeCom(RequestMixin): def get_user_id_by_code(self, code): # # https://open.work.weixin.qq.com/api/doc/90000/90135/91437 - - params = { - 'code': code, - } + params = {'code': code} data = self._requests.get(URL.GET_USER_ID_BY_CODE, params=params, check_errcode_is_0=False) errcode = data['errcode'] @@ -175,12 +173,15 @@ class WeCom(RequestMixin): logger.error(f'WeCom response 200 but get field from json error: fields=UserId|OpenId') raise WeComError - def get_user_detail(self, id): + def get_user_detail(self, user_id, **kwargs): # https://open.work.weixin.qq.com/api/doc/90000/90135/90196 - - params = { - 'userid': id, + params = {'userid': user_id} + data = self._requests.get(URL.GET_USER_DETAIL, params) + username = data.get('userid') + name = data.get('name', username) + email = data.get('email') or data.get('biz_mail') + email = construct_user_email(username, email) + return { + 'username': username, 'name': name, 'email': email } - data = self._requests.get(URL.GET_USER_DETAIL, params) - return data diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 343224675..ba2593efd 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -365,18 +365,21 @@ class Config(dict): 'WECOM_CORPID': '', 'WECOM_AGENTID': '', 'WECOM_SECRET': '', + 'WECOM_CREATE_USER_IF_NOT_EXIST': False, # 钉钉 'AUTH_DINGTALK': False, 'DINGTALK_AGENTID': '', 'DINGTALK_APPKEY': '', 'DINGTALK_APPSECRET': '', + 'DINGTALK_CREATE_USER_IF_NOT_EXIST': False, # 飞书 'AUTH_FEISHU': False, 'FEISHU_APP_ID': '', 'FEISHU_APP_SECRET': '', 'FEISHU_VERSION': 'feishu', + 'FEISHU_CREATE_USER_IF_NOT_EXIST': False, 'LOGIN_REDIRECT_TO_BACKEND': '', # 'OPENID / CAS / SAML2 'LOGIN_REDIRECT_MSG_ENABLED': True, diff --git a/apps/settings/serializers/auth/dingtalk.py b/apps/settings/serializers/auth/dingtalk.py index 4ec7814a1..f74d267b8 100644 --- a/apps/settings/serializers/auth/dingtalk.py +++ b/apps/settings/serializers/auth/dingtalk.py @@ -13,3 +13,6 @@ class DingTalkSettingSerializer(serializers.Serializer): DINGTALK_APPKEY = serializers.CharField(max_length=256, required=True, label='AppKey') DINGTALK_APPSECRET = EncryptedField(max_length=256, required=False, label='AppSecret') AUTH_DINGTALK = serializers.BooleanField(default=False, label=_('Enable DingTalk Auth')) + DINGTALK_CREATE_USER_IF_NOT_EXIST = serializers.BooleanField( + default=False, label=_('Create user if not') + ) diff --git a/apps/settings/serializers/auth/feishu.py b/apps/settings/serializers/auth/feishu.py index a06d41b23..0e12f04ab 100644 --- a/apps/settings/serializers/auth/feishu.py +++ b/apps/settings/serializers/auth/feishu.py @@ -19,3 +19,6 @@ class FeiShuSettingSerializer(serializers.Serializer): FEISHU_VERSION = serializers.ChoiceField( choices=VERSION_CHOICES, default='feishu', label=_('Version') ) + FEISHU_CREATE_USER_IF_NOT_EXIST = serializers.BooleanField( + default=False, label=_('Create user if not') + ) diff --git a/apps/settings/serializers/auth/wecom.py b/apps/settings/serializers/auth/wecom.py index 462b17db6..3e57683a6 100644 --- a/apps/settings/serializers/auth/wecom.py +++ b/apps/settings/serializers/auth/wecom.py @@ -13,3 +13,6 @@ class WeComSettingSerializer(serializers.Serializer): WECOM_AGENTID = serializers.CharField(max_length=256, required=True, label='agentid') WECOM_SECRET = EncryptedField(max_length=256, required=False, label='secret') AUTH_WECOM = serializers.BooleanField(default=False, label=_('Enable WeCom Auth')) + WECOM_CREATE_USER_IF_NOT_EXIST = serializers.BooleanField( + default=False, label=_('Create user if not') + ) diff --git a/apps/users/migrations/0040_alter_user_source.py b/apps/users/migrations/0040_alter_user_source.py index 56ad9befc..cb90c4971 100644 --- a/apps/users/migrations/0040_alter_user_source.py +++ b/apps/users/migrations/0040_alter_user_source.py @@ -13,6 +13,6 @@ class Migration(migrations.Migration): 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'), ('custom', 'Custom')], default='local', max_length=30, verbose_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'), ('custom', 'Custom')], default='local', max_length=30, verbose_name='Source'), ), ] diff --git a/apps/users/models/user.py b/apps/users/models/user.py index b487365e5..efe6ca570 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -677,14 +677,15 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): cas = 'cas', 'CAS' saml2 = 'saml2', 'SAML2' oauth2 = 'oauth2', 'OAuth2' + wecom = 'wecom', _('WeCom') + dingtalk = 'dingtalk', _('DingTalk') + feishu = 'feishu', _('FeiShu') custom = 'custom', 'Custom' SOURCE_BACKEND_MAPPING = { Source.local: [ settings.AUTH_BACKEND_MODEL, settings.AUTH_BACKEND_PUBKEY, - settings.AUTH_BACKEND_WECOM, - settings.AUTH_BACKEND_DINGTALK, ], Source.ldap: [ settings.AUTH_BACKEND_LDAP @@ -705,6 +706,15 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): Source.oauth2: [ settings.AUTH_BACKEND_OAUTH2 ], + Source.wecom: [ + settings.AUTH_BACKEND_WECOM + ], + Source.feishu: [ + settings.AUTH_BACKEND_FEISHU + ], + Source.dingtalk: [ + settings.AUTH_BACKEND_DINGTALK + ], Source.custom: [ settings.AUTH_BACKEND_CUSTOM ]