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.utils import get_logger
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
@ -462,25 +462,45 @@ class NodeAssetsMixin(NodeAllAssetsMappingMixin):
class SomeNodesMixin:
key = ''
default_key = '1'
default_value = 'Default'
empty_key = '-11'
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
def default_node(cls):
cls.correct_default_node_if_need()
default_org = Organization.default()
with tmp_to_org(default_org):
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(
defaults=defaults, key=cls.default_key,
)
obj, created = cls.objects.get_or_create(
defaults=defaults, key=cls.default_key,
)
return obj
def is_default_node(self):
@ -500,7 +520,7 @@ class SomeNodesMixin:
if not org_nodes_roots_keys:
org_nodes_roots_keys = ['1']
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
@classmethod

View File

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

View File

@ -1,6 +1,5 @@
from importlib import import_module
from django.contrib.auth import BACKEND_SESSION_KEY
from django.conf import settings
from django.contrib.auth import user_logged_in
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)
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)
@receiver(openid_user_login_failed)
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)
@receiver(cas_user_authenticated)
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)

View File

@ -109,9 +109,9 @@ class PermissionsMixin(UserPassesTestMixin):
return True
class UserCanUseCurrentOrg(permissions.BasePermission):
class UserCanAnyPermCurrentOrg(permissions.BasePermission):
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):

View File

@ -8,7 +8,7 @@ from rest_framework_bulk import BulkModelViewSet
from rest_framework.generics import RetrieveAPIView
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 .models import Organization, ROLE
from .serializers import (
@ -18,9 +18,13 @@ from .serializers import (
CurrentOrgSerializer
)
from users.models import User, UserGroup
from assets.models import Asset, Domain, AdminUser, SystemUser, Label
from perms.models import AssetPermission
from orgs.utils import current_org
from assets.models import (
Asset, Domain, AdminUser, SystemUser, Label, Node, Gateway,
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 .filters import OrgMemberRelationFilterSet
from .models import OrganizationMember
@ -29,6 +33,15 @@ from .models import OrganizationMember
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):
filterset_fields = ('name',)
search_fields = ('name', 'comment')
@ -44,24 +57,23 @@ class OrgViewSet(BulkModelViewSet):
}
return mapper.get(self.action, super().get_serializer_class())
@tmp_to_root_org()
def get_data_from_model(self, model):
if model == 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:
data = model.objects.filter(org_id=self.org.id)
return data
def destroy(self, request, *args, **kwargs):
self.org = self.get_object()
models = [
User, UserGroup,
Asset, Domain, AdminUser, SystemUser, Label,
AssetPermission,
]
for model in models:
for model in org_related_models:
data = self.get_data_from_model(model)
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)
else:
if str(current_org) == str(self.org):
@ -136,7 +148,7 @@ class OrgMemberUserRelationBulkViewSet(JMSBulkRelationModelViewSet):
class CurrentOrgDetailApi(RetrieveAPIView):
serializer_class = CurrentOrgSerializer
permission_classes = (IsValidUser, UserCanUseCurrentOrg)
permission_classes = (IsValidUser, UserCanAnyPermCurrentOrg)
def get_object(self):
return current_org

View File

@ -51,7 +51,14 @@ class Organization(models.Model):
def get_instance_from_memory(cls, id_or_name):
if not isinstance(cls.orgs_mapping, dict):
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
def construct_orgs_mapping(cls):
@ -111,6 +118,8 @@ class Organization(models.Model):
def can_audit_by(self, user):
if user.is_superuser or user.is_super_auditor:
return True
if self.can_admin_by(user):
return True
if self.auditors.filter(id=user.id).exists():
return True
return False
@ -118,10 +127,17 @@ class Organization(models.Model):
def can_use_by(self, user):
if user.is_superuser or user.is_super_auditor:
return True
if self.can_audit_by(user):
return True
if self.users.filter(id=user.id).exists():
return True
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
def get_user_orgs_by_role(cls, user, role):
if not isinstance(role, (tuple, list)):

View File

@ -7,7 +7,8 @@ from functools import partial
from django.dispatch import receiver
from django.utils.functional import LazyObject
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.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.utils import get_logger
from common.utils.connection import RedisPubSub
from common.exceptions import JMSException
logger = get_logger(__file__)
@ -75,6 +77,15 @@ def on_org_delete(sender, **kwargs):
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):
with tmp_to_org(org):
if not isinstance(users, (tuple, list, set)):

View File

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

View File

@ -10,7 +10,7 @@ from django_filters import utils
from terminal import const
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 ..models import CommandStorage, ReplayStorage
from ..serializers import CommandStorageSerializer, ReplayStorageSerializer
@ -42,7 +42,7 @@ class CommandStorageViewSet(BaseStorageViewSetMixin, viewsets.ModelViewSet):
permission_classes = (IsSuperUser,)
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):
storage_qs = self.get_queryset().exclude(name='null')
storages_with_count = []

View File

@ -2,7 +2,7 @@
from django.core.cache import cache
from django.utils.translation import ugettext as _
from rest_framework.decorators import action
from django.conf import settings
from rest_framework import generics
from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet
@ -88,17 +88,14 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet):
def get_permissions(self):
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,)
else:
self.permission_classes = (IsOrgAdminOrAppUser,)
elif self.action in ['destroy']:
self.permission_classes = (IsSuperUser,)
return super().get_permissions()
def perform_destroy(self, instance):
if not current_org.is_root():
instance.remove()
else:
return super().perform_destroy(instance)
def perform_bulk_destroy(self, objects):
for obj in objects:
self.check_object_permissions(self.request, obj)
@ -164,6 +161,21 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet):
OrganizationMember.objects.bulk_create(relations, ignore_conflicts=True)
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):
permission_classes = (IsOrgAdmin,)

View File

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

View File

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

2
jms
View File

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