From bd3909ad27b5cbff63d35ec7ac17b31ea731bf06 Mon Sep 17 00:00:00 2001 From: feng <1304903146@qq.com> Date: Thu, 1 Aug 2024 18:36:01 +0800 Subject: [PATCH] perf: Third-party user login settings default organization --- .../backends/radius/__init__.py | 1 + .../{radius.py => radius/backends.py} | 8 +-- .../authentication/backends/radius/signals.py | 3 + apps/authentication/views/base.py | 3 +- apps/jumpserver/conf.py | 14 +++- apps/settings/serializers/auth/base.py | 15 +++++ apps/settings/serializers/auth/cas.py | 7 +- apps/settings/serializers/auth/dingtalk.py | 2 + apps/settings/serializers/auth/feishu.py | 3 + apps/settings/serializers/auth/lark.py | 2 + apps/settings/serializers/auth/ldap.py | 5 +- apps/settings/serializers/auth/oauth2.py | 2 + apps/settings/serializers/auth/oidc.py | 6 +- apps/settings/serializers/auth/radius.py | 4 +- apps/settings/serializers/auth/saml2.py | 3 + apps/settings/serializers/auth/slack.py | 2 + apps/settings/serializers/auth/wecom.py | 2 + apps/users/models/user/_role.py | 1 + apps/users/signal_handlers.py | 67 ++++++++++++++----- 19 files changed, 120 insertions(+), 30 deletions(-) create mode 100644 apps/authentication/backends/radius/__init__.py rename apps/authentication/backends/{radius.py => radius/backends.py} (91%) create mode 100644 apps/authentication/backends/radius/signals.py diff --git a/apps/authentication/backends/radius/__init__.py b/apps/authentication/backends/radius/__init__.py new file mode 100644 index 000000000..a0957e5c9 --- /dev/null +++ b/apps/authentication/backends/radius/__init__.py @@ -0,0 +1 @@ +from .backends import * diff --git a/apps/authentication/backends/radius.py b/apps/authentication/backends/radius/backends.py similarity index 91% rename from apps/authentication/backends/radius.py rename to apps/authentication/backends/radius/backends.py index 84f88165a..148e9bac2 100644 --- a/apps/authentication/backends/radius.py +++ b/apps/authentication/backends/radius/backends.py @@ -2,12 +2,12 @@ # import traceback +from django.conf import settings from django.contrib.auth import get_user_model from radiusauth.backends import RADIUSBackend, RADIUSRealmBackend -from django.conf import settings - -from .base import JMSBaseAuthBackend +from authentication.backends.base import JMSBaseAuthBackend +from .signals import radius_create_user User = get_user_model() @@ -28,8 +28,8 @@ class CreateUserMixin: email = '{}@{}'.format(username, email_suffix) user = User(username=username, name=username, email=email) - user.source = user.Source.radius.value user.save() + radius_create_user.send(sender=user.__class__, user=user) return user def _perform_radius_auth(self, client, packet): diff --git a/apps/authentication/backends/radius/signals.py b/apps/authentication/backends/radius/signals.py new file mode 100644 index 000000000..9e62b557d --- /dev/null +++ b/apps/authentication/backends/radius/signals.py @@ -0,0 +1,3 @@ +from django.dispatch import Signal + +radius_create_user = Signal() diff --git a/apps/authentication/views/base.py b/apps/authentication/views/base.py index 7e4cf8692..80878d641 100644 --- a/apps/authentication/views/base.py +++ b/apps/authentication/views/base.py @@ -15,7 +15,7 @@ from common.utils import get_logger from common.utils.common import get_request_ip from common.utils.django import reverse, get_object_or_none from users.models import User -from users.signal_handlers import check_only_allow_exist_user_auth +from users.signal_handlers import check_only_allow_exist_user_auth, bind_user_to_org_role from .mixins import FlashMessageMixin logger = get_logger(__file__) @@ -64,6 +64,7 @@ class BaseLoginCallbackView(AuthMixin, FlashMessageMixin, IMClientMixin, View): setattr(user, f'{self.user_type}_id', user_id) if create: setattr(user, 'source', self.user_type) + bind_user_to_org_role(user) user.save() except IntegrityError as err: logger.error(f'{self.msg_client_err}: create user error: {err}') diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 1a95fb531..d44fb8bbe 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -28,6 +28,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_DIR = os.path.dirname(BASE_DIR) XPACK_DIR = os.path.join(BASE_DIR, 'xpack') HAS_XPACK = os.path.isdir(XPACK_DIR) +DEFAULT_ID = '00000000-0000-0000-0000-000000000002' logger = logging.getLogger('jumpserver.conf') @@ -282,7 +283,7 @@ class Config(dict): 'AUTH_LDAP_SYNC_IS_PERIODIC': False, 'AUTH_LDAP_SYNC_INTERVAL': None, 'AUTH_LDAP_SYNC_CRONTAB': None, - 'AUTH_LDAP_SYNC_ORG_IDS': ['00000000-0000-0000-0000-000000000002'], + 'AUTH_LDAP_SYNC_ORG_IDS': [DEFAULT_ID], 'AUTH_LDAP_SYNC_RECEIVERS': [], 'AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS': False, 'AUTH_LDAP_OPTIONS_OPT_REFERRALS': -1, @@ -323,6 +324,7 @@ class Config(dict): 'AUTH_OPENID_KEYCLOAK': True, 'AUTH_OPENID_SERVER_URL': 'https://keycloak.example.com', 'AUTH_OPENID_REALM_NAME': None, + 'OPENID_ORG_IDS': [DEFAULT_ID], # Raidus 认证 'AUTH_RADIUS': False, @@ -332,6 +334,7 @@ class Config(dict): 'RADIUS_ATTRIBUTES': {}, 'RADIUS_ENCRYPT_PASSWORD': True, 'OTP_IN_RADIUS': False, + 'RADIUS_ORG_IDS': [DEFAULT_ID], # Cas 认证 'AUTH_CAS': False, @@ -343,6 +346,7 @@ class Config(dict): 'CAS_APPLY_ATTRIBUTES_TO_USER': False, 'CAS_RENAME_ATTRIBUTES': {'cas:user': 'username'}, 'CAS_CREATE_USER': True, + 'CAS_ORG_IDS': [DEFAULT_ID], 'AUTH_SSO': False, 'AUTH_SSO_AUTHKEY_TTL': 60 * 15, @@ -370,6 +374,7 @@ class Config(dict): 'SAML2_SP_CERT_CONTENT': '', 'AUTH_SAML2_PROVIDER_AUTHORIZATION_ENDPOINT': '/', 'AUTH_SAML2_AUTHENTICATION_FAILURE_REDIRECT_URI': '/', + 'SAML2_ORG_IDS': [DEFAULT_ID], # OAuth2 认证 'AUTH_OAUTH2': False, @@ -388,6 +393,8 @@ class Config(dict): 'AUTH_OAUTH2_USER_ATTR_MAP': { 'name': 'name', 'username': 'username', 'email': 'email' }, + 'OAUTH2_ORG_IDS': [DEFAULT_ID], + 'AUTH_PASSKEY': False, 'FIDO_SERVER_ID': '', 'FIDO_SERVER_NAME': 'JumpServer', @@ -402,6 +409,7 @@ class Config(dict): 'username': 'userid', 'email': 'email' }, + 'WECOM_ORG_IDS': [DEFAULT_ID], # 钉钉 'AUTH_DINGTALK': False, @@ -413,6 +421,7 @@ class Config(dict): 'username': 'user_id', 'email': 'email' }, + 'DINGTALK_ORG_IDS': [DEFAULT_ID], # 飞书 'AUTH_FEISHU': False, @@ -423,6 +432,7 @@ class Config(dict): 'username': 'user_id', 'email': 'enterprise_email' }, + 'FEISHU_ORG_IDS': [DEFAULT_ID], # Lark 'AUTH_LARK': False, @@ -433,6 +443,7 @@ class Config(dict): 'username': 'user_id', 'email': 'enterprise_email' }, + 'LARK_ORG_IDS': [DEFAULT_ID], # Slack 'AUTH_SLACK': False, @@ -444,6 +455,7 @@ class Config(dict): 'username': 'name', 'email': 'profile.email' }, + 'SLACK_ORG_IDS': [DEFAULT_ID], 'LOGIN_REDIRECT_TO_BACKEND': '', # 'OPENID / CAS / SAML2 'LOGIN_REDIRECT_MSG_ENABLED': True, diff --git a/apps/settings/serializers/auth/base.py b/apps/settings/serializers/auth/base.py index 4b7409f08..abfbf79b4 100644 --- a/apps/settings/serializers/auth/base.py +++ b/apps/settings/serializers/auth/base.py @@ -3,6 +3,7 @@ from rest_framework import serializers __all__ = [ 'AuthSettingSerializer', + 'OrgListField' ] @@ -42,3 +43,17 @@ class AuthSettingSerializer(serializers.Serializer): "authentication when the administrator enables third-party redirect authentication" ) ) + + +class OrgListField(serializers.ListField): + def __init__(self, **kwargs): + defaults = { + 'required': False, + 'label': _('Organization'), + 'help_text': _( + 'When you create a user, you associate the user to the organization of your choice. ' + 'Users always belong to the Default organization.' + ) + } + defaults.update(kwargs) + super().__init__(**defaults) diff --git a/apps/settings/serializers/auth/cas.py b/apps/settings/serializers/auth/cas.py index 73b3f4736..2a9e44377 100644 --- a/apps/settings/serializers/auth/cas.py +++ b/apps/settings/serializers/auth/cas.py @@ -1,6 +1,8 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers +from .base import OrgListField + __all__ = [ 'CASSettingSerializer', ] @@ -16,7 +18,7 @@ class CASSettingSerializer(serializers.Serializer): max_length=1024, label=_('Proxy Server') ) CAS_LOGOUT_COMPLETELY = serializers.BooleanField( - required=False, label=_('Logout completely'), + required=False, label=_('Logout completely'), help_text=_('When the user signs out, they also be logged out from the CAS server') ) CAS_VERSION = serializers.IntegerField( @@ -36,9 +38,10 @@ class CASSettingSerializer(serializers.Serializer): ) ) CAS_CREATE_USER = serializers.BooleanField( - required=False, label=_('Create user'), + required=False, label=_('Create user'), help_text=_( 'After successful user authentication, if the user does not exist, ' 'automatically create the user' ) ) + CAS_ORG_IDS = OrgListField() \ No newline at end of file diff --git a/apps/settings/serializers/auth/dingtalk.py b/apps/settings/serializers/auth/dingtalk.py index 0c6a57e51..c5a30572c 100644 --- a/apps/settings/serializers/auth/dingtalk.py +++ b/apps/settings/serializers/auth/dingtalk.py @@ -2,6 +2,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from common.serializers.fields import EncryptedField +from .base import OrgListField __all__ = ['DingTalkSettingSerializer'] @@ -20,3 +21,4 @@ class DingTalkSettingSerializer(serializers.Serializer): '`value` is the DingTalk service user attribute name' ) ) + DINGTALK_ORG_IDS = OrgListField() diff --git a/apps/settings/serializers/auth/feishu.py b/apps/settings/serializers/auth/feishu.py index e457055c7..5d06e5b22 100644 --- a/apps/settings/serializers/auth/feishu.py +++ b/apps/settings/serializers/auth/feishu.py @@ -5,6 +5,8 @@ from common.serializers.fields import EncryptedField __all__ = ['FeiShuSettingSerializer'] +from .base import OrgListField + class FeiShuSettingSerializer(serializers.Serializer): PREFIX_TITLE = _('FeiShu') @@ -19,3 +21,4 @@ class FeiShuSettingSerializer(serializers.Serializer): '`value` is the FeiShu service user attribute name' ) ) + FEISHU_ORG_IDS = OrgListField() diff --git a/apps/settings/serializers/auth/lark.py b/apps/settings/serializers/auth/lark.py index df12e2c0c..7d6e58c20 100644 --- a/apps/settings/serializers/auth/lark.py +++ b/apps/settings/serializers/auth/lark.py @@ -2,6 +2,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from common.serializers.fields import EncryptedField +from .base import OrgListField __all__ = ['LarkSettingSerializer'] @@ -19,3 +20,4 @@ class LarkSettingSerializer(serializers.Serializer): '`value` is the Lark service user attribute name' ) ) + LARK_ORG_IDS = OrgListField() diff --git a/apps/settings/serializers/auth/ldap.py b/apps/settings/serializers/auth/ldap.py index 6952f074b..e0bd51389 100644 --- a/apps/settings/serializers/auth/ldap.py +++ b/apps/settings/serializers/auth/ldap.py @@ -2,6 +2,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from common.serializers.fields import EncryptedField +from .base import OrgListField __all__ = [ 'LDAPTestConfigSerializer', 'LDAPUserSerializer', 'LDAPTestLoginSerializer', @@ -67,9 +68,6 @@ class LDAPSettingSerializer(serializers.Serializer): '`value` is the LDAP service user attribute name' ) ) - AUTH_LDAP_SYNC_ORG_IDS = serializers.ListField( - required=False, label=_('Organization'), max_length=36 - ) AUTH_LDAP_SYNC_IS_PERIODIC = serializers.BooleanField( required=False, label=_('Periodic run') ) @@ -102,6 +100,7 @@ class LDAPSettingSerializer(serializers.Serializer): ) AUTH_LDAP = serializers.BooleanField(required=False, label=_('LDAP')) + AUTH_LDAP_SYNC_ORG_IDS = OrgListField() def post_save(self): keys = ['AUTH_LDAP_SYNC_IS_PERIODIC', 'AUTH_LDAP_SYNC_INTERVAL', 'AUTH_LDAP_SYNC_CRONTAB'] diff --git a/apps/settings/serializers/auth/oauth2.py b/apps/settings/serializers/auth/oauth2.py index 997bfc4a3..6d23e742f 100644 --- a/apps/settings/serializers/auth/oauth2.py +++ b/apps/settings/serializers/auth/oauth2.py @@ -3,6 +3,7 @@ from rest_framework import serializers from common.serializers.fields import EncryptedField from common.utils import static_or_direct +from .base import OrgListField __all__ = [ 'OAuth2SettingSerializer', @@ -65,3 +66,4 @@ class OAuth2SettingSerializer(serializers.Serializer): AUTH_OAUTH2_ALWAYS_UPDATE_USER = serializers.BooleanField( default=True, label=_('Always update user') ) + OAUTH2_ORG_IDS = OrgListField() diff --git a/apps/settings/serializers/auth/oidc.py b/apps/settings/serializers/auth/oidc.py index 2f974286e..31c2790a3 100644 --- a/apps/settings/serializers/auth/oidc.py +++ b/apps/settings/serializers/auth/oidc.py @@ -2,6 +2,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from common.serializers.fields import EncryptedField +from .base import OrgListField __all__ = [ 'OIDCSettingSerializer', 'KeycloakSettingSerializer', @@ -13,7 +14,7 @@ class CommonSettingSerializer(serializers.Serializer): # OpenID 公有配置参数 (version <= 1.5.8 或 version >= 1.5.8) BASE_SITE_URL = serializers.CharField( required=False, allow_null=True, allow_blank=True, - max_length=1024, label=_('Base site URL'), + max_length=1024, label=_('Base site URL'), help_text=_("The current site's URL is used to construct the callback address") ) AUTH_OPENID_CLIENT_ID = serializers.CharField( @@ -107,7 +108,8 @@ class OIDCSettingSerializer(KeycloakSettingSerializer): ) AUTH_OPENID_USE_NONCE = serializers.BooleanField( required=False, label=_('Use nonce') - ) + ) AUTH_OPENID_ALWAYS_UPDATE_USER = serializers.BooleanField( required=False, label=_('Always update user') ) + OPENID_ORG_IDS = OrgListField() diff --git a/apps/settings/serializers/auth/radius.py b/apps/settings/serializers/auth/radius.py index 56746f65a..ec6f8fd32 100644 --- a/apps/settings/serializers/auth/radius.py +++ b/apps/settings/serializers/auth/radius.py @@ -5,6 +5,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from common.serializers.fields import EncryptedField +from .base import OrgListField __all__ = ['RadiusSettingSerializer'] @@ -19,6 +20,7 @@ class RadiusSettingSerializer(serializers.Serializer): required=False, max_length=1024, allow_null=True, label=_('Secret'), ) OTP_IN_RADIUS = serializers.BooleanField( - required=False, label=_('OTP in RADIUS'), + required=False, label=_('OTP in RADIUS'), help_text=_('* Using OTP in RADIUS means users can employ RADIUS as a method for MFA') ) + RADIUS_ORG_IDS = OrgListField() diff --git a/apps/settings/serializers/auth/saml2.py b/apps/settings/serializers/auth/saml2.py index b54118b16..46f185916 100644 --- a/apps/settings/serializers/auth/saml2.py +++ b/apps/settings/serializers/auth/saml2.py @@ -1,6 +1,8 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers +from .base import OrgListField + __all__ = [ 'SAML2SettingSerializer', ] @@ -41,3 +43,4 @@ class SAML2SettingSerializer(serializers.Serializer): help_text=_('When the user signs out, they also be logged out from the SAML2 server') ) AUTH_SAML2_ALWAYS_UPDATE_USER = serializers.BooleanField(required=False, label=_('Always update user')) + SAML2_ORG_IDS = OrgListField() diff --git a/apps/settings/serializers/auth/slack.py b/apps/settings/serializers/auth/slack.py index c67cb16ea..f3bf5ad5e 100644 --- a/apps/settings/serializers/auth/slack.py +++ b/apps/settings/serializers/auth/slack.py @@ -2,6 +2,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from common.serializers.fields import EncryptedField +from .base import OrgListField __all__ = ['SlackSettingSerializer'] @@ -20,3 +21,4 @@ class SlackSettingSerializer(serializers.Serializer): '`value` is the Slack service user attribute name' ) ) + SLACK_ORG_IDS = OrgListField() diff --git a/apps/settings/serializers/auth/wecom.py b/apps/settings/serializers/auth/wecom.py index 3e382d16d..b398721f6 100644 --- a/apps/settings/serializers/auth/wecom.py +++ b/apps/settings/serializers/auth/wecom.py @@ -2,6 +2,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from common.serializers.fields import EncryptedField +from .base import OrgListField __all__ = ['WeComSettingSerializer'] @@ -20,3 +21,4 @@ class WeComSettingSerializer(serializers.Serializer): '`value` is the WeCom service user attribute name' ) ) + WECOM_ORG_IDS = OrgListField() diff --git a/apps/users/models/user/_role.py b/apps/users/models/user/_role.py index af7a49e57..f2f654388 100644 --- a/apps/users/models/user/_role.py +++ b/apps/users/models/user/_role.py @@ -183,6 +183,7 @@ class RoleMixin: is_authenticated: bool is_valid: bool id: str + source: str _org_roles = None _system_roles = None PERM_CACHE_KEY = "USER_PERMS_ROLES_{}_{}" diff --git a/apps/users/signal_handlers.py b/apps/users/signal_handlers.py index 250064182..5bc116adf 100644 --- a/apps/users/signal_handlers.py +++ b/apps/users/signal_handlers.py @@ -12,6 +12,7 @@ from django_cas_ng.signals import cas_user_authenticated from audits.models import UserSession from authentication.backends.oauth2.signals import oauth2_create_or_update_user from authentication.backends.oidc.signals import openid_create_or_update_user +from authentication.backends.radius.signals import radius_create_user from authentication.backends.saml2.signals import saml2_create_or_update_user from common.const.crontab import CRONTAB_AT_AM_TWO from common.decorators import on_transaction_commit @@ -20,6 +21,9 @@ from common.signals import django_ready from common.utils import get_logger from jumpserver.utils import get_current_request from ops.celery.decorator import register_as_period_task +from rbac.builtin import BuiltinRole +from rbac.const import Scope +from rbac.models import RoleBinding from settings.signals import setting_changed from .models import User, UserPasswordHistory from .signals import post_user_create @@ -40,12 +44,13 @@ def check_only_allow_exist_user_auth(created): def user_authenticated_handle(user, created, source, attrs=None, **kwargs): + if not check_only_allow_exist_user_auth(created): + return + if created: user.source = source user.save() - - if not check_only_allow_exist_user_auth(created): - return + bind_user_to_org_role(user) if not attrs: return @@ -71,9 +76,9 @@ def save_passwd_change(sender, instance: User, **kwargs): return passwords = UserPasswordHistory.objects \ - .filter(user=instance) \ - .order_by('-date_created') \ - .values_list('password', flat=True)[:settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT] + .filter(user=instance) \ + .order_by('-date_created') \ + .values_list('password', flat=True)[:settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT] if instance.password not in list(passwords): UserPasswordHistory.objects.create( @@ -133,17 +138,18 @@ def on_oauth2_create_or_update_user(sender, user, created, attrs, **kwargs): user_authenticated_handle(user, created, source, attrs, **kwargs) -@receiver(populate_user) -def on_ldap_create_user(sender, user, ldap_user, **kwargs): - if user and user.username not in ['admin']: - exists = User.objects.filter(username=user.username).exists() - if not exists: - user.source = user.Source.ldap.value - user.save() +@receiver(radius_create_user) +def radius_create_user(sender, user, **kwargs): + user.source = user.Source.radius.value + user.save() + bind_user_to_org_role(user) @receiver(openid_create_or_update_user) def on_openid_create_or_update_user(sender, request, user, created, name, username, email, **kwargs): + if not check_only_allow_exist_user_auth(created): + return + if created: logger.debug( "Receive OpenID user created signal: {}, " @@ -151,9 +157,7 @@ def on_openid_create_or_update_user(sender, request, user, created, name, userna ) user.source = User.Source.openid.value user.save() - - if not check_only_allow_exist_user_auth(created): - return + bind_user_to_org_role(user) if not created and settings.AUTH_OPENID_ALWAYS_UPDATE_USER: logger.debug( @@ -167,6 +171,15 @@ def on_openid_create_or_update_user(sender, request, user, created, name, userna user.save() +@receiver(populate_user) +def on_ldap_create_user(sender, user, ldap_user, **kwargs): + if user and user.username not in ['admin']: + exists = User.objects.filter(username=user.username).exists() + if not exists: + user.source = user.Source.ldap.value + user.save() + + @shared_task(verbose_name=_('Clean up expired user sessions')) @register_as_period_task(crontab=CRONTAB_AT_AM_TWO) def clean_expired_user_session_period(): @@ -190,3 +203,25 @@ def on_auth_setting_changed_clear_source_choice(sender, name='', **kwargs): @receiver(django_ready) def on_django_ready_refresh_source(sender, **kwargs): User._source_choices = [] + + +def bind_user_to_org_role(user): + source = user.source.upper() + org_ids = getattr(settings, f"{source}_ORG_IDS", None) + + if not org_ids: + logger.error(f"User {user} has no {source} orgs") + return + + org_role_ids = [BuiltinRole.org_user.id] + + bindings = [ + RoleBinding( + user=user, org_id=org_id, scope=Scope.org, + role_id=role_id, + ) + for role_id in org_role_ids + for org_id in org_ids + ] + + RoleBinding.objects.bulk_create(bindings, ignore_conflicts=True)