mirror of https://github.com/jumpserver/jumpserver
				
				
				
			Fix rbac (#7713)
* fix: token 系统用户增加 protocol * fix: 修复清除orphan session时同时清除对应的 session_task * perf: 修改 connection token api * fix: 修复无法获取系统角色绑定的问题 * perf: 增加 db terminal 及 magnus 组件 * perf: 修改 migrations * fix: 修复AUTHENTICATION_BACKENDS相关的逻辑 * fix: 修改判断backend认证逻辑 * fix: 修复资产账号查看密码跳过mfa * fix: 修复用户组授权权限错误 * feat: 支持COS对象存储 * feat: 升级依赖 jms_storage==0.0.42 * fix: 修复 koko api 问题 * feat: 修改存储翻译信息 * perf: 修改 ticket 权限 * fix: 修复获取资产授权系统用户 get_queryset * perf: 抽取 ticket * perf: 修改 cmd filter 的权限 * fix: 修改 ticket perm * fix: 修复oidc依赖问题 Co-authored-by: Eric <xplzv@126.com> Co-authored-by: ibuler <ibuler@qq.com> Co-authored-by: 小冯 <xiaofeng@xiaofengdeMacBook-Pro.local> Co-authored-by: feng626 <1304903146@qq.com>pull/7714/head^2
							parent
							
								
									edfca5eb24
								
							
						
					
					
						commit
						03afa4f974
					
				|  | @ -13,6 +13,12 @@ __all__ = ['LoginAssetCheckAPI', 'LoginAssetConfirmStatusAPI'] | |||
| class LoginAssetCheckAPI(CreateAPIView): | ||||
|     serializer_class = serializers.LoginAssetCheckSerializer | ||||
|     model = LoginAssetACL | ||||
|     rbac_perms = { | ||||
|         'POST': 'tickets.add_superticket' | ||||
|     } | ||||
| 
 | ||||
|     def get_queryset(self): | ||||
|         return LoginAssetACL.objects.all() | ||||
| 
 | ||||
|     def create(self, request, *args, **kwargs): | ||||
|         is_need_confirm, response_data = self.check_if_need_confirm() | ||||
|  |  | |||
|  | @ -1,12 +1,14 @@ | |||
| from django.db.models import F, Q | ||||
| from rest_framework.decorators import action | ||||
| from django_filters import rest_framework as filters | ||||
| from rest_framework.response import Response | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from django_filters import rest_framework as filters | ||||
| from rest_framework.decorators import action | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.generics import CreateAPIView | ||||
| 
 | ||||
| from orgs.mixins.api import OrgBulkModelViewSet | ||||
| from rbac.permissions import RBACPermission | ||||
| from common.drf.filters import BaseFilterSet | ||||
| from common.permissions import NeedMFAVerify | ||||
| from ..tasks.account_connectivity import test_accounts_connectivity_manual | ||||
| from ..models import AuthBook, Node | ||||
| from .. import serializers | ||||
|  | @ -84,6 +86,7 @@ class AccountSecretsViewSet(AccountViewSet): | |||
|         'default': serializers.AccountSecretSerializer | ||||
|     } | ||||
|     http_method_names = ['get'] | ||||
|     permission_classes = [RBACPermission, NeedMFAVerify] | ||||
|     rbac_perms = { | ||||
|         'list': 'assets.view_assetsecret', | ||||
|         'retrieve': 'assets.view_assetsecret', | ||||
|  |  | |||
|  | @ -42,7 +42,7 @@ class CommandFilterRuleViewSet(OrgBulkModelViewSet): | |||
| class CommandConfirmAPI(CreateAPIView): | ||||
|     serializer_class = serializers.CommandConfirmSerializer | ||||
|     rbac_perms = { | ||||
|         'create': 'tickets.add_ticket' | ||||
|         'POST': 'tickets.add_superticket' | ||||
|     } | ||||
| 
 | ||||
|     def create(self, request, *args, **kwargs): | ||||
|  |  | |||
|  | @ -105,7 +105,7 @@ class ClientProtocolMixin: | |||
|         width = self.request.query_params.get('width') | ||||
|         full_screen = is_true(self.request.query_params.get('full_screen')) | ||||
|         drives_redirect = is_true(self.request.query_params.get('drives_redirect')) | ||||
|         token = self.create_token(user, asset, application, system_user) | ||||
|         token, secret = self.create_token(user, asset, application, system_user) | ||||
| 
 | ||||
|         # 设置磁盘挂载 | ||||
|         if drives_redirect: | ||||
|  | @ -381,15 +381,15 @@ class UserConnectionTokenViewSet( | |||
| 
 | ||||
|         key = self.CACHE_KEY_PREFIX.format(token) | ||||
|         cache.set(key, value, timeout=ttl) | ||||
|         return token | ||||
|         return token, secret | ||||
| 
 | ||||
|     def create(self, request, *args, **kwargs): | ||||
|         serializer = self.get_serializer(data=request.data) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
| 
 | ||||
|         asset, application, system_user, user = self.get_request_resource(serializer) | ||||
|         token = self.create_token(user, asset, application, system_user) | ||||
|         return Response({"token": token}, status=201) | ||||
|         token, secret = self.create_token(user, asset, application, system_user) | ||||
|         return Response({"id": token, 'secret': secret}, status=201) | ||||
| 
 | ||||
|     def valid_token(self, token): | ||||
|         from users.models import User | ||||
|  |  | |||
|  | @ -17,32 +17,35 @@ class JMSBaseAuthBackend: | |||
|     def has_perm(self, user_obj, perm, obj=None): | ||||
|         return False | ||||
| 
 | ||||
|     # can authenticate | ||||
|     def username_can_authenticate(self, username): | ||||
|         return self.allow_authenticate(username=username) | ||||
| 
 | ||||
|     def user_can_authenticate(self, user): | ||||
|         if not self.allow_authenticate(user=user): | ||||
|             return False | ||||
|         """ | ||||
|         Reject users with is_valid=False. Custom user models that don't have | ||||
|         that attribute are allowed. | ||||
|         """ | ||||
|         is_valid = getattr(user, 'is_valid', None) | ||||
|         return is_valid or is_valid is None | ||||
| 
 | ||||
|     @property | ||||
|     def backend_path(self): | ||||
|         return f'{self.__module__}.{self.__class__.__name__}' | ||||
|     # allow user to authenticate | ||||
|     def username_allow_authenticate(self, username): | ||||
|         return self.allow_authenticate(username=username) | ||||
| 
 | ||||
|     def user_allow_authenticate(self, user): | ||||
|         return self.allow_authenticate(user=user) | ||||
| 
 | ||||
|     def allow_authenticate(self, user=None, username=None): | ||||
|         if user: | ||||
|             allowed_backends = user.get_allowed_auth_backends() | ||||
|             allowed_backend_paths = user.get_allowed_auth_backend_paths() | ||||
|         else: | ||||
|             allowed_backends = User.get_user_allowed_auth_backends(username) | ||||
|         if allowed_backends is None: | ||||
|             allowed_backend_paths = User.get_user_allowed_auth_backend_paths(username) | ||||
|         if allowed_backend_paths is None: | ||||
|             # 特殊值 None 表示没有限制 | ||||
|             return True | ||||
|         allow = self.backend_path in allowed_backends | ||||
|         backend_name = self.__class__.__name__ | ||||
|         allowed_backend_names = [path.split('.')[-1] for path in allowed_backend_paths] | ||||
|         allow = backend_name in allowed_backend_names | ||||
|         if not allow: | ||||
|             info = 'User {} skip authentication backend {}, because it not in {}' | ||||
|             info = info.format(username, self.backend_path, ','.join(allowed_backends)) | ||||
|             info = info.format(username, backend_name, ','.join(allowed_backend_names)) | ||||
|             logger.debug(info) | ||||
|         return allow | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,3 +3,4 @@ | |||
| 
 | ||||
| # 保证 utils 中的模块进行初始化 | ||||
| from . import utils | ||||
| from .backends import * | ||||
|  |  | |||
|  | @ -0,0 +1 @@ | |||
| from .backends import * | ||||
|  | @ -28,6 +28,8 @@ from .signals import ( | |||
| 
 | ||||
| logger = get_logger(__file__) | ||||
| 
 | ||||
| __all__ = ['OIDCAuthCodeBackend', 'OIDCAuthPasswordBackend'] | ||||
| 
 | ||||
| 
 | ||||
| class UserMixin: | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,2 +1,4 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # | ||||
| 
 | ||||
| from .backends import * | ||||
|  |  | |||
|  | @ -57,8 +57,8 @@ def authenticate(request=None, **credentials): | |||
|     username = credentials.get('username') | ||||
| 
 | ||||
|     for backend, backend_path in _get_backends(return_tuples=True): | ||||
|         # 预先检查,不浪费认证时间 | ||||
|         if not backend.username_can_authenticate(username): | ||||
|         # 检查用户名是否允许认证 (预先检查,不浪费认证时间) | ||||
|         if not backend.username_allow_authenticate(username): | ||||
|             continue | ||||
| 
 | ||||
|         # 原生 | ||||
|  | @ -76,8 +76,8 @@ def authenticate(request=None, **credentials): | |||
|         if user is None: | ||||
|             continue | ||||
| 
 | ||||
|         # 再次检查遇检查中遗漏的用户 | ||||
|         if not backend.user_can_authenticate(user): | ||||
|         # 检查用户是否允许认证 | ||||
|         if not backend.user_allow_authenticate(user): | ||||
|             continue | ||||
| 
 | ||||
|         # Annotate the user object with the path of the backend. | ||||
|  |  | |||
|  | @ -169,7 +169,7 @@ class ConnectionTokenAssetSerializer(serializers.ModelSerializer): | |||
| class ConnectionTokenSystemUserSerializer(serializers.ModelSerializer): | ||||
|     class Meta: | ||||
|         model = SystemUser | ||||
|         fields = ['id', 'name', 'username', 'password', 'private_key', 'ad_domain', 'org_id'] | ||||
|         fields = ['id', 'name', 'username', 'password', 'private_key', 'protocol', 'ad_domain', 'org_id'] | ||||
| 
 | ||||
| 
 | ||||
| class ConnectionTokenGatewaySerializer(serializers.ModelSerializer): | ||||
|  |  | |||
|  | @ -184,9 +184,7 @@ class UserLoginView(mixins.AuthMixin, FormView): | |||
|     @staticmethod | ||||
|     def get_forgot_password_url(): | ||||
|         forgot_password_url = reverse('authentication:forgot-password') | ||||
|         has_other_auth_backend = settings.AUTHENTICATION_BACKENDS[1] != settings.AUTH_BACKEND_MODEL | ||||
|         if has_other_auth_backend and settings.FORGOT_PASSWORD_URL: | ||||
|             forgot_password_url = settings.FORGOT_PASSWORD_URL | ||||
|         forgot_password_url = settings.FORGOT_PASSWORD_URL or forgot_password_url | ||||
|         return forgot_password_url | ||||
| 
 | ||||
|     def get_context_data(self, **kwargs): | ||||
|  |  | |||
|  | @ -145,20 +145,20 @@ TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION | |||
| OTP_IN_RADIUS = CONFIG.OTP_IN_RADIUS | ||||
| 
 | ||||
| 
 | ||||
| AUTH_BACKEND_MODEL = 'authentication.backends.base.JMSModelBackend' | ||||
| RBAC_BACKEND = 'rbac.backends.RBACBackend' | ||||
| AUTH_BACKEND_MODEL = 'authentication.backends.base.JMSModelBackend' | ||||
| AUTH_BACKEND_PUBKEY = 'authentication.backends.pubkey.PublicKeyAuthBackend' | ||||
| AUTH_BACKEND_LDAP = 'authentication.backends.ldap.LDAPAuthorizationBackend' | ||||
| AUTH_BACKEND_OIDC_PASSWORD = 'authentication.backends.oidc.backends.OIDCAuthPasswordBackend' | ||||
| AUTH_BACKEND_OIDC_CODE = 'authentication.backends.oidc.backends.OIDCAuthCodeBackend' | ||||
| AUTH_BACKEND_OIDC_PASSWORD = 'authentication.backends.oidc.OIDCAuthPasswordBackend' | ||||
| AUTH_BACKEND_OIDC_CODE = 'authentication.backends.oidc.OIDCAuthCodeBackend' | ||||
| AUTH_BACKEND_RADIUS = 'authentication.backends.radius.RadiusBackend' | ||||
| AUTH_BACKEND_CAS = 'authentication.backends.cas.backends.CASBackend' | ||||
| AUTH_BACKEND_CAS = 'authentication.backends.cas.CASBackend' | ||||
| AUTH_BACKEND_SSO = 'authentication.backends.sso.SSOAuthentication' | ||||
| AUTH_BACKEND_WECOM = 'authentication.backends.sso.WeComAuthentication' | ||||
| AUTH_BACKEND_DINGTALK = 'authentication.backends.sso.DingTalkAuthentication' | ||||
| AUTH_BACKEND_FEISHU = 'authentication.backends.sso.FeiShuAuthentication' | ||||
| AUTH_BACKEND_AUTH_TOKEN = 'authentication.backends.sso.AuthorizationTokenAuthentication' | ||||
| AUTH_BACKEND_SAML2 = 'authentication.backends.saml2.backends.SAML2Backend' | ||||
| AUTH_BACKEND_SAML2 = 'authentication.backends.saml2.SAML2Backend' | ||||
| 
 | ||||
| 
 | ||||
| AUTHENTICATION_BACKENDS = [ | ||||
|  |  | |||
|  | @ -36,7 +36,7 @@ class UserGroupGrantedAssetsApi(ListAPIView): | |||
|     filterset_fields = ['hostname', 'ip', 'id', 'comment'] | ||||
|     search_fields = ['hostname', 'ip', 'comment'] | ||||
|     rbac_perms = { | ||||
|         'list': 'perms.view_userassets' | ||||
|         'list': 'perms.view_usergroupassets', | ||||
|     } | ||||
| 
 | ||||
|     def get_queryset(self): | ||||
|  | @ -73,7 +73,7 @@ class UserGroupGrantedNodeAssetsApi(ListAPIView): | |||
|     filterset_fields = ['hostname', 'ip', 'id', 'comment'] | ||||
|     search_fields = ['hostname', 'ip', 'comment'] | ||||
|     rbac_perms = { | ||||
|         'list': 'perms.view_userassets' | ||||
|         'list': 'perms.view_usergroupassets', | ||||
|     } | ||||
| 
 | ||||
|     def get_queryset(self): | ||||
|  | @ -125,7 +125,7 @@ class UserGroupGrantedNodeAssetsApi(ListAPIView): | |||
| class UserGroupGrantedNodesApi(ListAPIView): | ||||
|     serializer_class = serializers.NodeGrantedSerializer | ||||
|     rbac_perms = { | ||||
|         'list': 'perms.view_userassets' | ||||
|         'list': 'perms.view_usergroupassets', | ||||
|     } | ||||
| 
 | ||||
|     def get_queryset(self): | ||||
|  | @ -142,7 +142,8 @@ class UserGroupGrantedNodesApi(ListAPIView): | |||
| 
 | ||||
| class UserGroupGrantedNodeChildrenAsTreeApi(SerializeToTreeNodeMixin, ListAPIView): | ||||
|     rbac_perms = { | ||||
|         'list': 'perms.view_userassets' | ||||
|         'list': 'perms.view_usergroupassets', | ||||
|         'GET': 'perms.view_usergroupassets', | ||||
|     } | ||||
| 
 | ||||
|     def get_children_nodes(self, parent_key): | ||||
|  |  | |||
|  | @ -35,7 +35,8 @@ __all__ = [ | |||
| class GetUserAssetPermissionActionsApi(RetrieveAPIView): | ||||
|     serializer_class = serializers.ActionsSerializer | ||||
|     rbac_perms = { | ||||
|         'retrieve': 'perms.view_userassets' | ||||
|         'retrieve': 'perms.view_userassets', | ||||
|         'GET': 'perms.view_userassets', | ||||
|     } | ||||
| 
 | ||||
|     def get_user(self): | ||||
|  | @ -114,23 +115,38 @@ class UserGrantedAssetSystemUsersForAdminApi(ListAPIView): | |||
|         user_id = self.kwargs.get('pk') | ||||
|         return User.objects.get(id=user_id) | ||||
| 
 | ||||
|     @lazyproperty | ||||
|     def system_users_with_actions(self): | ||||
|         asset_id = self.kwargs.get('asset_id') | ||||
|         asset = get_object_or_404(Asset, id=asset_id, is_active=True) | ||||
|         return self.get_asset_system_user_ids_with_actions(asset) | ||||
| 
 | ||||
|     def get_asset_system_user_ids_with_actions(self, asset): | ||||
|         return get_asset_system_user_ids_with_actions_by_user(self.user, asset) | ||||
| 
 | ||||
|     def get_queryset(self): | ||||
|         asset_id = self.kwargs.get('asset_id') | ||||
|         asset = get_object_or_404(Asset, id=asset_id, is_active=True) | ||||
|         system_users_with_actions = self.get_asset_system_user_ids_with_actions(asset) | ||||
|         system_user_ids = system_users_with_actions.keys() | ||||
|         system_users = SystemUser.objects.filter(id__in=system_user_ids)\ | ||||
|         system_user_ids = self.system_users_with_actions.keys() | ||||
|         system_users = SystemUser.objects.filter(id__in=system_user_ids) \ | ||||
|             .only(*self.serializer_class.Meta.only_fields) \ | ||||
|             .order_by('name') | ||||
|         system_users = list(system_users) | ||||
|         for system_user in system_users: | ||||
|             actions = system_users_with_actions.get(system_user.id, 0) | ||||
|             system_user.actions = actions | ||||
|         return system_users | ||||
| 
 | ||||
|     def paginate_queryset(self, queryset): | ||||
|         page = super().paginate_queryset(queryset) | ||||
| 
 | ||||
|         if page: | ||||
|             page = self.set_systemusers_action(page) | ||||
|         else: | ||||
|             self.set_systemusers_action(queryset) | ||||
|         return page | ||||
| 
 | ||||
|     def set_systemusers_action(self, queryset): | ||||
|         queryset_list = list(queryset) | ||||
|         for system_user in queryset_list: | ||||
|             actions = self.system_users_with_actions.get(system_user.id, 0) | ||||
|             system_user.actions = actions | ||||
|         return queryset_list | ||||
| 
 | ||||
| 
 | ||||
| @method_decorator(tmp_to_root_org(), name='list') | ||||
| class MyGrantedAssetSystemUsersApi(UserGrantedAssetSystemUsersForAdminApi): | ||||
|  |  | |||
|  | @ -46,7 +46,10 @@ class OrgRoleBindingViewSet(RoleBindingViewSet): | |||
|     def perform_bulk_create(self, serializer): | ||||
|         validated_data = serializer.validated_data | ||||
|         bindings = [ | ||||
|             OrgRoleBinding(role=d['role'], user=d['user'], org_id=current_org.id, scope='org') | ||||
|             OrgRoleBinding( | ||||
|                 role=d['role'], user=d['user'], | ||||
|                 org_id=current_org.id, scope='org' | ||||
|             ) | ||||
|             for d in validated_data | ||||
|         ] | ||||
|         OrgRoleBinding.objects.bulk_create(bindings, ignore_conflicts=True) | ||||
|  |  | |||
|  | @ -12,7 +12,7 @@ class RBACBackend(JMSBaseAuthBackend): | |||
|     def authenticate(self, *args, **kwargs): | ||||
|         return None | ||||
| 
 | ||||
|     def username_can_authenticate(self, username): | ||||
|     def username_allow_authenticate(self, username): | ||||
|         return False | ||||
| 
 | ||||
|     def has_perm(self, user_obj, perm, obj=None): | ||||
|  |  | |||
|  | @ -51,7 +51,7 @@ exclude_permissions = ( | |||
|     ('audits', 'userloginlog', 'change,delete,change', 'userloginlog'), | ||||
|     ('audits', 'ftplog', 'change,delete', 'ftplog'), | ||||
|     ('terminal', 'session', 'delete', 'session'), | ||||
|     ('tickets', '*', '*', '*'), | ||||
|     ('tickets', 'ticket', '*', '*'), | ||||
|     ('users', 'userpasswordhistory', '*', '*'), | ||||
|     ('xpack', 'interface', 'add,delete', 'interface'), | ||||
| ) | ||||
|  |  | |||
|  | @ -27,7 +27,7 @@ class Migration(migrations.Migration): | |||
|             ], | ||||
|             options={ | ||||
|                 'verbose_name': 'Menu permission', | ||||
|                 'permissions': [('view_adminview', 'Admin view'), ('view_auditview', 'Audit view'), ('view_userview', 'User view')], | ||||
|                 'permissions': [('view_adminview', 'view console view'), ('view_auditview', 'view audit view'), ('view_userview', 'view workspace view')], | ||||
|                 'default_permissions': [], | ||||
|             }, | ||||
|         ), | ||||
|  |  | |||
|  | @ -12,7 +12,7 @@ class MenuPermission(models.Model): | |||
|         default_permissions = [] | ||||
|         verbose_name = _('Menu permission') | ||||
|         permissions = [ | ||||
|             ('view_adminview', _('Console view')), | ||||
|             ('view_auditview', _('Audit view')), | ||||
|             ('view_userview', _('Workspace view')), | ||||
|             ('view_adminview', _('view console view')), | ||||
|             ('view_auditview', _('view audit view')), | ||||
|             ('view_userview', _('view workspace view')), | ||||
|         ] | ||||
|  |  | |||
|  | @ -90,7 +90,7 @@ class OrgRoleBindingManager(models.Manager): | |||
|         if current_org.is_root(): | ||||
|             return queryset.none() | ||||
| 
 | ||||
|         queryset = queryset.filter(org=current_org.id) | ||||
|         queryset = queryset.filter(org=current_org.id, scope=Scope.org) | ||||
|         return queryset | ||||
| 
 | ||||
| 
 | ||||
|  | @ -120,8 +120,7 @@ class OrgRoleBinding(RoleBinding): | |||
| 
 | ||||
| class SystemRoleBindingManager(models.Manager): | ||||
|     def get_queryset(self): | ||||
|         queryset = super().get_queryset().\ | ||||
|             filter(scope=Scope.org) | ||||
|         queryset = super().get_queryset().filter(scope=Scope.system) | ||||
|         return queryset | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -79,61 +79,9 @@ class Setting(models.Model): | |||
|         item.refresh_setting() | ||||
| 
 | ||||
|     def refresh_setting(self): | ||||
|         if hasattr(self.__class__, f'refresh_{self.name}'): | ||||
|             getattr(self.__class__, f'refresh_{self.name}')() | ||||
|         else: | ||||
|             setattr(settings, self.name, self.cleaned_value) | ||||
|         setattr(settings, self.name, self.cleaned_value) | ||||
|         self.refresh_keycloak_to_openid_if_need() | ||||
| 
 | ||||
|     @classmethod | ||||
|     def refresh_authentications(cls, name): | ||||
|         setting = cls.objects.filter(name=name).first() | ||||
|         if not setting: | ||||
|             return | ||||
| 
 | ||||
|         backends_map = { | ||||
|             'AUTH_LDAP': [settings.AUTH_BACKEND_LDAP], | ||||
|             'AUTH_OPENID': [settings.AUTH_BACKEND_OIDC_CODE, settings.AUTH_BACKEND_OIDC_PASSWORD], | ||||
|             'AUTH_RADIUS': [settings.AUTH_BACKEND_RADIUS], | ||||
|             'AUTH_CAS': [settings.AUTH_BACKEND_CAS], | ||||
|             'AUTH_SAML2': [settings.AUTH_BACKEND_SAML2], | ||||
|         } | ||||
|         setting_backends = backends_map[name] | ||||
|         auth_backends = settings.AUTHENTICATION_BACKENDS | ||||
| 
 | ||||
|         for backend in setting_backends: | ||||
|             has = backend in auth_backends | ||||
| 
 | ||||
|             # 添加 | ||||
|             if setting.cleaned_value and not has: | ||||
|                 logger.debug('Add auth backend: {}'.format(name)) | ||||
|                 settings.AUTHENTICATION_BACKENDS.insert(1, backend) | ||||
| 
 | ||||
|             # 去掉 | ||||
|             if not setting.cleaned_value and has: | ||||
|                 index = auth_backends.index(backend) | ||||
|                 logger.debug('Pop auth backend: {}'.format(name)) | ||||
|                 auth_backends.pop(index) | ||||
| 
 | ||||
|         # 设置内存值 | ||||
|         setattr(settings, name, setting.cleaned_value) | ||||
| 
 | ||||
|     @classmethod | ||||
|     def refresh_AUTH_CAS(cls): | ||||
|         cls.refresh_authentications('AUTH_CAS') | ||||
| 
 | ||||
|     @classmethod | ||||
|     def refresh_AUTH_LDAP(cls): | ||||
|         cls.refresh_authentications('AUTH_LDAP') | ||||
| 
 | ||||
|     @classmethod | ||||
|     def refresh_AUTH_OPENID(cls): | ||||
|         cls.refresh_authentications('AUTH_OPENID') | ||||
| 
 | ||||
|     @classmethod | ||||
|     def refresh_AUTH_SAML2(cls): | ||||
|         cls.refresh_authentications('AUTH_SAML2') | ||||
| 
 | ||||
|     def refresh_keycloak_to_openid_if_need(self): | ||||
|         watch_config_names = [ | ||||
|             'AUTH_OPENID', 'AUTH_OPENID_REALM_NAME', 'AUTH_OPENID_SERVER_URL', | ||||
|  | @ -170,10 +118,6 @@ class Setting(models.Model): | |||
|             setattr(settings, key, value) | ||||
|             self.__class__.update_or_create(key, value, encrypted=False, category=self.category) | ||||
| 
 | ||||
|     @classmethod | ||||
|     def refresh_AUTH_RADIUS(cls): | ||||
|         cls.refresh_authentications('AUTH_RADIUS') | ||||
| 
 | ||||
|     @classmethod | ||||
|     def update_or_create(cls, name='', value='', encrypted=False, category=''): | ||||
|         """ | ||||
|  |  | |||
|  | @ -1,7 +1,9 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # | ||||
| 
 | ||||
| import datetime | ||||
| import logging | ||||
| from django.utils import timezone | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from rest_framework import viewsets, generics | ||||
| from rest_framework.views import Response | ||||
|  | @ -28,14 +30,24 @@ class StatusViewSet(viewsets.ModelViewSet): | |||
|         serializer.is_valid(raise_exception=True) | ||||
|         self.handle_sessions() | ||||
|         self.perform_create(serializer) | ||||
|         tasks = self.request.user.terminal.task_set.filter(is_finished=False) | ||||
|         serializer = self.task_serializer_class(tasks, many=True) | ||||
|         return Response(serializer.data, status=201) | ||||
|         task_serializer = self.get_task_serializer() | ||||
|         return Response(task_serializer.data, status=201) | ||||
| 
 | ||||
|     def handle_sessions(self): | ||||
|         session_ids = self.request.data.get('sessions', []) | ||||
|         Session.set_sessions_active(session_ids) | ||||
| 
 | ||||
|     def perform_create(self, serializer): | ||||
|         serializer.validated_data.pop('sessions', None) | ||||
|         serializer.validated_data["terminal"] = self.request.user.terminal | ||||
|         return super().perform_create(serializer) | ||||
| 
 | ||||
|     def get_task_serializer(self): | ||||
|         critical_time = timezone.now() - datetime.timedelta(minutes=10) | ||||
|         tasks = self.request.user.terminal.task_set.filter(is_finished=False, date_created__gte=critical_time) | ||||
|         serializer = self.task_serializer_class(tasks, many=True) | ||||
|         return serializer | ||||
| 
 | ||||
|     def get_queryset(self): | ||||
|         terminal_id = self.kwargs.get("terminal", None) | ||||
|         if terminal_id: | ||||
|  | @ -43,11 +55,6 @@ class StatusViewSet(viewsets.ModelViewSet): | |||
|             return terminal.status_set.all() | ||||
|         return super().get_queryset() | ||||
| 
 | ||||
|     def perform_create(self, serializer): | ||||
|         serializer.validated_data.pop('sessions', None) | ||||
|         serializer.validated_data["terminal"] = self.request.user.terminal | ||||
|         return super().perform_create(serializer) | ||||
| 
 | ||||
| 
 | ||||
| class ComponentsMetricsAPIView(generics.GenericAPIView): | ||||
|     """ 返回汇总组件指标数据 """ | ||||
|  |  | |||
|  | @ -17,6 +17,7 @@ class ReplayStorageTypeChoices(TextChoices): | |||
|     oss = 'oss', 'OSS' | ||||
|     azure = 'azure', 'Azure' | ||||
|     obs = 'obs', 'OBS' | ||||
|     cos = 'cos', 'COS' | ||||
| 
 | ||||
| 
 | ||||
| class CommandStorageTypeChoices(TextChoices): | ||||
|  | @ -47,6 +48,7 @@ class TerminalTypeChoices(TextChoices): | |||
|     lion = 'lion', 'Lion' | ||||
|     core = 'core', 'Core' | ||||
|     celery = 'celery', 'Celery' | ||||
|     magnus = 'magnus',  'Magnus' | ||||
| 
 | ||||
|     @classmethod | ||||
|     def types(cls): | ||||
|  |  | |||
|  | @ -0,0 +1,28 @@ | |||
| # Generated by Django 3.1.13 on 2022-02-28 03:44 | ||||
| 
 | ||||
| from django.db import migrations, models | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ('terminal', '0044_auto_20220223_1539'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='session', | ||||
|             name='login_from', | ||||
|             field=models.CharField(choices=[('ST', 'SSH Terminal'), ('RT', 'RDP Terminal'), ('WT', 'Web Terminal'), ('DT', 'DB Terminal')], default='ST', max_length=2, verbose_name='Login from'), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='sessionjoinrecord', | ||||
|             name='login_from', | ||||
|             field=models.CharField(choices=[('ST', 'SSH Terminal'), ('RT', 'RDP Terminal'), ('WT', 'Web Terminal'), ('DT', 'DB Terminal')], default='WT', max_length=2, verbose_name='Login from'), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='terminal', | ||||
|             name='type', | ||||
|             field=models.CharField(choices=[('koko', 'KoKo'), ('guacamole', 'Guacamole'), ('omnidb', 'OmniDB'), ('xrdp', 'Xrdp'), ('lion', 'Lion'), ('core', 'Core'), ('celery', 'Celery'), ('magnus', 'Magnus')], default='koko', max_length=64, verbose_name='type'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -0,0 +1,18 @@ | |||
| # Generated by Django 3.1.13 on 2022-02-28 09:44 | ||||
| 
 | ||||
| from django.db import migrations, models | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ('terminal', '0045_auto_20220228_1144'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='replaystorage', | ||||
|             name='type', | ||||
|             field=models.CharField(choices=[('null', 'Null'), ('server', 'Server'), ('s3', 'S3'), ('ceph', 'Ceph'), ('swift', 'Swift'), ('oss', 'OSS'), ('azure', 'Azure'), ('obs', 'OBS'), ('cos', 'COS')], default='server', max_length=16, verbose_name='Type'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -22,6 +22,7 @@ class Session(OrgModelMixin): | |||
|         ST = 'ST', 'SSH Terminal' | ||||
|         RT = 'RT', 'RDP Terminal' | ||||
|         WT = 'WT', 'Web Terminal' | ||||
|         DT = 'DT', 'DB Terminal' | ||||
| 
 | ||||
|     class PROTOCOL(TextChoices): | ||||
|         SSH = 'ssh', 'ssh' | ||||
|  |  | |||
|  | @ -42,8 +42,8 @@ class ReplayStorageTypeBaseSerializer(serializers.Serializer): | |||
| 
 | ||||
| class ReplayStorageTypeS3Serializer(ReplayStorageTypeBaseSerializer): | ||||
|     endpoint_help_text = ''' | ||||
|         S3 format: http://s3.{REGION_NAME}.amazonaws.com | ||||
|         S3(China) format: http://s3.{REGION_NAME}.amazonaws.com.cn | ||||
|         S3 format: http://s3.{REGION_NAME}.amazonaws.com <br> | ||||
|         S3(China) format: http://s3.{REGION_NAME}.amazonaws.com.cn <br> | ||||
|         Such as: http://s3.cn-north-1.amazonaws.com.cn | ||||
|     ''' | ||||
|     ENDPOINT = serializers.CharField( | ||||
|  | @ -73,7 +73,7 @@ class ReplayStorageTypeSwiftSerializer(ReplayStorageTypeBaseSerializer): | |||
| 
 | ||||
| class ReplayStorageTypeOSSSerializer(ReplayStorageTypeBaseSerializer): | ||||
|     endpoint_help_text = ''' | ||||
|         OSS format: http://{REGION_NAME}.aliyuncs.com | ||||
|         OSS format: http://{REGION_NAME}.aliyuncs.com <br> | ||||
|         Such as: http://oss-cn-hangzhou.aliyuncs.com | ||||
|     ''' | ||||
|     ENDPOINT = serializers.CharField( | ||||
|  | @ -84,7 +84,7 @@ class ReplayStorageTypeOSSSerializer(ReplayStorageTypeBaseSerializer): | |||
| 
 | ||||
| class ReplayStorageTypeOBSSerializer(ReplayStorageTypeBaseSerializer): | ||||
|     endpoint_help_text = ''' | ||||
|         OBS format: obs.{REGION_NAME}.myhuaweicloud.com | ||||
|         OBS format: obs.{REGION_NAME}.myhuaweicloud.com <br> | ||||
|         Such as: obs.cn-north-4.myhuaweicloud.com | ||||
|     ''' | ||||
|     ENDPOINT = serializers.CharField( | ||||
|  | @ -92,6 +92,15 @@ class ReplayStorageTypeOBSSerializer(ReplayStorageTypeBaseSerializer): | |||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| class ReplayStorageTypeCOSSerializer(ReplayStorageTypeS3Serializer): | ||||
|     endpoint_help_text = '''Such as: http://cos.{REGION_NAME}.myqcloud.com''' | ||||
|     ENDPOINT = serializers.CharField( | ||||
|         validators=[replay_storage_endpoint_format_validator], | ||||
|         required=True, max_length=1024, label=_('Endpoint'), help_text=_(endpoint_help_text), | ||||
|         allow_null=True, | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| class ReplayStorageTypeAzureSerializer(serializers.Serializer): | ||||
|     class EndpointSuffixChoices(TextChoices): | ||||
|         china = 'core.chinacloudapi.cn', 'core.chinacloudapi.cn' | ||||
|  | @ -116,7 +125,8 @@ replay_storage_type_serializer_classes_mapping = { | |||
|     const.ReplayStorageTypeChoices.swift.value: ReplayStorageTypeSwiftSerializer, | ||||
|     const.ReplayStorageTypeChoices.oss.value: ReplayStorageTypeOSSSerializer, | ||||
|     const.ReplayStorageTypeChoices.azure.value: ReplayStorageTypeAzureSerializer, | ||||
|     const.ReplayStorageTypeChoices.obs.value: ReplayStorageTypeOBSSerializer | ||||
|     const.ReplayStorageTypeChoices.obs.value: ReplayStorageTypeOBSSerializer, | ||||
|     const.ReplayStorageTypeChoices.cos.value: ReplayStorageTypeCOSSerializer | ||||
| } | ||||
| 
 | ||||
| # Command storage serializers | ||||
|  | @ -143,7 +153,7 @@ def command_storage_es_host_format_validator(host): | |||
| class CommandStorageTypeESSerializer(serializers.Serializer): | ||||
| 
 | ||||
|     hosts_help_text = ''' | ||||
|         Tip: If there are multiple hosts, use a comma (,) to separate them.  | ||||
|         Tip: If there are multiple hosts, use a comma (,) to separate them. <br> | ||||
|         (eg: http://www.jumpserver.a.com:9100, http://www.jumpserver.b.com:9100) | ||||
|     ''' | ||||
|     HOSTS = serializers.ListField( | ||||
|  |  | |||
|  | @ -14,7 +14,7 @@ from common.utils import get_log_keep_day | |||
| from ops.celery.decorator import ( | ||||
|     register_as_period_task, after_app_ready_start, after_app_shutdown_clean_periodic | ||||
| ) | ||||
| from .models import Status, Session, Command | ||||
| from .models import Status, Session, Command, Task | ||||
| from .backends import server_replay_storage | ||||
| from .utils import find_session_replay_local | ||||
| 
 | ||||
|  | @ -39,6 +39,11 @@ def delete_terminal_status_period(): | |||
| def clean_orphan_session(): | ||||
|     active_sessions = Session.objects.filter(is_finished=False) | ||||
|     for session in active_sessions: | ||||
|         # finished task | ||||
|         Task.objects.filter(args=str(session.id), is_finished=False).update( | ||||
|             is_finished=True, date_finished=timezone.now() | ||||
|         ) | ||||
|         # finished session | ||||
|         if session.is_active(): | ||||
|             continue | ||||
|         session.is_finished = True | ||||
|  |  | |||
|  | @ -0,0 +1,25 @@ | |||
| # Generated by Django 3.1.14 on 2022-02-28 10:59 | ||||
| 
 | ||||
| from django.db import migrations | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ('tickets', '0014_auto_20220217_2135'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name='SuperTicket', | ||||
|             fields=[ | ||||
|             ], | ||||
|             options={ | ||||
|                 'verbose_name': 'Super ticket', | ||||
|                 'proxy': True, | ||||
|                 'indexes': [], | ||||
|                 'constraints': [], | ||||
|             }, | ||||
|             bases=('tickets.ticket',), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -48,7 +48,81 @@ class TicketAssignee(CommonModelMixin): | |||
|         return '{0.assignee.name}({0.assignee.username})_{0.step}'.format(self) | ||||
| 
 | ||||
| 
 | ||||
| class Ticket(CommonModelMixin, OrgModelMixin): | ||||
| class StatusMixin: | ||||
|     state: str | ||||
|     status: str | ||||
|     applicant: models.ForeignKey | ||||
|     current_node: models.Manager | ||||
| 
 | ||||
|     def set_state_approve(self): | ||||
|         self.state = TicketState.approved | ||||
| 
 | ||||
|     def set_state_reject(self): | ||||
|         self.state = TicketState.rejected | ||||
| 
 | ||||
|     def set_state_closed(self): | ||||
|         self.state = TicketState.closed | ||||
| 
 | ||||
|     def set_status_closed(self): | ||||
|         self.status = TicketStatus.closed | ||||
| 
 | ||||
|     # status | ||||
|     @property | ||||
|     def status_open(self): | ||||
|         return self.status == TicketStatus.open.value | ||||
| 
 | ||||
|     @property | ||||
|     def status_closed(self): | ||||
|         return self.status == TicketStatus.closed.value | ||||
| 
 | ||||
|     @property | ||||
|     def state_open(self): | ||||
|         return self.state == TicketState.open.value | ||||
| 
 | ||||
|     @property | ||||
|     def state_approve(self): | ||||
|         return self.state == TicketState.approved.value | ||||
| 
 | ||||
|     @property | ||||
|     def state_reject(self): | ||||
|         return self.state == TicketState.rejected.value | ||||
| 
 | ||||
|     @property | ||||
|     def state_close(self): | ||||
|         return self.state == TicketState.closed.value | ||||
| 
 | ||||
|     # action changed | ||||
|     def open(self, applicant): | ||||
|         self.applicant = applicant | ||||
|         self._change_action(TicketAction.open) | ||||
| 
 | ||||
|     def approve(self, processor): | ||||
|         self.update_current_step_state_and_assignee(processor, TicketState.approved) | ||||
|         self._change_action(TicketAction.approve) | ||||
| 
 | ||||
|     def reject(self, processor): | ||||
|         self.update_current_step_state_and_assignee(processor, TicketState.rejected) | ||||
|         self._change_action(TicketAction.reject) | ||||
| 
 | ||||
|     def close(self, processor): | ||||
|         self.update_current_step_state_and_assignee(processor, TicketState.closed) | ||||
|         self._change_action(TicketAction.close) | ||||
| 
 | ||||
|     def update_current_step_state_and_assignee(self, processor, state): | ||||
|         if self.status_closed: | ||||
|             raise AlreadyClosed | ||||
|         if state != TicketState.approved: | ||||
|             self.state = state | ||||
|         current_node = self.current_node | ||||
|         current_node.update(state=state) | ||||
|         current_node.first().ticket_assignees.filter(assignee=processor).update(state=state) | ||||
| 
 | ||||
|     def _change_action(self, action): | ||||
|         self.save() | ||||
|         post_change_ticket_action.send(sender=self.__class__, ticket=self, action=action) | ||||
| 
 | ||||
| 
 | ||||
| class Ticket(CommonModelMixin, StatusMixin, OrgModelMixin): | ||||
|     title = models.CharField(max_length=256, verbose_name=_("Title")) | ||||
|     type = models.CharField( | ||||
|         max_length=64, choices=TicketType.choices, | ||||
|  | @ -102,31 +176,6 @@ class Ticket(CommonModelMixin, OrgModelMixin): | |||
|     def type_login_confirm(self): | ||||
|         return self.type == TicketType.login_confirm.value | ||||
| 
 | ||||
|     # status | ||||
|     @property | ||||
|     def status_open(self): | ||||
|         return self.status == TicketStatus.open.value | ||||
| 
 | ||||
|     @property | ||||
|     def status_closed(self): | ||||
|         return self.status == TicketStatus.closed.value | ||||
| 
 | ||||
|     @property | ||||
|     def state_open(self): | ||||
|         return self.state == TicketState.open.value | ||||
| 
 | ||||
|     @property | ||||
|     def state_approve(self): | ||||
|         return self.state == TicketState.approved.value | ||||
| 
 | ||||
|     @property | ||||
|     def state_reject(self): | ||||
|         return self.state == TicketState.rejected.value | ||||
| 
 | ||||
|     @property | ||||
|     def state_close(self): | ||||
|         return self.state == TicketState.closed.value | ||||
| 
 | ||||
|     @property | ||||
|     def current_node(self): | ||||
|         return self.ticket_steps.filter(level=self.approval_step) | ||||
|  | @ -136,18 +185,6 @@ class Ticket(CommonModelMixin, OrgModelMixin): | |||
|         processor = self.current_node.first().ticket_assignees.exclude(state=ProcessStatus.notified).first() | ||||
|         return processor.assignee if processor else None | ||||
| 
 | ||||
|     def set_state_approve(self): | ||||
|         self.state = TicketState.approved | ||||
| 
 | ||||
|     def set_state_reject(self): | ||||
|         self.state = TicketState.rejected | ||||
| 
 | ||||
|     def set_state_closed(self): | ||||
|         self.state = TicketState.closed | ||||
| 
 | ||||
|     def set_status_closed(self): | ||||
|         self.status = TicketStatus.closed | ||||
| 
 | ||||
|     def create_related_node(self): | ||||
|         org_id = self.flow.org_id | ||||
|         approval_rule = self.get_current_ticket_flow_approve() | ||||
|  | @ -191,36 +228,6 @@ class Ticket(CommonModelMixin, OrgModelMixin): | |||
|             ticket_assignees.append(TicketAssignee(step=ticket_step, assignee=assignee)) | ||||
|         TicketAssignee.objects.bulk_create(ticket_assignees) | ||||
| 
 | ||||
|     # action changed | ||||
|     def open(self, applicant): | ||||
|         self.applicant = applicant | ||||
|         self._change_action(TicketAction.open) | ||||
| 
 | ||||
|     def update_current_step_state_and_assignee(self, processor, state): | ||||
|         if self.status_closed: | ||||
|             raise AlreadyClosed | ||||
|         if state != TicketState.approved: | ||||
|             self.state = state | ||||
|         current_node = self.current_node | ||||
|         current_node.update(state=state) | ||||
|         current_node.first().ticket_assignees.filter(assignee=processor).update(state=state) | ||||
| 
 | ||||
|     def approve(self, processor): | ||||
|         self.update_current_step_state_and_assignee(processor, TicketState.approved) | ||||
|         self._change_action(TicketAction.approve) | ||||
| 
 | ||||
|     def reject(self, processor): | ||||
|         self.update_current_step_state_and_assignee(processor, TicketState.rejected) | ||||
|         self._change_action(TicketAction.reject) | ||||
| 
 | ||||
|     def close(self, processor): | ||||
|         self.update_current_step_state_and_assignee(processor, TicketState.closed) | ||||
|         self._change_action(TicketAction.close) | ||||
| 
 | ||||
|     def _change_action(self, action): | ||||
|         self.save() | ||||
|         post_change_ticket_action.send(sender=self.__class__, ticket=self, action=action) | ||||
| 
 | ||||
|     def has_current_assignee(self, assignee): | ||||
|         return self.ticket_steps.filter(ticket_assignees__assignee=assignee, level=self.approval_step).exists() | ||||
| 
 | ||||
|  | @ -301,3 +308,9 @@ class Ticket(CommonModelMixin, OrgModelMixin): | |||
|                 raise JMSException(detail=_('Please try again'), code='please_try_again') | ||||
| 
 | ||||
|             raise e | ||||
| 
 | ||||
| 
 | ||||
| class SuperTicket(Ticket): | ||||
|     class Meta: | ||||
|         proxy = True | ||||
|         verbose_name = _("Super ticket") | ||||
|  |  | |||
|  | @ -739,15 +739,15 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): | |||
|         return super(User, self).delete() | ||||
| 
 | ||||
|     @classmethod | ||||
|     def get_user_allowed_auth_backends(cls, username): | ||||
|     def get_user_allowed_auth_backend_paths(cls, username): | ||||
|         if not settings.ONLY_ALLOW_AUTH_FROM_SOURCE or not username: | ||||
|             return None | ||||
|         user = cls.objects.filter(username=username).first() | ||||
|         if not user: | ||||
|             return None | ||||
|         return user.get_allowed_auth_backends() | ||||
|         return user.get_allowed_auth_backend_paths() | ||||
| 
 | ||||
|     def get_allowed_auth_backends(self): | ||||
|     def get_allowed_auth_backend_paths(self): | ||||
|         if not settings.ONLY_ALLOW_AUTH_FROM_SOURCE: | ||||
|             return None | ||||
|         return self.SOURCE_BACKEND_MAPPING.get(self.source, []) | ||||
|  |  | |||
|  | @ -55,14 +55,13 @@ pycparser==2.19 | |||
| pycryptodome==3.12.0 | ||||
| pycryptodomex==3.12.0 | ||||
| pyotp==2.2.6 | ||||
| PyNaCl==1.2.1 | ||||
| PyNaCl==1.5.0 | ||||
| python-dateutil==2.8.2 | ||||
| #python-gssapi==0.6.4 | ||||
| pytz==2018.3 | ||||
| PyYAML==6.0 | ||||
| redis==3.5.3 | ||||
| requests==2.25.1 | ||||
| jms-storage==0.0.41 | ||||
| jms-storage==0.0.42 | ||||
| s3transfer==0.5.0 | ||||
| simplejson==3.13.2 | ||||
| six==1.11.0 | ||||
|  | @ -128,3 +127,5 @@ kubernetes==21.7.0 | |||
| websocket-client==1.2.3 | ||||
| numpy==1.22.0 | ||||
| pandas==1.3.5 | ||||
| pyjwkest==1.4.2 | ||||
| jsonfield2==4.0.0.post0 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Jiangjie.Bai
						Jiangjie.Bai