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