diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py index 8ec151285..d0cc5662d 100644 --- a/apps/assets/api/system_user.py +++ b/apps/assets/api/system_user.py @@ -1,16 +1,15 @@ # ~*~ coding: utf-8 ~*~ from django.shortcuts import get_object_or_404 from rest_framework.response import Response +from rest_framework.exceptions import ValidationError from common.utils import get_logger -from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser -from common.drf.filters import CustomFilter +from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsValidUser from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins import generics -from orgs.utils import tmp_to_org from ..models import SystemUser, Asset from .. import serializers -from ..serializers import SystemUserWithAuthInfoSerializer +from ..serializers import SystemUserWithAuthInfoSerializer, SystemUserTempAuthSerializer from ..tasks import ( push_system_user_to_assets_manual, test_system_user_connectivity_manual, push_system_user_to_assets @@ -21,6 +20,7 @@ logger = get_logger(__file__) __all__ = [ 'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserAssetAuthInfoApi', 'SystemUserCommandFilterRuleListApi', 'SystemUserTaskApi', 'SystemUserAssetsListView', + 'SystemUserTempAuthInfoApi', 'SystemUserAppAuthInfoApi', ] @@ -57,6 +57,23 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView): return Response(status=204) +class SystemUserTempAuthInfoApi(generics.CreateAPIView): + model = SystemUser + permission_classes = (IsValidUser,) + serializer_class = SystemUserTempAuthSerializer + + def create(self, request, *args, **kwargs): + serializer = super().get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + pk = kwargs.get('pk') + instance = get_object_or_404(SystemUser, pk=pk) + data = serializer.validated_data + user = self.request.user + instance_id = data.get('instance_id') + instance.set_temp_auth(instance_id, user, data) + return Response(serializer.data, status=201) + + class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView): """ Get system user with asset auth info @@ -65,22 +82,30 @@ class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView): permission_classes = (IsOrgAdminOrAppUser,) serializer_class = SystemUserWithAuthInfoSerializer - def get_exception_handler(self): - def handler(e, context): - return Response({"error": str(e)}, status=400) - return handler + def get_object(self): + instance = super().get_object() + asset_id = self.kwargs.get('asset_id') + user_id = self.request.query_params.get("user_id") + username = self.request.query_params.get("username") + instance.load_asset_more_auth(asset_id=asset_id, user_id=user_id, username=username) + return instance + + +class SystemUserAppAuthInfoApi(generics.RetrieveAPIView): + """ + Get system user with asset auth info + """ + model = SystemUser + permission_classes = (IsOrgAdminOrAppUser,) + serializer_class = SystemUserWithAuthInfoSerializer def get_object(self): instance = super().get_object() - username = instance.username - if instance.username_same_with_user: - username = self.request.query_params.get("username") - asset_id = self.kwargs.get('aid') - asset = get_object_or_404(Asset, pk=asset_id) - - with tmp_to_org(asset.org_id): - instance.load_asset_special_auth(asset=asset, username=username) - return instance + app_id = self.kwargs.get('app_id') + user_id = self.request.query_params.get("user_id") + if user_id: + instance.load_app_more_auth(app_id, user_id) + return instance class SystemUserTaskApi(generics.CreateAPIView): diff --git a/apps/assets/models/base.py b/apps/assets/models/base.py index 5ed16741f..6623cc6a7 100644 --- a/apps/assets/models/base.py +++ b/apps/assets/models/base.py @@ -11,8 +11,7 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from django.conf import settings -from common.db.models import ChoiceSet -from common.utils import random_string +from common.utils import random_string, signer from common.utils import ( ssh_key_string_to_obj, ssh_key_gen, get_logger, lazyproperty ) diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index 37af8ea86..b64aeb758 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -7,9 +7,10 @@ import logging from django.db import models from django.utils.translation import ugettext_lazy as _ from django.core.validators import MinValueValidator, MaxValueValidator +from django.core.cache import cache -from common.utils import signer -from common.fields.model import JsonListCharField +from common.utils import signer, get_object_or_none +from common.exceptions import JMSException from .base import BaseUser from .asset import Asset @@ -185,6 +186,81 @@ class SystemUser(BaseUser): if self.username_same_with_user: self.username = other.username + def set_temp_auth(self, asset_or_app_id, user_id, auth, ttl=300): + if not auth: + raise ValueError('Auth not set') + key = 'TEMP_PASSWORD_{}_{}_{}'.format(self.id, asset_or_app_id, user_id) + logger.debug(f'Set system user temp auth: {key}') + cache.set(key, auth, ttl) + + def get_temp_auth(self, asset_or_app_id, user_id): + key = 'TEMP_PASSWORD_{}_{}_{}'.format(self.id, asset_or_app_id, user_id) + logger.debug(f'Get system user temp auth: {key}') + password = cache.get(key) + return password + + def load_tmp_auth_if_has(self, asset_or_app_id, user): + if not asset_or_app_id or not user: + return + if self.login_mode != self.LOGIN_MANUAL: + pass + + auth = self.get_temp_auth(asset_or_app_id, user) + if not auth: + return + username = auth.get('username') + password = auth.get('password') + + if username: + self.username = username + if password: + self.password = password + + def load_app_more_auth(self, app_id=None, user_id=None): + from users.models import User + + if self.login_mode == self.LOGIN_MANUAL: + self.password = '' + self.private_key = '' + if not user_id: + return + user = get_object_or_none(User, pk=user_id) + if not user: + return + self.load_tmp_auth_if_has(app_id, user) + + def load_asset_more_auth(self, asset_id=None, username=None, user_id=None): + from users.models import User + + if self.login_mode == self.LOGIN_MANUAL: + self.password = '' + self.private_key = '' + + asset = None + if asset_id: + asset = get_object_or_none(Asset, pk=asset_id) + # 没有资产就没有必要继续了 + if not asset: + logger.debug('Asset not found, pass') + return + + user = None + if user_id: + user = get_object_or_none(User, pk=user_id) + + if self.username_same_with_user: + if user and not username: + username = user.username + + # 加载某个资产的特殊配置认证信息 + try: + self.load_asset_special_auth(asset, username) + except Exception as e: + logger.error('Load special auth Error: ', e) + pass + + self.load_tmp_auth_if_has(asset_id, user) + @property def cmd_filter_rules(self): from .cmd_filter import CommandFilterRule diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py index 7f5befaed..726f53e34 100644 --- a/apps/assets/serializers/system_user.py +++ b/apps/assets/serializers/system_user.py @@ -14,6 +14,7 @@ __all__ = [ 'SystemUserSimpleSerializer', 'SystemUserAssetRelationSerializer', 'SystemUserNodeRelationSerializer', 'SystemUserTaskSerializer', 'SystemUserUserRelationSerializer', 'SystemUserWithAuthInfoSerializer', + 'SystemUserTempAuthSerializer', ] @@ -272,3 +273,10 @@ class SystemUserTaskSerializer(serializers.Serializer): many=True ) task = serializers.CharField(read_only=True) + + +class SystemUserTempAuthSerializer(SystemUserSerializer): + instance_id = serializers.CharField() + + class Meta(SystemUserSerializer.Meta): + fields = ['instance_id', 'username', 'password'] diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index c8413f83e..8bc20a162 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -46,7 +46,9 @@ urlpatterns = [ path('system-users//auth-info/', api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'), path('system-users//assets/', api.SystemUserAssetsListView.as_view(), name='system-user-assets'), - path('system-users//assets//auth-info/', api.SystemUserAssetAuthInfoApi.as_view(), name='system-user-asset-auth-info'), + path('system-users//assets//auth-info/', api.SystemUserAssetAuthInfoApi.as_view(), name='system-user-asset-auth-info'), + path('system-users//applications//auth-info/', api.SystemUserAppAuthInfoApi.as_view(), name='system-user-app-auth-info'), + path('system-users//temp-auth/', api.SystemUserTempAuthInfoApi.as_view(), name='system-user-asset-temp-info'), path('system-users//tasks/', api.SystemUserTaskApi.as_view(), name='system-user-task-create'), path('system-users//cmd-filter-rules/', api.SystemUserCommandFilterRuleListApi.as_view(), name='system-user-cmd-filter-rule-list'), diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index ceebf3bde..ef908ed71 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -93,7 +93,7 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView token = self.create_token(user, asset, application, system_user) return Response({"token": token}, status=201) - @action(methods=['POST', 'GET'], detail=False, url_path='rdp/file') + @action(methods=['POST', 'GET'], detail=False, url_path='rdp/file', permission_classes=[IsValidUser]) def get_rdp_file(self, request, *args, **kwargs): options = { 'full address:s': '', @@ -134,13 +134,15 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView asset = serializer.validated_data.get('asset') application = serializer.validated_data.get('application') system_user = serializer.validated_data['system_user'] - user = serializer.validated_data.get('user') height = serializer.validated_data.get('height') width = serializer.validated_data.get('width') + user = request.user token = self.create_token(user, asset, application, system_user) # Todo: 上线后地址是 JumpServerAddr:3389 - address = self.request.query_params.get('address') or '1.1.1.1' + address = settings.RDP_ADDR + if address == 'localhost:3389': + address = request.get_host().split(':')[0] + ':3389' options['full address:s'] = address options['username:s'] = '{}|{}'.format(user.username, token) options['desktopwidth:i'] = width @@ -217,10 +219,16 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView if value.get('type') == 'asset': asset_detail = self._get_asset_secret_detail(value, user=user, system_user=system_user) + asset = asset_detail.get('asset') + if asset: + system_user.load_asset_more_auth(asset.id, user.username, user.id) data['type'] = 'asset' data.update(asset_detail) else: app_detail = self._get_application_secret_detail(value) + app = app_detail.get("application") + if app: + system_user.load_app_more_auth(app.id, user.id) data['type'] = 'application' data.update(app_detail) diff --git a/apps/authentication/serializers.py b/apps/authentication/serializers.py index 72b54e3ee..9265755d6 100644 --- a/apps/authentication/serializers.py +++ b/apps/authentication/serializers.py @@ -199,5 +199,5 @@ class ConnectionTokenSecretSerializer(serializers.Serializer): class RDPFileSerializer(ConnectionTokenSerializer): - width = serializers.IntegerField(default=1280) - height = serializers.IntegerField(default=800) + width = serializers.IntegerField(default=1600) + height = serializers.IntegerField(default=900) diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 68be2776b..f3b59d2d9 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -300,7 +300,9 @@ class Config(dict): 'SESSION_SAVE_EVERY_REQUEST': True, 'SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE': False, 'FORGOT_PASSWORD_URL': '', - 'HEALTH_CHECK_TOKEN': '' + 'HEALTH_CHECK_TOKEN': '', + + 'RDP_ADDR': 'localhost:3389' } def compatible_auth_openid_of_key(self): diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index ed37ea49c..d68195792 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -125,3 +125,5 @@ FORGOT_PASSWORD_URL = CONFIG.FORGOT_PASSWORD_URL # 自定义默认组织名 GLOBAL_ORG_DISPLAY_NAME = CONFIG.GLOBAL_ORG_DISPLAY_NAME HEALTH_CHECK_TOKEN = CONFIG.HEALTH_CHECK_TOKEN + +RDP_ADDR = CONFIG.RDP_ADDR diff --git a/apps/orgs/api.py b/apps/orgs/api.py index ace14112b..14ad9cb20 100644 --- a/apps/orgs/api.py +++ b/apps/orgs/api.py @@ -48,7 +48,6 @@ class OrgViewSet(BulkModelViewSet): queryset = Organization.objects.all() serializer_class = OrgSerializer permission_classes = (IsSuperUserOrAppUser,) - org = None def get_serializer_class(self): mapper = { @@ -58,32 +57,36 @@ class OrgViewSet(BulkModelViewSet): 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, org, model): if model == User: data = model.objects.filter( - orgs__id=self.org.id, - m2m_org_members__role__in=[ROLE.USER, ROLE.ADMIN, ROLE.AUDITOR] + orgs__id=org.id, m2m_org_members__role__in=[ROLE.USER, ROLE.ADMIN, ROLE.AUDITOR] ) elif model == Node: - # 跟节点不能手动删除,所以排除检查 - data = model.objects.filter(org_id=self.org.id).exclude(parent_key='', key__regex=r'^[0-9]+$') + # 根节点不能手动删除,所以排除检查 + data = model.objects.filter(org_id=org.id).exclude(parent_key='', key__regex=r'^[0-9]+$') else: - data = model.objects.filter(org_id=self.org.id) + data = model.objects.filter(org_id=org.id) return data - def destroy(self, request, *args, **kwargs): - self.org = self.get_object() + def allow_bulk_destroy(self, qs, filtered): + return False + + def perform_destroy(self, instance): + if str(current_org) == str(instance): + msg = _('The current organization ({}) cannot be deleted'.format(current_org)) + raise PermissionDenied(detail=msg) + for model in org_related_models: - data = self.get_data_from_model(model) - if data: - msg = _('Have {} exists, Please delete').format(model._meta.verbose_name) - return Response(data={'error': msg}, status=status.HTTP_403_FORBIDDEN) - else: - if str(current_org) == str(self.org): - msg = _('The current organization cannot be deleted') - return Response(data={'error': msg}, status=status.HTTP_403_FORBIDDEN) - self.org.delete() - return Response({'msg': True}, status=status.HTTP_200_OK) + data = self.get_data_from_model(instance, model) + if not data: + continue + msg = _( + 'The organization have resource ({}) cannot be deleted' + ).format(model._meta.verbose_name) + raise PermissionDenied(detail=msg) + + super().perform_destroy(instance) class OrgMemberRelationBulkViewSet(JMSBulkRelationModelViewSet): diff --git a/apps/settings/serializers/settings.py b/apps/settings/serializers/settings.py index f26679c29..a997c5466 100644 --- a/apps/settings/serializers/settings.py +++ b/apps/settings/serializers/settings.py @@ -13,7 +13,12 @@ __all__ = [ class BasicSettingSerializer(serializers.Serializer): SITE_URL = serializers.URLField( required=True, label=_("Site url"), - help_text=_('eg: http://demo.jumpserver.org:8080') + help_text=_('eg: http://dev.jumpserver.org:8080') + ) + RDP_ADDR = serializers.CharField( + required=True, label=_("RDP address"), + max_length=1024, + help_text=_('RDP visit address, eg: dev.jumpserver.org:3389') ) USER_GUIDE_URL = serializers.URLField( required=False, allow_blank=True, allow_null=True, label=_("User guide url"), diff --git a/apps/terminal/const.py b/apps/terminal/const.py index ff638325d..c2512e024 100644 --- a/apps/terminal/const.py +++ b/apps/terminal/const.py @@ -16,6 +16,7 @@ class ReplayStorageTypeChoices(TextChoices): swift = 'swift', 'Swift' oss = 'oss', 'OSS' azure = 'azure', 'Azure' + obs = 'obs', 'OBS' class CommandStorageTypeChoices(TextChoices): diff --git a/apps/terminal/migrations/0035_auto_20210412_1905.py b/apps/terminal/migrations/0035_auto_20210412_1905.py new file mode 100644 index 000000000..251287935 --- /dev/null +++ b/apps/terminal/migrations/0035_auto_20210412_1905.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.6 on 2021-04-12 11:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0034_auto_20210406_1434'), + ] + + 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')], default='server', max_length=16, verbose_name='Type'), + ), + ] diff --git a/apps/terminal/serializers/storage.py b/apps/terminal/serializers/storage.py index ff794eef9..cdd6e75a3 100644 --- a/apps/terminal/serializers/storage.py +++ b/apps/terminal/serializers/storage.py @@ -82,6 +82,16 @@ class ReplayStorageTypeOSSSerializer(ReplayStorageTypeBaseSerializer): ) +class ReplayStorageTypeOBSSerializer(ReplayStorageTypeBaseSerializer): + endpoint_help_text = ''' + OBS format: obs.{REGION_NAME}.myhuaweicloud.com + Such as: obs.cn-north-4.myhuaweicloud.com + ''' + ENDPOINT = serializers.CharField( + max_length=1024, label=_('Endpoint'), help_text=_(endpoint_help_text), allow_null=True, + ) + + class ReplayStorageTypeAzureSerializer(serializers.Serializer): class EndpointSuffixChoices(TextChoices): china = 'core.chinacloudapi.cn', 'core.chinacloudapi.cn' @@ -105,7 +115,8 @@ replay_storage_type_serializer_classes_mapping = { const.ReplayStorageTypeChoices.ceph.value: ReplayStorageTypeCephSerializer, const.ReplayStorageTypeChoices.swift.value: ReplayStorageTypeSwiftSerializer, const.ReplayStorageTypeChoices.oss.value: ReplayStorageTypeOSSSerializer, - const.ReplayStorageTypeChoices.azure.value: ReplayStorageTypeAzureSerializer + const.ReplayStorageTypeChoices.azure.value: ReplayStorageTypeAzureSerializer, + const.ReplayStorageTypeChoices.obs.value: ReplayStorageTypeOBSSerializer } # ReplayStorageSerializer