Merge pull request #5754 from jumpserver/dev

v2.8 发版 (rc3)
pull/5813/head
老广 2021-03-15 19:59:31 +08:00 committed by GitHub
commit 957d3660ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 121 additions and 54 deletions

View File

@ -22,7 +22,7 @@ from common.utils.common import timeit
from common.db.models import output_as_string from common.db.models import output_as_string
from common.utils import get_logger from common.utils import get_logger
from orgs.mixins.models import OrgModelMixin, OrgManager from orgs.mixins.models import OrgModelMixin, OrgManager
from orgs.utils import get_current_org, tmp_to_org from orgs.utils import get_current_org, tmp_to_org, tmp_to_root_org
from orgs.models import Organization from orgs.models import Organization
@ -462,22 +462,42 @@ class NodeAssetsMixin(NodeAllAssetsMappingMixin):
class SomeNodesMixin: class SomeNodesMixin:
key = '' key = ''
default_key = '1' default_key = '1'
default_value = 'Default'
empty_key = '-11' empty_key = '-11'
empty_value = _("empty") empty_value = _("empty")
@classmethod
def correct_default_node_if_need(cls):
with tmp_to_root_org():
wrong_default_org = cls.objects.filter(key='1', value='Default').first()
if not wrong_default_org:
return
if wrong_default_org.has_children_or_has_assets():
return
default_org = Organization.default()
right_default_org = cls.objects.filter(value=default_org.name).first()
if not right_default_org:
return
if right_default_org.date_create > wrong_default_org.date_create:
return
with atomic():
logger.warn(f'Correct default node: '
f'old={wrong_default_org.value}-{wrong_default_org.key} '
f'new={right_default_org.value}-{right_default_org.key}')
wrong_default_org.delete()
right_default_org.key = '1'
right_default_org.save()
@classmethod @classmethod
def default_node(cls): def default_node(cls):
cls.correct_default_node_if_need()
default_org = Organization.default() default_org = Organization.default()
with tmp_to_org(default_org): with tmp_to_org(default_org):
defaults = {'value': default_org.name} defaults = {'value': default_org.name}
try:
obj, created = cls.objects.get_or_create(
defaults=defaults, key=cls.default_key,
)
except IntegrityError as e:
logger.error("Create default node failed: {}".format(e))
cls.modify_other_org_root_node_key()
obj, created = cls.objects.get_or_create( obj, created = cls.objects.get_or_create(
defaults=defaults, key=cls.default_key, defaults=defaults, key=cls.default_key,
) )
@ -500,7 +520,7 @@ class SomeNodesMixin:
if not org_nodes_roots_keys: if not org_nodes_roots_keys:
org_nodes_roots_keys = ['1'] org_nodes_roots_keys = ['1']
max_key = max([int(k) for k in org_nodes_roots_keys]) max_key = max([int(k) for k in org_nodes_roots_keys])
key = str(max_key + 1) if max_key != 0 else '2' key = str(max_key + 1) if max_key > 0 else '2'
return key return key
@classmethod @classmethod

View File

