diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..191381ee7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +.git \ No newline at end of file diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index 653a7c2e5..aae75b86f 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -258,7 +258,8 @@ LOCALE_PATHS = [os.path.join(BASE_DIR, 'locale'), ] # https://docs.djangoproject.com/en/1.10/howto/static-files/ STATIC_URL = '/static/' -STATIC_ROOT = os.path.join(BASE_DIR, "static") +STATIC_ROOT = os.path.join(PROJECT_DIR, "data", "static") +STATIC_DIR = os.path.join(BASE_DIR, "static") STATICFILES_DIRS = ( diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index 839492e7a..21d42d0dd 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -36,6 +36,6 @@ urlpatterns = [ if settings.DEBUG: urlpatterns += [ url(r'^docs/', schema_view, name="docs"), - ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) \ + ] + static(settings.STATIC_URL, document_root=settings.STATIC_DIR) \ + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/apps/terminal/api.py b/apps/terminal/api.py index 576f068f7..ef065b23f 100644 --- a/apps/terminal/api.py +++ b/apps/terminal/api.py @@ -3,11 +3,13 @@ from collections import OrderedDict import copy import logging - import os +import uuid + from rest_framework import viewsets, serializers from rest_framework.views import APIView, Response from rest_framework.permissions import AllowAny +from django.core.cache import cache from django.shortcuts import get_object_or_404, redirect from django.utils import timezone from django.core.files.storage import default_storage @@ -35,7 +37,7 @@ class TerminalViewSet(viewsets.ModelViewSet): x_real_ip = request.META.get('X-Real-IP') remote_addr = x_real_ip or remote_ip - terminal = get_object_or_none(Terminal, name=name) + terminal = get_object_or_none(Terminal, name=name, is_deleted=False) if terminal: msg = 'Terminal name %s already used' % name return Response({'msg': msg}, status=409) @@ -46,12 +48,11 @@ class TerminalViewSet(viewsets.ModelViewSet): if serializer.is_valid(): terminal = serializer.save() - app_user, access_key = terminal.create_app_user() - data = OrderedDict() - data['terminal'] = copy.deepcopy(serializer.data) - data['user'] = app_user.to_json() - data['access_key'] = {'id': access_key.id, - 'secret': access_key.secret} + + # App should use id, token get access key, if accepted + token = uuid.uuid4().hex + cache.set(token, str(terminal.id), 3600) + data = {"id": str(terminal.id), "token": token, "msg": "Need accept"} return Response(data, status=201) else: data = serializer.errors @@ -63,6 +64,36 @@ class TerminalViewSet(viewsets.ModelViewSet): return super().get_permissions() +class TerminalTokenApi(APIView): + permission_classes = (AllowAny,) + queryset = Terminal.objects.filter(is_deleted=False) + + def get(self, request, *args, **kwargs): + try: + terminal = self.queryset.get(id=kwargs.get('terminal')) + except Terminal.DoesNotExist: + terminal = None + + token = request.query_params.get("token") + + if terminal is None: + return Response('May be reject by administrator', status=401) + + if token is None or cache.get(token, "") != str(terminal.id): + return Response('Token is not valid', status=401) + + if not terminal.is_accepted: + return Response("Terminal was not accepted yet", status=400) + + if not terminal.user or not terminal.user.access_key.all(): + return Response("No access key generate", status=401) + + access_key = terminal.user.access_key.first() + data = OrderedDict() + data['access_key'] = {'id': access_key.id, 'secret': access_key.secret} + return Response(data, status=200) + + class StatusViewSet(viewsets.ModelViewSet): queryset = Status.objects.all() serializer_class = StatusSerializer diff --git a/apps/terminal/models.py b/apps/terminal/models.py index 662e5deb4..dc9c7a165 100644 --- a/apps/terminal/models.py +++ b/apps/terminal/models.py @@ -11,7 +11,7 @@ from .backends.command.models import AbstractSessionCommand class Terminal(models.Model): id = models.UUIDField(default=uuid.uuid4, primary_key=True) - name = models.CharField(max_length=32, unique=True, verbose_name=_('Name')) + name = models.CharField(max_length=32, verbose_name=_('Name')) remote_addr = models.CharField(max_length=128, verbose_name=_('Remote Address')) ssh_port = models.IntegerField(verbose_name=_('SSH Port'), default=2222) http_port = models.IntegerField(verbose_name=_('HTTP Port'), default=5000) @@ -34,7 +34,8 @@ class Terminal(models.Model): self.user.save() def create_app_user(self): - user, access_key = User.create_app_user(name=self.name, comment=self.comment) + random = uuid.uuid4().hex[:6] + user, access_key = User.create_app_user(name="{}-{}".format(self.name, random), comment=self.comment) self.user = user self.save() return user, access_key @@ -42,6 +43,7 @@ class Terminal(models.Model): def delete(self, using=None, keep_parents=False): if self.user: self.user.delete() + self.user = None self.is_deleted = True self.save() return diff --git a/apps/terminal/serializers.py b/apps/terminal/serializers.py index b44d0b443..52b4d2b3c 100644 --- a/apps/terminal/serializers.py +++ b/apps/terminal/serializers.py @@ -14,8 +14,11 @@ class TerminalSerializer(serializers.ModelSerializer): class Meta: model = Terminal - fields = ['id', 'name', 'remote_addr', 'http_port', 'ssh_port', - 'comment', 'is_accepted', 'session_online', 'is_alive'] + fields = [ + 'id', 'name', 'remote_addr', 'http_port', 'ssh_port', + 'comment', 'is_accepted', "is_active", 'session_online', + 'is_alive' + ] @staticmethod def get_session_online(obj): diff --git a/apps/terminal/templates/terminal/terminal_list.html b/apps/terminal/templates/terminal/terminal_list.html index 3784806ce..314ed6c9a 100644 --- a/apps/terminal/templates/terminal/terminal_list.html +++ b/apps/terminal/templates/terminal/terminal_list.html @@ -89,7 +89,7 @@ function initTable() { ], ajax_url: '{% url "api-terminal:terminal-list" %}', columns: [{data: function(){return ""}}, {data: "name" }, {data: "remote_addr" }, {data: "ssh_port"}, {data: "http_port"}, - {data: "session_online"}, {data: "is_accepted" }, {data: 'is_alive'}, {data: "id"}], + {data: "session_online"}, {data: "is_active" }, {data: 'is_alive'}, {data: "id"}], op_html: $('#actions').html() }; jumpserver.initDataTable(options); diff --git a/apps/terminal/urls/api_urls.py b/apps/terminal/urls/api_urls.py index 2ce0857c8..55d77ee00 100644 --- a/apps/terminal/urls/api_urls.py +++ b/apps/terminal/urls/api_urls.py @@ -20,6 +20,7 @@ urlpatterns = [ url(r'^v1/sessions/(?P[0-9a-zA-Z\-]{36})/replay/$', api.SessionReplayViewSet.as_view({'get': 'retrieve', 'post': 'create'}), name='session-replay'), + url(r'^v1/terminal/(?P[a-zA-Z0-9\-]{36})/access-key', api.TerminalTokenApi.as_view(), name='terminal-access-key') ] urlpatterns += router.urls diff --git a/apps/terminal/views/terminal.py b/apps/terminal/views/terminal.py index 2551a97b8..30d16a1e2 100644 --- a/apps/terminal/views/terminal.py +++ b/apps/terminal/views/terminal.py @@ -73,6 +73,7 @@ class TerminalAcceptView(AdminUserRequiredMixin, JSONResponseMixin, UpdateView): def form_valid(self, form): terminal = form.save() + terminal.create_app_user() terminal.is_accepted = True terminal.is_active = True terminal.save() diff --git a/apps/users/models/user.py b/apps/users/models/user.py index c328902ff..ec9bfdc78 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -28,13 +28,13 @@ class User(AbstractUser): ('App', 'Application') ) id = models.UUIDField(default=uuid.uuid4, primary_key=True) - username = models.CharField(max_length=20, unique=True, verbose_name=_('Username')) - name = models.CharField(max_length=20, verbose_name=_('Name')) - email = models.EmailField(max_length=30, unique=True, verbose_name=_('Email')) + username = models.CharField(max_length=128, unique=True, verbose_name=_('Username')) + name = models.CharField(max_length=128, verbose_name=_('Name')) + email = models.EmailField(max_length=128, unique=True, verbose_name=_('Email')) groups = models.ManyToManyField(UserGroup, related_name='users', blank=True, verbose_name=_('User group')) role = models.CharField(choices=ROLE_CHOICES, default='User', max_length=10, blank=True, verbose_name=_('Role')) avatar = models.ImageField(upload_to="avatar", null=True, verbose_name=_('Avatar')) - wechat = models.CharField(max_length=30, blank=True, verbose_name=_('Wechat')) + wechat = models.CharField(max_length=128, blank=True, verbose_name=_('Wechat')) phone = models.CharField(max_length=20, blank=True, null=True, verbose_name=_('Phone')) enable_otp = models.BooleanField(default=False, verbose_name=_('Enable OTP')) secret_key_otp = models.CharField(max_length=16, blank=True) @@ -212,7 +212,7 @@ class User(AbstractUser): def create_app_user(cls, name, comment): from . import AccessKey app = cls.objects.create( - username=name, name=name, email='%s@local.domain'.format(), + username=name, name=name, email='{}@local.domain'.format(name), is_active=False, role='App', enable_otp=False, comment=comment, is_first_login=False, created_by='System' ) diff --git a/run_server.py b/run_server.py index 74a50455d..37f087b96 100644 --- a/run_server.py +++ b/run_server.py @@ -21,6 +21,11 @@ DEBUG = CONFIG.DEBUG LOG_LEVEL = CONFIG.LOG_LEVEL WORKERS = 4 +try: + os.makedirs(os.path.join(BASE_DIR, "data", "static")) + os.makedirs(os.path.join(BASE_DIR, "data", "media")) +except: + pass def start_gunicorn(): print("# Start Gunicorn WSGI HTTP Server")