* 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
Jiangjie.Bai 2022-02-28 19:28:58 +08:00 committed by GitHub
parent edfca5eb24
commit 03afa4f974
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 294 additions and 205 deletions

View File

@ -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()

View File

@ -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',

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -3,3 +3,4 @@
# 保证 utils 中的模块进行初始化 # 保证 utils 中的模块进行初始化
from . import utils from . import utils
from .backends import *

View File

@ -0,0 +1 @@
from .backends import *

View File

@ -28,6 +28,8 @@ from .signals import (
logger = get_logger(__file__) logger = get_logger(__file__)
__all__ = ['OIDCAuthCodeBackend', 'OIDCAuthPasswordBackend']
class UserMixin: class UserMixin:

View File

@ -1,2 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from .backends import *

View File

@ -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.

View File

@ -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):

View File

@ -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):

View File

@ -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 = [

View File

@ -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):

View File

@ -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):

View File

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

View File

@ -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):

View File

@ -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'),
) )

View File

@ -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': [],
}, },
), ),

View File

@ -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')),
] ]

View File

@ -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

View File

@ -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=''):
""" """

View File

@ -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):
""" 返回汇总组件指标数据 """ """ 返回汇总组件指标数据 """

View File

@ -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):

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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'

View File

@ -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(

View File

@ -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

View File

@ -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',),
),
]

View File

@ -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")

View File

@ -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, [])

View File

@ -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