@ -23,6 +23,7 @@ class AdminUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
read_only_fields = ['date_created', 'date_updated', 'created_by', 'assets_amount'] read_only_fields = ['date_created', 'date_updated', 'created_by', 'assets_amount']
extra_kwargs = { extra_kwargs = {
'username': {"required": True},
'password': {"write_only": True}, 'password': {"write_only": True},
'private_key': {"write_only": True}, 'private_key': {"write_only": True},
'public_key': {"write_only": True}, 'public_key': {"write_only": True},

View File

@ -1,6 +1,5 @@
from importlib import import_module from importlib import import_module
from django.contrib.auth import BACKEND_SESSION_KEY
from django.conf import settings from django.conf import settings
from django.contrib.auth import user_logged_in from django.contrib.auth import user_logged_in
from django.core.cache import cache from django.core.cache import cache
@ -25,17 +24,17 @@ def on_user_auth_login_success(sender, user, request, **kwargs):
@receiver(openid_user_login_success) @receiver(openid_user_login_success)
def on_oidc_user_login_success(sender, request, user, create=False, **kwargs): def on_oidc_user_login_success(sender, request, user, create=False, **kwargs):
request.session[BACKEND_SESSION_KEY] = 'OIDCAuthCodeBackend' request.session['auth_backend'] = settings.AUTH_BACKEND_OIDC_CODE
post_auth_success.send(sender, user=user, request=request) post_auth_success.send(sender, user=user, request=request)
@receiver(openid_user_login_failed) @receiver(openid_user_login_failed)
def on_oidc_user_login_failed(sender, username, request, reason, **kwargs): def on_oidc_user_login_failed(sender, username, request, reason, **kwargs):
request.session[BACKEND_SESSION_KEY] = 'OIDCAuthCodeBackend' request.session['auth_backend'] = settings.AUTH_BACKEND_OIDC_CODE
post_auth_failed.send(sender, username=username, request=request, reason=reason) post_auth_failed.send(sender, username=username, request=request, reason=reason)
@receiver(cas_user_authenticated) @receiver(cas_user_authenticated)
def on_cas_user_login_success(sender, request, user, **kwargs): def on_cas_user_login_success(sender, request, user, **kwargs):
request.session[BACKEND_SESSION_KEY] = 'CASBackend' request.session['auth_backend'] = settings.AUTH_BACKEND_CAS
post_auth_success.send(sender, user=user, request=request) post_auth_success.send(sender, user=user, request=request)

View File

@ -109,9 +109,9 @@ class PermissionsMixin(UserPassesTestMixin):
return True return True
class UserCanUseCurrentOrg(permissions.BasePermission): class UserCanAnyPermCurrentOrg(permissions.BasePermission):
def has_permission(self, request, view): def has_permission(self, request, view):
return current_org.can_use_by(request.user) return current_org.can_any_by(request.user)
class UserCanUpdatePassword(permissions.BasePermission): class UserCanUpdatePassword(permissions.BasePermission):

View File

@ -8,7 +8,7 @@ from rest_framework_bulk import BulkModelViewSet
from rest_framework.generics import RetrieveAPIView from rest_framework.generics import RetrieveAPIView
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from common.permissions import IsSuperUserOrAppUser, IsValidUser, UserCanUseCurrentOrg from common.permissions import IsSuperUserOrAppUser, IsValidUser, UserCanAnyPermCurrentOrg
from common.drf.api import JMSBulkRelationModelViewSet from common.drf.api import JMSBulkRelationModelViewSet
from .models import Organization, ROLE from .models import Organization, ROLE
from .serializers import ( from .serializers import (
@ -18,9 +18,13 @@ from .serializers import (
CurrentOrgSerializer CurrentOrgSerializer
) )
from users.models import User, UserGroup from users.models import User, UserGroup
from assets.models import Asset, Domain, AdminUser, SystemUser, Label from assets.models import (
from perms.models import AssetPermission Asset, Domain, AdminUser, SystemUser, Label, Node, Gateway,
from orgs.utils import current_org CommandFilter, CommandFilterRule, GatheredUser
)
from applications.models import Application
from perms.models import AssetPermission, ApplicationPermission
from orgs.utils import current_org, tmp_to_root_org
from common.utils import get_logger from common.utils import get_logger
from .filters import OrgMemberRelationFilterSet from .filters import OrgMemberRelationFilterSet
from .models import OrganizationMember from .models import OrganizationMember
@ -29,6 +33,15 @@ from .models import OrganizationMember
logger = get_logger(__file__) logger = get_logger(__file__)
# 部分 org 相关的 model需要清空这些数据之后才能删除该组织
org_related_models = [
User, UserGroup, Asset, Label, Domain, Gateway, Node, AdminUser, SystemUser, Label,
CommandFilter, CommandFilterRule, GatheredUser,
AssetPermission, ApplicationPermission,
Application,
]
class OrgViewSet(BulkModelViewSet): class OrgViewSet(BulkModelViewSet):
filterset_fields = ('name',) filterset_fields = ('name',)
search_fields = ('name', 'comment') search_fields = ('name', 'comment')
@ -44,24 +57,23 @@ class OrgViewSet(BulkModelViewSet):
} }
return mapper.get(self.action, super().get_serializer_class()) return mapper.get(self.action, super().get_serializer_class())
@tmp_to_root_org()
def get_data_from_model(self, model): def get_data_from_model(self, model):
if model == User: if model == User:
data = model.objects.filter(orgs__id=self.org.id, m2m_org_members__role=ROLE.USER) data = model.objects.filter(orgs__id=self.org.id, m2m_org_members__role=ROLE.USER)
elif model == Node:
# 跟节点不能手动删除,所以排除检查
data = model.objects.filter(org_id=self.org.id).exclude(parent_key='', key__regex=r'^[0-9]+$')
else: else:
data = model.objects.filter(org_id=self.org.id) data = model.objects.filter(org_id=self.org.id)
return data return data
def destroy(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs):
self.org = self.get_object() self.org = self.get_object()
models = [ for model in org_related_models:
User, UserGroup,
Asset, Domain, AdminUser, SystemUser, Label,
AssetPermission,
]
for model in models:
data = self.get_data_from_model(model) data = self.get_data_from_model(model)
if data: if data:
msg = _('Organization contains undeleted resources') msg = _(f'Have `{model._meta.verbose_name}` exists, Please delete')
return Response(data={'error': msg}, status=status.HTTP_403_FORBIDDEN) return Response(data={'error': msg}, status=status.HTTP_403_FORBIDDEN)
else: else:
if str(current_org) == str(self.org): if str(current_org) == str(self.org):
@ -136,7 +148,7 @@ class OrgMemberUserRelationBulkViewSet(JMSBulkRelationModelViewSet):
class CurrentOrgDetailApi(RetrieveAPIView): class CurrentOrgDetailApi(RetrieveAPIView):
serializer_class = CurrentOrgSerializer serializer_class = CurrentOrgSerializer
permission_classes = (IsValidUser, UserCanUseCurrentOrg) permission_classes = (IsValidUser, UserCanAnyPermCurrentOrg)
def get_object(self): def get_object(self):
return current_org return current_org

View File

@ -51,7 +51,14 @@ class Organization(models.Model):
def get_instance_from_memory(cls, id_or_name): def get_instance_from_memory(cls, id_or_name):
if not isinstance(cls.orgs_mapping, dict): if not isinstance(cls.orgs_mapping, dict):
cls.orgs_mapping = cls.construct_orgs_mapping() cls.orgs_mapping = cls.construct_orgs_mapping()
return cls.orgs_mapping.get(str(id_or_name))
org = cls.orgs_mapping.get(str(id_or_name))
if not org:
# 内存失效速度慢于读取速度(on_org_create_or_update)
cls.orgs_mapping = cls.construct_orgs_mapping()
org = cls.orgs_mapping.get(str(id_or_name))
return org
@classmethod @classmethod
def construct_orgs_mapping(cls): def construct_orgs_mapping(cls):
@ -111,6 +118,8 @@ class Organization(models.Model):
def can_audit_by(self, user): def can_audit_by(self, user):
if user.is_superuser or user.is_super_auditor: if user.is_superuser or user.is_super_auditor:
return True return True
if self.can_admin_by(user):
return True
if self.auditors.filter(id=user.id).exists(): if self.auditors.filter(id=user.id).exists():
return True return True
return False return False
@ -118,10 +127,17 @@ class Organization(models.Model):
def can_use_by(self, user): def can_use_by(self, user):
if user.is_superuser or user.is_super_auditor: if user.is_superuser or user.is_super_auditor:
return True return True
if self.can_audit_by(user):
return True
if self.users.filter(id=user.id).exists(): if self.users.filter(id=user.id).exists():
return True return True
return False return False
def can_any_by(self, user):
if user.is_superuser or user.is_super_auditor:
return True
return self.members.filter(id=user.id).exists()
@classmethod @classmethod
def get_user_orgs_by_role(cls, user, role): def get_user_orgs_by_role(cls, user, role):
if not isinstance(role, (tuple, list)): if not isinstance(role, (tuple, list)):

View File

@ -7,7 +7,8 @@ from functools import partial
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.functional import LazyObject from django.utils.functional import LazyObject
from django.db.models.signals import m2m_changed from django.db.models.signals import m2m_changed
from django.db.models.signals import post_save, post_delete from django.db.models.signals import post_save, post_delete, pre_delete
from django.utils.translation import ugettext as _
from orgs.utils import tmp_to_org from orgs.utils import tmp_to_org
from orgs.models import Organization, OrganizationMember from orgs.models import Organization, OrganizationMember
@ -18,6 +19,7 @@ from common.const.signals import PRE_REMOVE, POST_REMOVE
from common.signals import django_ready from common.signals import django_ready
from common.utils import get_logger from common.utils import get_logger
from common.utils.connection import RedisPubSub from common.utils.connection import RedisPubSub
from common.exceptions import JMSException
logger = get_logger(__file__) logger = get_logger(__file__)
@ -75,6 +77,15 @@ def on_org_delete(sender, **kwargs):
expire_orgs_mapping_for_memory() expire_orgs_mapping_for_memory()
@receiver(pre_delete, sender=Organization)
def on_org_delete(sender, instance, **kwargs):
# 删除该组织下所有 节点
with tmp_to_org(instance):
root_node = Node.org_root()
if root_node:
root_node.delete()
def _remove_users(model, users, org): def _remove_users(model, users, org):
with tmp_to_org(org): with tmp_to_org(org):
if not isinstance(users, (tuple, list, set)): if not isinstance(users, (tuple, list, set)):

View File

@ -98,12 +98,9 @@ class PublicSettingApi(generics.RetrieveAPIView):
def get_xpack_license_is_valid(): def get_xpack_license_is_valid():
if not settings.XPACK_ENABLED: if not settings.XPACK_ENABLED:
return False return False
try:
from xpack.plugins.license.models import License from xpack.plugins.license.models import License
return License.has_valid_license() return License.has_valid_license()
except Exception as e:
logger.error(e)
return False
@staticmethod @staticmethod
def get_login_title(): def get_login_title():

View File

@ -10,7 +10,7 @@ from django_filters import utils
from terminal import const from terminal import const
from common.const.http import GET from common.const.http import GET
from common.permissions import IsSuperUser from common.permissions import IsSuperUser, IsOrgAuditor
from terminal.filters import CommandStorageFilter, CommandFilter, CommandFilterForStorageTree from terminal.filters import CommandStorageFilter, CommandFilter, CommandFilterForStorageTree
from ..models import CommandStorage, ReplayStorage from ..models import CommandStorage, ReplayStorage
from ..serializers import CommandStorageSerializer, ReplayStorageSerializer from ..serializers import CommandStorageSerializer, ReplayStorageSerializer
@ -42,7 +42,7 @@ class CommandStorageViewSet(BaseStorageViewSetMixin, viewsets.ModelViewSet):
permission_classes = (IsSuperUser,) permission_classes = (IsSuperUser,)
filterset_class = CommandStorageFilter filterset_class = CommandStorageFilter
@action(methods=[GET], detail=False, filterset_class=CommandFilterForStorageTree) @action(methods=[GET], detail=False, permission_classes=(IsOrgAuditor, ), filterset_class=CommandFilterForStorageTree)
def tree(self, request: Request): def tree(self, request: Request):
storage_qs = self.get_queryset().exclude(name='null') storage_qs = self.get_queryset().exclude(name='null')
storages_with_count = [] storages_with_count = []

View File

@ -2,7 +2,7 @@
from django.core.cache import cache from django.core.cache import cache
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from rest_framework.decorators import action from rest_framework.decorators import action
from django.conf import settings
from rest_framework import generics from rest_framework import generics
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet from rest_framework_bulk import BulkModelViewSet
@ -88,16 +88,13 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet):
def get_permissions(self): def get_permissions(self):
if self.action in ["retrieve", "list"]: if self.action in ["retrieve", "list"]:
self.permission_classes = (IsOrgAdminOrAppUser,)
if self.request.query_params.get('all'): if self.request.query_params.get('all'):
self.permission_classes = (IsSuperUser,) self.permission_classes = (IsSuperUser,)
return super().get_permissions()
def perform_destroy(self, instance):
if not current_org.is_root():
instance.remove()
else: else:
return super().perform_destroy(instance) self.permission_classes = (IsOrgAdminOrAppUser,)
elif self.action in ['destroy']:
self.permission_classes = (IsSuperUser,)
return super().get_permissions()
def perform_bulk_destroy(self, objects): def perform_bulk_destroy(self, objects):
for obj in objects: for obj in objects:
@ -164,6 +161,21 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet):
OrganizationMember.objects.bulk_create(relations, ignore_conflicts=True) OrganizationMember.objects.bulk_create(relations, ignore_conflicts=True)
return Response(serializer.data, status=201) return Response(serializer.data, status=201)
@action(methods=['post'], detail=True, permission_classes=(IsOrgAdmin,))
def remove(self, request, *args, **kwargs):
instance = self.get_object()
instance.remove()
return Response(status=204)
@action(methods=['post'], detail=False, permission_classes=(IsOrgAdmin,), url_path='remove')
def bulk_remove(self, request, *args, **kwargs):
qs = self.get_queryset()
filtered = self.filter_queryset(qs)
for instance in filtered:
instance.remove()
return Response(status=204)
class UserChangePasswordApi(UserQuerysetMixin, generics.RetrieveUpdateAPIView): class UserChangePasswordApi(UserQuerysetMixin, generics.RetrieveUpdateAPIView):
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)

