From 7151201d58c489f171ba5e281d4834c1d46f64ef Mon Sep 17 00:00:00 2001 From: "Jiangjie.Bai" Date: Wed, 24 Aug 2022 11:41:48 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E8=87=AA=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=E8=AE=A4=E8=AF=81=20backend=EF=BC=9B=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E5=85=B6=E4=BB=96=E8=AE=A4=E8=AF=81=E6=96=B9=E5=BC=8F?= =?UTF-8?q?=E7=9A=84=E4=BF=A1=E5=8F=B7=E8=A7=A6=E5=8F=91=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/backends/custom.py | 64 +++++++++++++++++++ .../backends/oauth2/backends.py | 14 ++-- .../authentication/backends/oauth2/signals.py | 2 - apps/authentication/backends/oidc/backends.py | 34 ++++++---- apps/authentication/backends/oidc/signals.py | 2 - .../authentication/backends/saml2/backends.py | 14 ++-- apps/authentication/backends/saml2/signals.py | 2 - apps/authentication/signal_handlers.py | 50 +++------------ apps/authentication/signals.py | 4 ++ apps/jumpserver/settings/auth.py | 5 +- .../migrations/0040_alter_user_source.py | 18 ++++++ apps/users/models/user.py | 4 ++ 12 files changed, 142 insertions(+), 71 deletions(-) create mode 100644 apps/authentication/backends/custom.py create mode 100644 apps/users/migrations/0040_alter_user_source.py diff --git a/apps/authentication/backends/custom.py b/apps/authentication/backends/custom.py new file mode 100644 index 000000000..4432e2919 --- /dev/null +++ b/apps/authentication/backends/custom.py @@ -0,0 +1,64 @@ +import os +from django.conf import settings +import inspect +from django.utils.module_loading import import_string +from common.utils import get_logger +from django.contrib.auth import get_user_model +from authentication.signals import user_auth_failed, user_auth_success + + +from .base import JMSModelBackend + +logger = get_logger(__file__) + + +class CustomAuthBackend(JMSModelBackend): + custom_auth_method_path = 'data.auth.main.authenticate' + + def is_enabled(self): + try: + self.load_authenticate_method() + except Exception as e: + logger.warning('Not enabled custom auth backend: {}'.format(e)) + return False + else: + logger.info('Enabled custom auth backend') + return True + + @staticmethod + def get_or_create_user_from_userinfo(userinfo: dict): + username = userinfo['username'] + attrs = ['name', 'username', 'email'] + defaults = {attr: userinfo[attr] for attr in attrs} + user, created = get_user_model().objects.get_or_create( + username=username, defaults=defaults + ) + return user, created + + def load_authenticate_method(self): + return import_string(self.custom_auth_method_path) + + def authenticate(self, request, username=None, password=None, **kwargs): + try: + authenticate = self.load_authenticate_method() + userinfo: dict = authenticate(username=username, password=password) + user, created = self.get_or_create_user_from_userinfo(userinfo) + except Exception as e: + logger.error('Custom authenticate error: {}'.format(e)) + return None + + if self.user_can_authenticate(user): + logger.info(f'Custom authenticate success: {user.username}') + user_auth_success.send( + sender=self.__class__, request=request, user=user, + backend=settings.AUTH_BACKEND_CUSTOM + ) + return user + else: + logger.info(f'Custom authenticate failed: {user.username}') + user_auth_failed.send( + sender=self.__class__, request=request, username=user.username, + reason=_('User invalid, disabled or expired'), + backend=settings.AUTH_BACKEND_CUSTOM + ) + return None diff --git a/apps/authentication/backends/oauth2/backends.py b/apps/authentication/backends/oauth2/backends.py index 755d5ef54..6469f121b 100644 --- a/apps/authentication/backends/oauth2/backends.py +++ b/apps/authentication/backends/oauth2/backends.py @@ -10,11 +10,11 @@ from django.urls import reverse from common.utils import get_logger from users.utils import construct_user_email from authentication.utils import build_absolute_uri +from authentication.signals import user_auth_failed, user_auth_success from common.exceptions import JMSException from .signals import ( - oauth2_create_or_update_user, oauth2_user_login_failed, - oauth2_user_login_success + oauth2_create_or_update_user ) from ..base import JMSModelBackend @@ -145,13 +145,17 @@ class OAuth2Backend(JMSModelBackend): if self.user_can_authenticate(user): logger.debug(log_prompt.format('OAuth2 user login success')) logger.debug(log_prompt.format('Send signal => oauth2 user login success')) - oauth2_user_login_success.send(sender=self.__class__, request=request, user=user) + user_auth_success.send( + sender=self.__class__, request=request, user=user, + backend=settings.AUTH_BACKEND_OAUTH2 + ) return user else: logger.debug(log_prompt.format('OAuth2 user login failed')) logger.debug(log_prompt.format('Send signal => oauth2 user login failed')) - oauth2_user_login_failed.send( + user_auth_failed.send( sender=self.__class__, request=request, username=user.username, - reason=_('User invalid, disabled or expired') + reason=_('User invalid, disabled or expired'), + backend=settings.AUTH_BACKEND_OAUTH2 ) return None diff --git a/apps/authentication/backends/oauth2/signals.py b/apps/authentication/backends/oauth2/signals.py index 50c7837f8..b82ae90d7 100644 --- a/apps/authentication/backends/oauth2/signals.py +++ b/apps/authentication/backends/oauth2/signals.py @@ -4,6 +4,4 @@ from django.dispatch import Signal oauth2_create_or_update_user = Signal( providing_args=['request', 'user', 'created', 'name', 'username', 'email'] ) -oauth2_user_login_success = Signal(providing_args=['request', 'user']) -oauth2_user_login_failed = Signal(providing_args=['request', 'username', 'reason']) diff --git a/apps/authentication/backends/oidc/backends.py b/apps/authentication/backends/oidc/backends.py index 03b71334f..70e0f759c 100644 --- a/apps/authentication/backends/oidc/backends.py +++ b/apps/authentication/backends/oidc/backends.py @@ -26,8 +26,9 @@ from ..base import JMSBaseAuthBackend from .utils import validate_and_return_id_token from .decorator import ssl_verification from .signals import ( - openid_create_or_update_user, openid_user_login_failed, openid_user_login_success + openid_create_or_update_user ) +from authentication.signals import user_auth_success, user_auth_failed logger = get_logger(__file__) @@ -213,14 +214,18 @@ class OIDCAuthCodeBackend(OIDCBaseBackend): if self.user_can_authenticate(user): logger.debug(log_prompt.format('OpenID user login success')) logger.debug(log_prompt.format('Send signal => openid user login success')) - openid_user_login_success.send(sender=self.__class__, request=request, user=user) + user_auth_success.send( + sender=self.__class__, request=request, user=user, + backend=settings.AUTH_BACKEND_OIDC_CODE + ) return user else: logger.debug(log_prompt.format('OpenID user login failed')) logger.debug(log_prompt.format('Send signal => openid user login failed')) - openid_user_login_failed.send( + user_auth_failed.send( sender=self.__class__, request=request, username=user.username, - reason="User is invalid" + reason="User is invalid", backend=settings.AUTH_BACKEND_OIDC_CODE + ) return None @@ -271,8 +276,9 @@ class OIDCAuthPasswordBackend(OIDCBaseBackend): "content is: {}, error is: {}".format(token_response.content, str(e)) logger.debug(log_prompt.format(error)) logger.debug(log_prompt.format('Send signal => openid user login failed')) - openid_user_login_failed.send( - sender=self.__class__, request=request, username=username, reason=error + user_auth_failed.send( + sender=self.__class__, request=request, username=username, reason=error, + backend=settings.AUTH_BACKEND_OIDC_PASSWORD ) return @@ -299,8 +305,9 @@ class OIDCAuthPasswordBackend(OIDCBaseBackend): "content is: {}, error is: {}".format(claims_response.content, str(e)) logger.debug(log_prompt.format(error)) logger.debug(log_prompt.format('Send signal => openid user login failed')) - openid_user_login_failed.send( - sender=self.__class__, request=request, username=username, reason=error + user_auth_failed.send( + sender=self.__class__, request=request, username=username, reason=error, + backend=settings.AUTH_BACKEND_OIDC_PASSWORD ) return @@ -312,13 +319,16 @@ class OIDCAuthPasswordBackend(OIDCBaseBackend): if self.user_can_authenticate(user): logger.debug(log_prompt.format('OpenID user login success')) logger.debug(log_prompt.format('Send signal => openid user login success')) - openid_user_login_success.send( - sender=self.__class__, request=request, user=user + user_auth_success.send( + sender=self.__class__, request=request, user=user, + backend=settings.AUTH_BACKEND_OIDC_PASSWORD ) return user else: logger.debug(log_prompt.format('OpenID user login failed')) logger.debug(log_prompt.format('Send signal => openid user login failed')) - openid_user_login_failed.send( - sender=self.__class__, request=request, username=username, reason="User is invalid" + user_auth_failed.send( + sender=self.__class__, request=request, username=username, reason="User is invalid", + backend=settings.AUTH_BACKEND_OIDC_PASSWORD ) + return None diff --git a/apps/authentication/backends/oidc/signals.py b/apps/authentication/backends/oidc/signals.py index 85d2dcd94..2f4c071e4 100644 --- a/apps/authentication/backends/oidc/signals.py +++ b/apps/authentication/backends/oidc/signals.py @@ -13,6 +13,4 @@ from django.dispatch import Signal openid_create_or_update_user = Signal( providing_args=['request', 'user', 'created', 'name', 'username', 'email'] ) -openid_user_login_success = Signal(providing_args=['request', 'user']) -openid_user_login_failed = Signal(providing_args=['request', 'username', 'reason']) diff --git a/apps/authentication/backends/saml2/backends.py b/apps/authentication/backends/saml2/backends.py index 70667cd94..570557c1d 100644 --- a/apps/authentication/backends/saml2/backends.py +++ b/apps/authentication/backends/saml2/backends.py @@ -7,9 +7,9 @@ from django.db import transaction from common.utils import get_logger from authentication.errors import reason_choices, reason_user_invalid from .signals import ( - saml2_user_authenticated, saml2_user_authentication_failed, saml2_create_or_update_user ) +from authentication.signals import user_auth_failed, user_auth_success from ..base import JMSModelBackend __all__ = ['SAML2Backend'] @@ -55,14 +55,16 @@ class SAML2Backend(JMSModelBackend): if self.user_can_authenticate(user): logger.debug(log_prompt.format('SAML2 user login success')) - saml2_user_authenticated.send( - sender=self, request=request, user=user, created=created + user_auth_success.send( + sender=self.__class__, request=request, user=user, created=created, + backend=settings.AUTH_BACKEND_SAML2 ) return user else: logger.debug(log_prompt.format('SAML2 user login failed')) - saml2_user_authentication_failed.send( - sender=self, request=request, username=username, - reason=reason_choices.get(reason_user_invalid) + user_auth_failed.send( + sender=self.__class__, request=request, username=username, + reason=reason_choices.get(reason_user_invalid), + backend=settings.AUTH_BACKEND_SAML2 ) return None diff --git a/apps/authentication/backends/saml2/signals.py b/apps/authentication/backends/saml2/signals.py index 42252f4d0..3dcdd9d35 100644 --- a/apps/authentication/backends/saml2/signals.py +++ b/apps/authentication/backends/saml2/signals.py @@ -2,5 +2,3 @@ from django.dispatch import Signal saml2_create_or_update_user = Signal(providing_args=('user', 'created', 'request', 'attrs')) -saml2_user_authenticated = Signal(providing_args=('user', 'created', 'request')) -saml2_user_authentication_failed = Signal(providing_args=('request', 'username', 'reason')) diff --git a/apps/authentication/signal_handlers.py b/apps/authentication/signal_handlers.py index e7be3465c..b1f55f689 100644 --- a/apps/authentication/signal_handlers.py +++ b/apps/authentication/signal_handlers.py @@ -7,16 +7,7 @@ from django.dispatch import receiver from django_cas_ng.signals import cas_user_authenticated from apps.jumpserver.settings.auth import AUTHENTICATION_BACKENDS_THIRD_PARTY -from authentication.backends.oidc.signals import ( - openid_user_login_failed, openid_user_login_success -) -from authentication.backends.saml2.signals import ( - saml2_user_authenticated, saml2_user_authentication_failed -) -from authentication.backends.oauth2.signals import ( - oauth2_user_login_failed, oauth2_user_login_success -) -from .signals import post_auth_success, post_auth_failed +from .signals import post_auth_success, post_auth_failed, user_auth_failed, user_auth_success @receiver(user_logged_in) @@ -29,7 +20,8 @@ def on_user_auth_login_success(sender, user, request, **kwargs): and user.mfa_enabled \ and not request.session.get('auth_mfa'): request.session['auth_mfa_required'] = 1 - if not request.session.get("auth_third_party_done") and request.session.get('auth_backend') in AUTHENTICATION_BACKENDS_THIRD_PARTY: + if not request.session.get("auth_third_party_done") and \ + request.session.get('auth_backend') in AUTHENTICATION_BACKENDS_THIRD_PARTY: request.session['auth_third_party_required'] = 1 # 单点登录,超过了自动退出 if settings.USER_LOGIN_SINGLE_MACHINE_ENABLED: @@ -44,43 +36,19 @@ def on_user_auth_login_success(sender, user, request, **kwargs): request.session['auth_session_expiration_required'] = 1 -@receiver(openid_user_login_success) -def on_oidc_user_login_success(sender, request, user, create=False, **kwargs): - request.session['auth_backend'] = settings.AUTH_BACKEND_OIDC_CODE - post_auth_success.send(sender, user=user, request=request) - - -@receiver(openid_user_login_failed) -def on_oidc_user_login_failed(sender, username, request, reason, **kwargs): - request.session['auth_backend'] = settings.AUTH_BACKEND_OIDC_CODE - post_auth_failed.send(sender, username=username, request=request, reason=reason) - - @receiver(cas_user_authenticated) def on_cas_user_login_success(sender, request, user, **kwargs): request.session['auth_backend'] = settings.AUTH_BACKEND_CAS post_auth_success.send(sender, user=user, request=request) -@receiver(saml2_user_authenticated) -def on_saml2_user_login_success(sender, request, user, **kwargs): - request.session['auth_backend'] = settings.AUTH_BACKEND_SAML2 +@receiver(user_auth_success) +def on_user_login_success(sender, request, user, backend, create=False, **kwargs): + request.session['auth_backend'] = backend post_auth_success.send(sender, user=user, request=request) -@receiver(saml2_user_authentication_failed) -def on_saml2_user_login_failed(sender, request, username, reason, **kwargs): - request.session['auth_backend'] = settings.AUTH_BACKEND_SAML2 - post_auth_failed.send(sender, username=username, request=request, reason=reason) - - -@receiver(oauth2_user_login_success) -def on_oauth2_user_login_success(sender, request, user, **kwargs): - request.session['auth_backend'] = settings.AUTH_BACKEND_OAUTH2 - post_auth_success.send(sender, user=user, request=request) - - -@receiver(oauth2_user_login_failed) -def on_oauth2_user_login_failed(sender, username, request, reason, **kwargs): - request.session['auth_backend'] = settings.AUTH_BACKEND_OAUTH2 +@receiver(user_auth_failed) +def on_user_login_failed(sender, username, request, reason, backend, **kwargs): + request.session['auth_backend'] = backend post_auth_failed.send(sender, username=username, request=request, reason=reason) diff --git a/apps/authentication/signals.py b/apps/authentication/signals.py index 0a305290c..556bba614 100644 --- a/apps/authentication/signals.py +++ b/apps/authentication/signals.py @@ -3,3 +3,7 @@ from django.dispatch import Signal post_auth_success = Signal(providing_args=('user', 'request')) post_auth_failed = Signal(providing_args=('username', 'request', 'reason')) + + +user_auth_success = Signal(providing_args=('user', 'request', 'backend', 'create')) +user_auth_failed = Signal(providing_args=('username', 'request', 'reason', 'backend')) diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index de43b03be..4e1e63ad8 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -195,6 +195,7 @@ AUTH_BACKEND_AUTH_TOKEN = 'authentication.backends.sso.AuthorizationTokenAuthent AUTH_BACKEND_SAML2 = 'authentication.backends.saml2.SAML2Backend' AUTH_BACKEND_OAUTH2 = 'authentication.backends.oauth2.OAuth2Backend' AUTH_BACKEND_TEMP_TOKEN = 'authentication.backends.token.TempTokenAuthBackend' +AUTH_BACKEND_CUSTOM = 'authentication.backends.custom.CustomAuthBackend' AUTHENTICATION_BACKENDS = [ @@ -208,7 +209,9 @@ AUTHENTICATION_BACKENDS = [ # 扫码模式 AUTH_BACKEND_WECOM, AUTH_BACKEND_DINGTALK, AUTH_BACKEND_FEISHU, # 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_CUSTOM ] AUTHENTICATION_BACKENDS_THIRD_PARTY = [AUTH_BACKEND_OIDC_CODE, AUTH_BACKEND_CAS, AUTH_BACKEND_SAML2, AUTH_BACKEND_OAUTH2] diff --git a/apps/users/migrations/0040_alter_user_source.py b/apps/users/migrations/0040_alter_user_source.py new file mode 100644 index 000000000..7d798e5c4 --- /dev/null +++ b/apps/users/migrations/0040_alter_user_source.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.13 on 2022-08-24 02:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0039_auto_20211229_1852'), + ] + + operations = [ + 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'), + ), + ] diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 78d5eb540..6830c2ac5 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -629,6 +629,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): cas = 'cas', 'CAS' saml2 = 'saml2', 'SAML2' oauth2 = 'oauth2', 'OAuth2' + custom = 'custom', 'Custom' SOURCE_BACKEND_MAPPING = { Source.local: [ @@ -656,6 +657,9 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): Source.oauth2: [ settings.AUTH_BACKEND_OAUTH2 ], + Source.custom: [ + settings.AUTH_BACKEND_CUSTOM + ] } id = models.UUIDField(default=uuid.uuid4, primary_key=True)