diff --git a/apps/acls/api/login_asset_check.py b/apps/acls/api/login_asset_check.py index e27eefc07..49044509e 100644 --- a/apps/acls/api/login_asset_check.py +++ b/apps/acls/api/login_asset_check.py @@ -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() diff --git a/apps/assets/api/accounts.py b/apps/assets/api/accounts.py index 04cfb5d29..fcdcce8d7 100644 --- a/apps/assets/api/accounts.py +++ b/apps/assets/api/accounts.py @@ -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', diff --git a/apps/assets/api/cmd_filter.py b/apps/assets/api/cmd_filter.py index 7db425ab9..1c73d6d0b 100644 --- a/apps/assets/api/cmd_filter.py +++ b/apps/assets/api/cmd_filter.py @@ -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): diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index f88490c27..3712bdf51 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -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 diff --git a/apps/authentication/backends/base.py b/apps/authentication/backends/base.py index 6465a29b9..12b978250 100644 --- a/apps/authentication/backends/base.py +++ b/apps/authentication/backends/base.py @@ -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 diff --git a/apps/authentication/backends/cas/__init__.py b/apps/authentication/backends/cas/__init__.py index d2b30d4cc..e12668747 100644 --- a/apps/authentication/backends/cas/__init__.py +++ b/apps/authentication/backends/cas/__init__.py @@ -3,3 +3,4 @@ # 保证 utils 中的模块进行初始化 from . import utils +from .backends import * diff --git a/apps/authentication/backends/oidc/__init__.py b/apps/authentication/backends/oidc/__init__.py index e69de29bb..a0957e5c9 100644 --- a/apps/authentication/backends/oidc/__init__.py +++ b/apps/authentication/backends/oidc/__init__.py @@ -0,0 +1 @@ +from .backends import * diff --git a/apps/authentication/backends/oidc/backends.py b/apps/authentication/backends/oidc/backends.py index b8b829277..d1d7bd48c 100644 --- a/apps/authentication/backends/oidc/backends.py +++ b/apps/authentication/backends/oidc/backends.py @@ -28,6 +28,8 @@ from .signals import ( logger = get_logger(__file__) +__all__ = ['OIDCAuthCodeBackend', 'OIDCAuthPasswordBackend'] + class UserMixin: diff --git a/apps/authentication/backends/saml2/__init__.py b/apps/authentication/backends/saml2/__init__.py index ec51c5a2b..448096520 100644 --- a/apps/authentication/backends/saml2/__init__.py +++ b/apps/authentication/backends/saml2/__init__.py @@ -1,2 +1,4 @@ # -*- coding: utf-8 -*- # + +from .backends import * diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index f01b28796..56216e91f 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -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. diff --git a/apps/authentication/serializers.py b/apps/authentication/serializers.py index 61450c363..678b7763b 100644 --- a/apps/authentication/serializers.py +++ b/apps/authentication/serializers.py @@ -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): diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index 30ac80e3b..ee831eb6c 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -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): diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index 6f80a44fc..c545772e1 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -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 = [ diff --git a/apps/perms/api/asset/user_group_permission.py b/apps/perms/api/asset/user_group_permission.py index 4b1464594..9b6499bb3 100644 --- a/apps/perms/api/asset/user_group_permission.py +++ b/apps/perms/api/asset/user_group_permission.py @@ -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): diff --git a/apps/perms/api/asset/user_permission/common.py b/apps/perms/api/asset/user_permission/common.py index c5ca3a711..609fb52c3 100644 --- a/apps/perms/api/asset/user_permission/common.py +++ b/apps/perms/api/asset/user_permission/common.py @@ -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): diff --git a/apps/rbac/api/rolebinding.py b/apps/rbac/api/rolebinding.py index ad24f43bf..7d31d6415 100644 --- a/apps/rbac/api/rolebinding.py +++ b/apps/rbac/api/rolebinding.py @@ -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) diff --git a/apps/rbac/backends.py b/apps/rbac/backends.py index 11c3b8004..6f0d53c49 100644 --- a/apps/rbac/backends.py +++ b/apps/rbac/backends.py @@ -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): diff --git a/apps/rbac/const.py b/apps/rbac/const.py index 3547a165f..b7c76a261 100644 --- a/apps/rbac/const.py +++ b/apps/rbac/const.py @@ -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'), ) diff --git a/apps/rbac/migrations/0001_initial.py b/apps/rbac/migrations/0001_initial.py index 772a37258..f5ff465f9 100644 --- a/apps/rbac/migrations/0001_initial.py +++ b/apps/rbac/migrations/0001_initial.py @@ -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': [], }, ), diff --git a/apps/rbac/models/menu.py b/apps/rbac/models/menu.py index 56434feb5..b89fef6f0 100644 --- a/apps/rbac/models/menu.py +++ b/apps/rbac/models/menu.py @@ -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')), ] diff --git a/apps/rbac/models/rolebinding.py b/apps/rbac/models/rolebinding.py index c4c68fbf5..6d7fa0aa6 100644 --- a/apps/rbac/models/rolebinding.py +++ b/apps/rbac/models/rolebinding.py @@ -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 diff --git a/apps/settings/models.py b/apps/settings/models.py index 26f085f57..c89f5e81b 100644 --- a/apps/settings/models.py +++ b/apps/settings/models.py @@ -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=''): """ diff --git a/apps/terminal/api/status.py b/apps/terminal/api/status.py index 1873876e2..3ec00436b 100644 --- a/apps/terminal/api/status.py +++ b/apps/terminal/api/status.py @@ -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): """ 返回汇总组件指标数据 """ diff --git a/apps/terminal/const.py b/apps/terminal/const.py index 74844d714..931a3e887 100644 --- a/apps/terminal/const.py +++ b/apps/terminal/const.py @@ -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): diff --git a/apps/terminal/migrations/0045_auto_20220228_1144.py b/apps/terminal/migrations/0045_auto_20220228_1144.py new file mode 100644 index 000000000..000dcb53b --- /dev/null +++ b/apps/terminal/migrations/0045_auto_20220228_1144.py @@ -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'), + ), + ] diff --git a/apps/terminal/migrations/0046_auto_20220228_1744.py b/apps/terminal/migrations/0046_auto_20220228_1744.py new file mode 100644 index 000000000..be651022f --- /dev/null +++ b/apps/terminal/migrations/0046_auto_20220228_1744.py @@ -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'), + ), + ] diff --git a/apps/terminal/models/session.py b/apps/terminal/models/session.py index 5bd71d25a..09f1c9e91 100644 --- a/apps/terminal/models/session.py +++ b/apps/terminal/models/session.py @@ -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' diff --git a/apps/terminal/serializers/storage.py b/apps/terminal/serializers/storage.py index 3e4e99bc0..2b21625bd 100644 --- a/apps/terminal/serializers/storage.py +++ b/apps/terminal/serializers/storage.py @@ -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
+ S3(China) format: http://s3.{REGION_NAME}.amazonaws.com.cn
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
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
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.
(eg: http://www.jumpserver.a.com:9100, http://www.jumpserver.b.com:9100) ''' HOSTS = serializers.ListField( diff --git a/apps/terminal/tasks.py b/apps/terminal/tasks.py index 0f19d0435..59cf00fa4 100644 --- a/apps/terminal/tasks.py +++ b/apps/terminal/tasks.py @@ -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 diff --git a/apps/tickets/migrations/0015_superticket.py b/apps/tickets/migrations/0015_superticket.py new file mode 100644 index 000000000..6d2c526b7 --- /dev/null +++ b/apps/tickets/migrations/0015_superticket.py @@ -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',), + ), + ] diff --git a/apps/tickets/models/ticket.py b/apps/tickets/models/ticket.py index 5c96f61ba..f09c444eb 100644 --- a/apps/tickets/models/ticket.py +++ b/apps/tickets/models/ticket.py @@ -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") diff --git a/apps/users/models/user.py b/apps/users/models/user.py index b23d67b2c..b0c8c3009 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -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, []) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 2eff04412..aea4e3184 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -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