View File

@ -11,6 +11,8 @@ from .user import UserSerializer
class UserOrgSerializer(serializers.Serializer): class UserOrgSerializer(serializers.Serializer):
id = serializers.CharField() id = serializers.CharField()
name = serializers.CharField() name = serializers.CharField()
is_default = serializers.BooleanField(read_only=True)
is_root = serializers.BooleanField(read_only=True)
class UserOrgLabelSerializer(serializers.Serializer): class UserOrgLabelSerializer(serializers.Serializer):

View File

@ -51,9 +51,6 @@ class UserOtpEnableInstallAppView(TemplateView):
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
class UserOtpEnableBindView(AuthMixin, TemplateView, FormView): class UserOtpEnableBindView(AuthMixin, TemplateView, FormView):
template_name = 'users/user_otp_enable_bind.html' template_name = 'users/user_otp_enable_bind.html'
form_class = forms.UserCheckOtpCodeForm form_class = forms.UserCheckOtpCodeForm

2
jms
View File

@ -28,7 +28,7 @@ try:
except ImportError as e: except ImportError as e:
print("Not found __version__: {}".format(e)) print("Not found __version__: {}".format(e))
print("Python is: ") print("Python is: ")
logging.info(subprocess.call('which python', shell=True)) logging.info(sys.executable)
__version__ = 'Unknown' __version__ = 'Unknown'
sys.exit(1) sys.exit(1)