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):
|
class LoginAssetCheckAPI(CreateAPIView):
|
||||||
serializer_class = serializers.LoginAssetCheckSerializer
|
serializer_class = serializers.LoginAssetCheckSerializer
|
||||||
model = LoginAssetACL
|
model = LoginAssetACL
|
||||||
|
rbac_perms = {
|
||||||
|
'POST': 'tickets.add_superticket'
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return LoginAssetACL.objects.all()
|
||||||
|
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
is_need_confirm, response_data = self.check_if_need_confirm()
|
is_need_confirm, response_data = self.check_if_need_confirm()
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
from django.db.models import F, Q
|
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.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 rest_framework.generics import CreateAPIView
|
||||||
|
|
||||||
from orgs.mixins.api import OrgBulkModelViewSet
|
from orgs.mixins.api import OrgBulkModelViewSet
|
||||||
|
from rbac.permissions import RBACPermission
|
||||||
from common.drf.filters import BaseFilterSet
|
from common.drf.filters import BaseFilterSet
|
||||||
|
from common.permissions import NeedMFAVerify
|
||||||
from ..tasks.account_connectivity import test_accounts_connectivity_manual
|
from ..tasks.account_connectivity import test_accounts_connectivity_manual
|
||||||
from ..models import AuthBook, Node
|
from ..models import AuthBook, Node
|
||||||
from .. import serializers
|
from .. import serializers
|
||||||
|
@ -84,6 +86,7 @@ class AccountSecretsViewSet(AccountViewSet):
|
||||||
'default': serializers.AccountSecretSerializer
|
'default': serializers.AccountSecretSerializer
|
||||||
}
|
}
|
||||||
http_method_names = ['get']
|
http_method_names = ['get']
|
||||||
|
permission_classes = [RBACPermission, NeedMFAVerify]
|
||||||
rbac_perms = {
|
rbac_perms = {
|
||||||
'list': 'assets.view_assetsecret',
|
'list': 'assets.view_assetsecret',
|
||||||
'retrieve': 'assets.view_assetsecret',
|
'retrieve': 'assets.view_assetsecret',
|
||||||
|
|
|
@ -42,7 +42,7 @@ class CommandFilterRuleViewSet(OrgBulkModelViewSet):
|
||||||
class CommandConfirmAPI(CreateAPIView):
|
class CommandConfirmAPI(CreateAPIView):
|
||||||
serializer_class = serializers.CommandConfirmSerializer
|
serializer_class = serializers.CommandConfirmSerializer
|
||||||
rbac_perms = {
|
rbac_perms = {
|
||||||
'create': 'tickets.add_ticket'
|
'POST': 'tickets.add_superticket'
|
||||||
}
|
}
|
||||||
|
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
|
|
|
@ -105,7 +105,7 @@ class ClientProtocolMixin:
|
||||||
width = self.request.query_params.get('width')
|
width = self.request.query_params.get('width')
|
||||||
full_screen = is_true(self.request.query_params.get('full_screen'))
|
full_screen = is_true(self.request.query_params.get('full_screen'))
|
||||||
drives_redirect = is_true(self.request.query_params.get('drives_redirect'))
|
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:
|
if drives_redirect:
|
||||||
|
@ -381,15 +381,15 @@ class UserConnectionTokenViewSet(
|
||||||
|
|
||||||
key = self.CACHE_KEY_PREFIX.format(token)
|
key = self.CACHE_KEY_PREFIX.format(token)
|
||||||
cache.set(key, value, timeout=ttl)
|
cache.set(key, value, timeout=ttl)
|
||||||
return token
|
return token, secret
|
||||||
|
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
serializer = self.get_serializer(data=request.data)
|
serializer = self.get_serializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
asset, application, system_user, user = self.get_request_resource(serializer)
|
asset, application, system_user, user = self.get_request_resource(serializer)
|
||||||
token = self.create_token(user, asset, application, system_user)
|
token, secret = self.create_token(user, asset, application, system_user)
|
||||||
return Response({"token": token}, status=201)
|
return Response({"id": token, 'secret': secret}, status=201)
|
||||||
|
|
||||||
def valid_token(self, token):
|
def valid_token(self, token):
|
||||||
from users.models import User
|
from users.models import User
|
||||||
|
|
|
@ -17,32 +17,35 @@ class JMSBaseAuthBackend:
|
||||||
def has_perm(self, user_obj, perm, obj=None):
|
def has_perm(self, user_obj, perm, obj=None):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# can authenticate
|
|
||||||
def username_can_authenticate(self, username):
|
|
||||||
return self.allow_authenticate(username=username)
|
|
||||||
|
|
||||||
def user_can_authenticate(self, user):
|
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)
|
is_valid = getattr(user, 'is_valid', None)
|
||||||
return is_valid or is_valid is None
|
return is_valid or is_valid is None
|
||||||
|
|
||||||
@property
|
# allow user to authenticate
|
||||||
def backend_path(self):
|
def username_allow_authenticate(self, username):
|
||||||
return f'{self.__module__}.{self.__class__.__name__}'
|
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):
|
def allow_authenticate(self, user=None, username=None):
|
||||||
if user:
|
if user:
|
||||||
allowed_backends = user.get_allowed_auth_backends()
|
allowed_backend_paths = user.get_allowed_auth_backend_paths()
|
||||||
else:
|
else:
|
||||||
allowed_backends = User.get_user_allowed_auth_backends(username)
|
allowed_backend_paths = User.get_user_allowed_auth_backend_paths(username)
|
||||||
if allowed_backends is None:
|
if allowed_backend_paths is None:
|
||||||
# 特殊值 None 表示没有限制
|
# 特殊值 None 表示没有限制
|
||||||
return True
|
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:
|
if not allow:
|
||||||
info = 'User {} skip authentication backend {}, because it not in {}'
|
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)
|
logger.debug(info)
|
||||||
return allow
|
return allow
|
||||||
|
|
||||||
|
|
|
@ -3,3 +3,4 @@
|
||||||
|
|
||||||
# 保证 utils 中的模块进行初始化
|
# 保证 utils 中的模块进行初始化
|
||||||
from . import utils
|
from . import utils
|
||||||
|
from .backends import *
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
from .backends import *
|
|
@ -28,6 +28,8 @@ from .signals import (
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
__all__ = ['OIDCAuthCodeBackend', 'OIDCAuthPasswordBackend']
|
||||||
|
|
||||||
|
|
||||||
class UserMixin:
|
class UserMixin:
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1,4 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
|
|
||||||
|
from .backends import *
|
||||||
|
|
|
@ -57,8 +57,8 @@ def authenticate(request=None, **credentials):
|
||||||
username = credentials.get('username')
|
username = credentials.get('username')
|
||||||
|
|
||||||
for backend, backend_path in _get_backends(return_tuples=True):
|
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
|
continue
|
||||||
|
|
||||||
# 原生
|
# 原生
|
||||||
|
@ -76,8 +76,8 @@ def authenticate(request=None, **credentials):
|
||||||
if user is None:
|
if user is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 再次检查遇检查中遗漏的用户
|
# 检查用户是否允许认证
|
||||||
if not backend.user_can_authenticate(user):
|
if not backend.user_allow_authenticate(user):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Annotate the user object with the path of the backend.
|
# Annotate the user object with the path of the backend.
|
||||||
|
|
|
@ -169,7 +169,7 @@ class ConnectionTokenAssetSerializer(serializers.ModelSerializer):
|
||||||
class ConnectionTokenSystemUserSerializer(serializers.ModelSerializer):
|
class ConnectionTokenSystemUserSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SystemUser
|
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):
|
class ConnectionTokenGatewaySerializer(serializers.ModelSerializer):
|
||||||
|
|
|
@ -184,9 +184,7 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_forgot_password_url():
|
def get_forgot_password_url():
|
||||||
forgot_password_url = reverse('authentication:forgot-password')
|
forgot_password_url = reverse('authentication:forgot-password')
|
||||||
has_other_auth_backend = settings.AUTHENTICATION_BACKENDS[1] != settings.AUTH_BACKEND_MODEL
|
forgot_password_url = settings.FORGOT_PASSWORD_URL or forgot_password_url
|
||||||
if has_other_auth_backend and settings.FORGOT_PASSWORD_URL:
|
|
||||||
forgot_password_url = settings.FORGOT_PASSWORD_URL
|
|
||||||
return forgot_password_url
|
return forgot_password_url
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
|
|
@ -145,20 +145,20 @@ TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
|
||||||
OTP_IN_RADIUS = CONFIG.OTP_IN_RADIUS
|
OTP_IN_RADIUS = CONFIG.OTP_IN_RADIUS
|
||||||
|
|
||||||
|
|
||||||
AUTH_BACKEND_MODEL = 'authentication.backends.base.JMSModelBackend'
|
|
||||||
RBAC_BACKEND = 'rbac.backends.RBACBackend'
|
RBAC_BACKEND = 'rbac.backends.RBACBackend'
|
||||||
|
AUTH_BACKEND_MODEL = 'authentication.backends.base.JMSModelBackend'
|
||||||
AUTH_BACKEND_PUBKEY = 'authentication.backends.pubkey.PublicKeyAuthBackend'
|
AUTH_BACKEND_PUBKEY = 'authentication.backends.pubkey.PublicKeyAuthBackend'
|
||||||
AUTH_BACKEND_LDAP = 'authentication.backends.ldap.LDAPAuthorizationBackend'
|
AUTH_BACKEND_LDAP = 'authentication.backends.ldap.LDAPAuthorizationBackend'
|
||||||
AUTH_BACKEND_OIDC_PASSWORD = 'authentication.backends.oidc.backends.OIDCAuthPasswordBackend'
|
AUTH_BACKEND_OIDC_PASSWORD = 'authentication.backends.oidc.OIDCAuthPasswordBackend'
|
||||||
AUTH_BACKEND_OIDC_CODE = 'authentication.backends.oidc.backends.OIDCAuthCodeBackend'
|
AUTH_BACKEND_OIDC_CODE = 'authentication.backends.oidc.OIDCAuthCodeBackend'
|
||||||
AUTH_BACKEND_RADIUS = 'authentication.backends.radius.RadiusBackend'
|
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_SSO = 'authentication.backends.sso.SSOAuthentication'
|
||||||
AUTH_BACKEND_WECOM = 'authentication.backends.sso.WeComAuthentication'
|
AUTH_BACKEND_WECOM = 'authentication.backends.sso.WeComAuthentication'
|
||||||
AUTH_BACKEND_DINGTALK = 'authentication.backends.sso.DingTalkAuthentication'
|
AUTH_BACKEND_DINGTALK = 'authentication.backends.sso.DingTalkAuthentication'
|
||||||
AUTH_BACKEND_FEISHU = 'authentication.backends.sso.FeiShuAuthentication'
|
AUTH_BACKEND_FEISHU = 'authentication.backends.sso.FeiShuAuthentication'
|
||||||
AUTH_BACKEND_AUTH_TOKEN = 'authentication.backends.sso.AuthorizationTokenAuthentication'
|
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 = [
|
AUTHENTICATION_BACKENDS = [
|
||||||
|
|
|
@ -36,7 +36,7 @@ class UserGroupGrantedAssetsApi(ListAPIView):
|
||||||
filterset_fields = ['hostname', 'ip', 'id', 'comment']
|
filterset_fields = ['hostname', 'ip', 'id', 'comment']
|
||||||
search_fields = ['hostname', 'ip', 'comment']
|
search_fields = ['hostname', 'ip', 'comment']
|
||||||
rbac_perms = {
|
rbac_perms = {
|
||||||
'list': 'perms.view_userassets'
|
'list': 'perms.view_usergroupassets',
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
@ -73,7 +73,7 @@ class UserGroupGrantedNodeAssetsApi(ListAPIView):
|
||||||
filterset_fields = ['hostname', 'ip', 'id', 'comment']
|
filterset_fields = ['hostname', 'ip', 'id', 'comment']
|
||||||
search_fields = ['hostname', 'ip', 'comment']
|
search_fields = ['hostname', 'ip', 'comment']
|
||||||
rbac_perms = {
|
rbac_perms = {
|
||||||
'list': 'perms.view_userassets'
|
'list': 'perms.view_usergroupassets',
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
@ -125,7 +125,7 @@ class UserGroupGrantedNodeAssetsApi(ListAPIView):
|
||||||
class UserGroupGrantedNodesApi(ListAPIView):
|
class UserGroupGrantedNodesApi(ListAPIView):
|
||||||
serializer_class = serializers.NodeGrantedSerializer
|
serializer_class = serializers.NodeGrantedSerializer
|
||||||
rbac_perms = {
|
rbac_perms = {
|
||||||
'list': 'perms.view_userassets'
|
'list': 'perms.view_usergroupassets',
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
@ -142,7 +142,8 @@ class UserGroupGrantedNodesApi(ListAPIView):
|
||||||
|
|
||||||
class UserGroupGrantedNodeChildrenAsTreeApi(SerializeToTreeNodeMixin, ListAPIView):
|
class UserGroupGrantedNodeChildrenAsTreeApi(SerializeToTreeNodeMixin, ListAPIView):
|
||||||
rbac_perms = {
|
rbac_perms = {
|
||||||
'list': 'perms.view_userassets'
|
'list': 'perms.view_usergroupassets',
|
||||||
|
'GET': 'perms.view_usergroupassets',
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_children_nodes(self, parent_key):
|
def get_children_nodes(self, parent_key):
|
||||||
|
|
|
@ -35,7 +35,8 @@ __all__ = [
|
||||||
class GetUserAssetPermissionActionsApi(RetrieveAPIView):
|
class GetUserAssetPermissionActionsApi(RetrieveAPIView):
|
||||||
serializer_class = serializers.ActionsSerializer
|
serializer_class = serializers.ActionsSerializer
|
||||||
rbac_perms = {
|
rbac_perms = {
|
||||||
'retrieve': 'perms.view_userassets'
|
'retrieve': 'perms.view_userassets',
|
||||||
|
'GET': 'perms.view_userassets',
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_user(self):
|
def get_user(self):
|
||||||
|
@ -114,23 +115,38 @@ class UserGrantedAssetSystemUsersForAdminApi(ListAPIView):
|
||||||
user_id = self.kwargs.get('pk')
|
user_id = self.kwargs.get('pk')
|
||||||
return User.objects.get(id=user_id)
|
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):
|
def get_asset_system_user_ids_with_actions(self, asset):
|
||||||
return get_asset_system_user_ids_with_actions_by_user(self.user, asset)
|
return get_asset_system_user_ids_with_actions_by_user(self.user, asset)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
asset_id = self.kwargs.get('asset_id')
|
system_user_ids = self.system_users_with_actions.keys()
|
||||||
asset = get_object_or_404(Asset, id=asset_id, is_active=True)
|
system_users = SystemUser.objects.filter(id__in=system_user_ids) \
|
||||||
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)\
|
|
||||||
.only(*self.serializer_class.Meta.only_fields) \
|
.only(*self.serializer_class.Meta.only_fields) \
|
||||||
.order_by('name')
|
.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
|
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')
|
@method_decorator(tmp_to_root_org(), name='list')
|
||||||
class MyGrantedAssetSystemUsersApi(UserGrantedAssetSystemUsersForAdminApi):
|
class MyGrantedAssetSystemUsersApi(UserGrantedAssetSystemUsersForAdminApi):
|
||||||
|
|
|
@ -46,7 +46,10 @@ class OrgRoleBindingViewSet(RoleBindingViewSet):
|
||||||
def perform_bulk_create(self, serializer):
|
def perform_bulk_create(self, serializer):
|
||||||
validated_data = serializer.validated_data
|
validated_data = serializer.validated_data
|
||||||
bindings = [
|
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
|
for d in validated_data
|
||||||
]
|
]
|
||||||
OrgRoleBinding.objects.bulk_create(bindings, ignore_conflicts=True)
|
OrgRoleBinding.objects.bulk_create(bindings, ignore_conflicts=True)
|
||||||
|
|
|
@ -12,7 +12,7 @@ class RBACBackend(JMSBaseAuthBackend):
|
||||||
def authenticate(self, *args, **kwargs):
|
def authenticate(self, *args, **kwargs):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def username_can_authenticate(self, username):
|
def username_allow_authenticate(self, username):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def has_perm(self, user_obj, perm, obj=None):
|
def has_perm(self, user_obj, perm, obj=None):
|
||||||
|
|
|
@ -51,7 +51,7 @@ exclude_permissions = (
|
||||||
('audits', 'userloginlog', 'change,delete,change', 'userloginlog'),
|
('audits', 'userloginlog', 'change,delete,change', 'userloginlog'),
|
||||||
('audits', 'ftplog', 'change,delete', 'ftplog'),
|
('audits', 'ftplog', 'change,delete', 'ftplog'),
|
||||||
('terminal', 'session', 'delete', 'session'),
|
('terminal', 'session', 'delete', 'session'),
|
||||||
('tickets', '*', '*', '*'),
|
('tickets', 'ticket', '*', '*'),
|
||||||
('users', 'userpasswordhistory', '*', '*'),
|
('users', 'userpasswordhistory', '*', '*'),
|
||||||
('xpack', 'interface', 'add,delete', 'interface'),
|
('xpack', 'interface', 'add,delete', 'interface'),
|
||||||
)
|
)
|
||||||
|
|
|
@ -27,7 +27,7 @@ class Migration(migrations.Migration):
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'Menu permission',
|
'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': [],
|
'default_permissions': [],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
@ -12,7 +12,7 @@ class MenuPermission(models.Model):
|
||||||
default_permissions = []
|
default_permissions = []
|
||||||
verbose_name = _('Menu permission')
|
verbose_name = _('Menu permission')
|
||||||
permissions = [
|
permissions = [
|
||||||
('view_adminview', _('Console view')),
|
('view_adminview', _('view console view')),
|
||||||
('view_auditview', _('Audit view')),
|
('view_auditview', _('view audit view')),
|
||||||
('view_userview', _('Workspace view')),
|
('view_userview', _('view workspace view')),
|
||||||
]
|
]
|
||||||
|
|
|
@ -90,7 +90,7 @@ class OrgRoleBindingManager(models.Manager):
|
||||||
if current_org.is_root():
|
if current_org.is_root():
|
||||||
return queryset.none()
|
return queryset.none()
|
||||||
|
|
||||||
queryset = queryset.filter(org=current_org.id)
|
queryset = queryset.filter(org=current_org.id, scope=Scope.org)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
@ -120,8 +120,7 @@ class OrgRoleBinding(RoleBinding):
|
||||||
|
|
||||||
class SystemRoleBindingManager(models.Manager):
|
class SystemRoleBindingManager(models.Manager):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = super().get_queryset().\
|
queryset = super().get_queryset().filter(scope=Scope.system)
|
||||||
filter(scope=Scope.org)
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -79,61 +79,9 @@ class Setting(models.Model):
|
||||||
item.refresh_setting()
|
item.refresh_setting()
|
||||||
|
|
||||||
def refresh_setting(self):
|
def refresh_setting(self):
|
||||||
if hasattr(self.__class__, f'refresh_{self.name}'):
|
setattr(settings, self.name, self.cleaned_value)
|
||||||
getattr(self.__class__, f'refresh_{self.name}')()
|
|
||||||
else:
|
|
||||||
setattr(settings, self.name, self.cleaned_value)
|
|
||||||
self.refresh_keycloak_to_openid_if_need()
|
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):
|
def refresh_keycloak_to_openid_if_need(self):
|
||||||
watch_config_names = [
|
watch_config_names = [
|
||||||
'AUTH_OPENID', 'AUTH_OPENID_REALM_NAME', 'AUTH_OPENID_SERVER_URL',
|
'AUTH_OPENID', 'AUTH_OPENID_REALM_NAME', 'AUTH_OPENID_SERVER_URL',
|
||||||
|
@ -170,10 +118,6 @@ class Setting(models.Model):
|
||||||
setattr(settings, key, value)
|
setattr(settings, key, value)
|
||||||
self.__class__.update_or_create(key, value, encrypted=False, category=self.category)
|
self.__class__.update_or_create(key, value, encrypted=False, category=self.category)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def refresh_AUTH_RADIUS(cls):
|
|
||||||
cls.refresh_authentications('AUTH_RADIUS')
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_or_create(cls, name='', value='', encrypted=False, category=''):
|
def update_or_create(cls, name='', value='', encrypted=False, category=''):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
|
|
||||||
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
|
from django.utils import timezone
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from rest_framework import viewsets, generics
|
from rest_framework import viewsets, generics
|
||||||
from rest_framework.views import Response
|
from rest_framework.views import Response
|
||||||
|
@ -28,14 +30,24 @@ class StatusViewSet(viewsets.ModelViewSet):
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
self.handle_sessions()
|
self.handle_sessions()
|
||||||
self.perform_create(serializer)
|
self.perform_create(serializer)
|
||||||
tasks = self.request.user.terminal.task_set.filter(is_finished=False)
|
task_serializer = self.get_task_serializer()
|
||||||
serializer = self.task_serializer_class(tasks, many=True)
|
return Response(task_serializer.data, status=201)
|
||||||
return Response(serializer.data, status=201)
|
|
||||||
|
|
||||||
def handle_sessions(self):
|
def handle_sessions(self):
|
||||||
session_ids = self.request.data.get('sessions', [])
|
session_ids = self.request.data.get('sessions', [])
|
||||||
Session.set_sessions_active(session_ids)
|
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):
|
def get_queryset(self):
|
||||||
terminal_id = self.kwargs.get("terminal", None)
|
terminal_id = self.kwargs.get("terminal", None)
|
||||||
if terminal_id:
|
if terminal_id:
|
||||||
|
@ -43,11 +55,6 @@ class StatusViewSet(viewsets.ModelViewSet):
|
||||||
return terminal.status_set.all()
|
return terminal.status_set.all()
|
||||||
return super().get_queryset()
|
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):
|
class ComponentsMetricsAPIView(generics.GenericAPIView):
|
||||||
""" 返回汇总组件指标数据 """
|
""" 返回汇总组件指标数据 """
|
||||||
|
|
|
@ -17,6 +17,7 @@ class ReplayStorageTypeChoices(TextChoices):
|
||||||
oss = 'oss', 'OSS'
|
oss = 'oss', 'OSS'
|
||||||
azure = 'azure', 'Azure'
|
azure = 'azure', 'Azure'
|
||||||
obs = 'obs', 'OBS'
|
obs = 'obs', 'OBS'
|
||||||
|
cos = 'cos', 'COS'
|
||||||
|
|
||||||
|
|
||||||
class CommandStorageTypeChoices(TextChoices):
|
class CommandStorageTypeChoices(TextChoices):
|
||||||
|
@ -47,6 +48,7 @@ class TerminalTypeChoices(TextChoices):
|
||||||
lion = 'lion', 'Lion'
|
lion = 'lion', 'Lion'
|
||||||
core = 'core', 'Core'
|
core = 'core', 'Core'
|
||||||
celery = 'celery', 'Celery'
|
celery = 'celery', 'Celery'
|
||||||
|
magnus = 'magnus', 'Magnus'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def types(cls):
|
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'
|
ST = 'ST', 'SSH Terminal'
|
||||||
RT = 'RT', 'RDP Terminal'
|
RT = 'RT', 'RDP Terminal'
|
||||||
WT = 'WT', 'Web Terminal'
|
WT = 'WT', 'Web Terminal'
|
||||||
|
DT = 'DT', 'DB Terminal'
|
||||||
|
|
||||||
class PROTOCOL(TextChoices):
|
class PROTOCOL(TextChoices):
|
||||||
SSH = 'ssh', 'ssh'
|
SSH = 'ssh', 'ssh'
|
||||||
|
|
|
@ -42,8 +42,8 @@ class ReplayStorageTypeBaseSerializer(serializers.Serializer):
|
||||||
|
|
||||||
class ReplayStorageTypeS3Serializer(ReplayStorageTypeBaseSerializer):
|
class ReplayStorageTypeS3Serializer(ReplayStorageTypeBaseSerializer):
|
||||||
endpoint_help_text = '''
|
endpoint_help_text = '''
|
||||||
S3 format: http://s3.{REGION_NAME}.amazonaws.com
|
S3 format: http://s3.{REGION_NAME}.amazonaws.com <br>
|
||||||
S3(China) format: http://s3.{REGION_NAME}.amazonaws.com.cn
|
S3(China) format: http://s3.{REGION_NAME}.amazonaws.com.cn <br>
|
||||||
Such as: http://s3.cn-north-1.amazonaws.com.cn
|
Such as: http://s3.cn-north-1.amazonaws.com.cn
|
||||||
'''
|
'''
|
||||||
ENDPOINT = serializers.CharField(
|
ENDPOINT = serializers.CharField(
|
||||||
|
@ -73,7 +73,7 @@ class ReplayStorageTypeSwiftSerializer(ReplayStorageTypeBaseSerializer):
|
||||||
|
|
||||||
class ReplayStorageTypeOSSSerializer(ReplayStorageTypeBaseSerializer):
|
class ReplayStorageTypeOSSSerializer(ReplayStorageTypeBaseSerializer):
|
||||||
endpoint_help_text = '''
|
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
|
Such as: http://oss-cn-hangzhou.aliyuncs.com
|
||||||
'''
|
'''
|
||||||
ENDPOINT = serializers.CharField(
|
ENDPOINT = serializers.CharField(
|
||||||
|
@ -84,7 +84,7 @@ class ReplayStorageTypeOSSSerializer(ReplayStorageTypeBaseSerializer):
|
||||||
|
|
||||||
class ReplayStorageTypeOBSSerializer(ReplayStorageTypeBaseSerializer):
|
class ReplayStorageTypeOBSSerializer(ReplayStorageTypeBaseSerializer):
|
||||||
endpoint_help_text = '''
|
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
|
Such as: obs.cn-north-4.myhuaweicloud.com
|
||||||
'''
|
'''
|
||||||
ENDPOINT = serializers.CharField(
|
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 ReplayStorageTypeAzureSerializer(serializers.Serializer):
|
||||||
class EndpointSuffixChoices(TextChoices):
|
class EndpointSuffixChoices(TextChoices):
|
||||||
china = 'core.chinacloudapi.cn', 'core.chinacloudapi.cn'
|
china = 'core.chinacloudapi.cn', 'core.chinacloudapi.cn'
|
||||||
|
@ -116,7 +125,8 @@ replay_storage_type_serializer_classes_mapping = {
|
||||||
const.ReplayStorageTypeChoices.swift.value: ReplayStorageTypeSwiftSerializer,
|
const.ReplayStorageTypeChoices.swift.value: ReplayStorageTypeSwiftSerializer,
|
||||||
const.ReplayStorageTypeChoices.oss.value: ReplayStorageTypeOSSSerializer,
|
const.ReplayStorageTypeChoices.oss.value: ReplayStorageTypeOSSSerializer,
|
||||||
const.ReplayStorageTypeChoices.azure.value: ReplayStorageTypeAzureSerializer,
|
const.ReplayStorageTypeChoices.azure.value: ReplayStorageTypeAzureSerializer,
|
||||||
const.ReplayStorageTypeChoices.obs.value: ReplayStorageTypeOBSSerializer
|
const.ReplayStorageTypeChoices.obs.value: ReplayStorageTypeOBSSerializer,
|
||||||
|
const.ReplayStorageTypeChoices.cos.value: ReplayStorageTypeCOSSerializer
|
||||||
}
|
}
|
||||||
|
|
||||||
# Command storage serializers
|
# Command storage serializers
|
||||||
|
@ -143,7 +153,7 @@ def command_storage_es_host_format_validator(host):
|
||||||
class CommandStorageTypeESSerializer(serializers.Serializer):
|
class CommandStorageTypeESSerializer(serializers.Serializer):
|
||||||
|
|
||||||
hosts_help_text = '''
|
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)
|
(eg: http://www.jumpserver.a.com:9100, http://www.jumpserver.b.com:9100)
|
||||||
'''
|
'''
|
||||||
HOSTS = serializers.ListField(
|
HOSTS = serializers.ListField(
|
||||||
|
|
|
@ -14,7 +14,7 @@ from common.utils import get_log_keep_day
|
||||||
from ops.celery.decorator import (
|
from ops.celery.decorator import (
|
||||||
register_as_period_task, after_app_ready_start, after_app_shutdown_clean_periodic
|
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 .backends import server_replay_storage
|
||||||
from .utils import find_session_replay_local
|
from .utils import find_session_replay_local
|
||||||
|
|
||||||
|
@ -39,6 +39,11 @@ def delete_terminal_status_period():
|
||||||
def clean_orphan_session():
|
def clean_orphan_session():
|
||||||
active_sessions = Session.objects.filter(is_finished=False)
|
active_sessions = Session.objects.filter(is_finished=False)
|
||||||
for session in active_sessions:
|
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():
|
if session.is_active():
|
||||||
continue
|
continue
|
||||||
session.is_finished = True
|
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)
|
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"))
|
title = models.CharField(max_length=256, verbose_name=_("Title"))
|
||||||
type = models.CharField(
|
type = models.CharField(
|
||||||
max_length=64, choices=TicketType.choices,
|
max_length=64, choices=TicketType.choices,
|
||||||
|
@ -102,31 +176,6 @@ class Ticket(CommonModelMixin, OrgModelMixin):
|
||||||
def type_login_confirm(self):
|
def type_login_confirm(self):
|
||||||
return self.type == TicketType.login_confirm.value
|
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
|
@property
|
||||||
def current_node(self):
|
def current_node(self):
|
||||||
return self.ticket_steps.filter(level=self.approval_step)
|
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()
|
processor = self.current_node.first().ticket_assignees.exclude(state=ProcessStatus.notified).first()
|
||||||
return processor.assignee if processor else None
|
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):
|
def create_related_node(self):
|
||||||
org_id = self.flow.org_id
|
org_id = self.flow.org_id
|
||||||
approval_rule = self.get_current_ticket_flow_approve()
|
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))
|
ticket_assignees.append(TicketAssignee(step=ticket_step, assignee=assignee))
|
||||||
TicketAssignee.objects.bulk_create(ticket_assignees)
|
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):
|
def has_current_assignee(self, assignee):
|
||||||
return self.ticket_steps.filter(ticket_assignees__assignee=assignee, level=self.approval_step).exists()
|
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 JMSException(detail=_('Please try again'), code='please_try_again')
|
||||||
|
|
||||||
raise e
|
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()
|
return super(User, self).delete()
|
||||||
|
|
||||||
@classmethod
|
@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:
|
if not settings.ONLY_ALLOW_AUTH_FROM_SOURCE or not username:
|
||||||
return None
|
return None
|
||||||
user = cls.objects.filter(username=username).first()
|
user = cls.objects.filter(username=username).first()
|
||||||
if not user:
|
if not user:
|
||||||
return None
|
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:
|
if not settings.ONLY_ALLOW_AUTH_FROM_SOURCE:
|
||||||
return None
|
return None
|
||||||
return self.SOURCE_BACKEND_MAPPING.get(self.source, [])
|
return self.SOURCE_BACKEND_MAPPING.get(self.source, [])
|
||||||
|
|
|
@ -55,14 +55,13 @@ pycparser==2.19
|
||||||
pycryptodome==3.12.0
|
pycryptodome==3.12.0
|
||||||
pycryptodomex==3.12.0
|
pycryptodomex==3.12.0
|
||||||
pyotp==2.2.6
|
pyotp==2.2.6
|
||||||
PyNaCl==1.2.1
|
PyNaCl==1.5.0
|
||||||
python-dateutil==2.8.2
|
python-dateutil==2.8.2
|
||||||
#python-gssapi==0.6.4
|
|
||||||
pytz==2018.3
|
pytz==2018.3
|
||||||
PyYAML==6.0
|
PyYAML==6.0
|
||||||
redis==3.5.3
|
redis==3.5.3
|
||||||
requests==2.25.1
|
requests==2.25.1
|
||||||
jms-storage==0.0.41
|
jms-storage==0.0.42
|
||||||
s3transfer==0.5.0
|
s3transfer==0.5.0
|
||||||
simplejson==3.13.2
|
simplejson==3.13.2
|
||||||
six==1.11.0
|
six==1.11.0
|
||||||
|
@ -128,3 +127,5 @@ kubernetes==21.7.0
|
||||||
websocket-client==1.2.3
|
websocket-client==1.2.3
|
||||||
numpy==1.22.0
|
numpy==1.22.0
|
||||||
pandas==1.3.5
|
pandas==1.3.5
|
||||||
|
pyjwkest==1.4.2
|
||||||
|
jsonfield2==4.0.0.post0
|
||||||
|
|
Loading…
Reference in New Issue