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_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)
|
||||
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