From 9cfcadc2f618daa447717c423ff006561bd52221 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81=E5=B9=BF?= Date: Fri, 23 Nov 2018 10:25:35 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E8=B4=A6=E5=8F=B7=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=E6=9C=BA=E5=88=B6=E6=9B=B4=E6=94=B9=20(#2079)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Update] 服务账号注册 * [Update] 修改settings配置 * [Update] 修改settings * [Update] 整理terminal api * [Update] 修改terminal api * [Update] 修改terminal注册机制 --- apps/common/models.py | 2 - apps/common/permissions.py | 10 + apps/jumpserver/settings.py | 5 + apps/jumpserver/swagger.py | 35 ++ apps/jumpserver/urls.py | 90 ++--- apps/jumpserver/views.py | 31 ++ apps/ops/api.py | 6 +- apps/terminal/api.py | 374 ------------------ apps/terminal/api/__init__.py | 2 + apps/terminal/api/v1/__init__.py | 5 + apps/terminal/api/v1/session.py | 184 +++++++++ apps/terminal/api/v1/task.py | 37 ++ apps/terminal/api/v1/terminal.py | 174 ++++++++ apps/terminal/api/v2/__init__.py | 3 + apps/terminal/api/v2/terminal.py | 22 ++ apps/terminal/models.py | 6 +- apps/terminal/serializers/__init__.py | 2 + .../{serializers.py => serializers/v1.py} | 9 +- apps/terminal/serializers/v2.py | 46 +++ apps/terminal/urls/api_urls.py | 9 +- apps/terminal/urls/api_urls_v2.py | 20 + apps/users/api/auth.py | 11 +- apps/users/api/user.py | 16 +- apps/users/api/v2/__init__.py | 4 + apps/users/api/v2/user.py | 12 + apps/users/authentication.py | 6 +- apps/users/models/authentication.py | 5 +- apps/users/models/user.py | 39 +- apps/users/serializers/__init__.py | 3 + .../{serializers.py => serializers/v1.py} | 6 +- apps/users/serializers/v2.py | 44 +++ apps/users/urls/api_urls.py | 6 +- apps/users/urls/api_urls_v2.py | 23 ++ apps/users/utils.py | 18 - requirements/requirements.txt | 1 + utils/create_assets_user/admin_users.txt | 3 + utils/create_assets_user/bulk_create_user.py | 159 ++++++++ utils/create_assets_user/system_users.txt | 4 + utils/create_assets_user/使用说明.txt | 21 + 39 files changed, 958 insertions(+), 495 deletions(-) create mode 100644 apps/jumpserver/swagger.py delete mode 100644 apps/terminal/api.py create mode 100644 apps/terminal/api/__init__.py create mode 100644 apps/terminal/api/v1/__init__.py create mode 100644 apps/terminal/api/v1/session.py create mode 100644 apps/terminal/api/v1/task.py create mode 100644 apps/terminal/api/v1/terminal.py create mode 100644 apps/terminal/api/v2/__init__.py create mode 100644 apps/terminal/api/v2/terminal.py create mode 100644 apps/terminal/serializers/__init__.py rename apps/terminal/{serializers.py => serializers/v1.py} (93%) create mode 100644 apps/terminal/serializers/v2.py create mode 100644 apps/terminal/urls/api_urls_v2.py create mode 100644 apps/users/api/v2/__init__.py create mode 100644 apps/users/api/v2/user.py create mode 100644 apps/users/serializers/__init__.py rename apps/users/{serializers.py => serializers/v1.py} (93%) create mode 100644 apps/users/serializers/v2.py create mode 100644 apps/users/urls/api_urls_v2.py create mode 100644 utils/create_assets_user/admin_users.txt create mode 100644 utils/create_assets_user/bulk_create_user.py create mode 100644 utils/create_assets_user/system_users.txt create mode 100644 utils/create_assets_user/使用说明.txt diff --git a/apps/common/models.py b/apps/common/models.py index 2e1790d4e..a864e9535 100644 --- a/apps/common/models.py +++ b/apps/common/models.py @@ -1,12 +1,10 @@ import json -import ldap from django.db import models from django.core.cache import cache from django.db.utils import ProgrammingError, OperationalError from django.utils.translation import ugettext_lazy as _ from django.conf import settings -from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion from .utils import get_signer diff --git a/apps/common/permissions.py b/apps/common/permissions.py index 20554e071..10ef329ed 100644 --- a/apps/common/permissions.py +++ b/apps/common/permissions.py @@ -5,6 +5,7 @@ from rest_framework import permissions from django.contrib.auth.mixins import UserPassesTestMixin from django.shortcuts import redirect from django.http.response import HttpResponseForbidden +from django.conf import settings from orgs.utils import current_org @@ -96,3 +97,12 @@ class SuperUserRequiredMixin(UserPassesTestMixin): def test_func(self): if self.request.user.is_authenticated and self.request.user.is_superuser: return True + + +class WithBootstrapToken(permissions.BasePermission): + def has_permission(self, request, view): + authorization = request.META.get('HTTP_AUTHORIZATION', '') + if not authorization: + return False + request_bootstrap_token = authorization.split()[-1] + return settings.BOOTSTRAP_TOKEN == request_bootstrap_token diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index 65ea4e26e..fff5b64db 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -30,6 +30,9 @@ CONFIG = load_user_config() # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = CONFIG.SECRET_KEY +# SECURITY WARNING: keep the token secret, remove it if all coco, guacamole ok +BOOTSTRAP_TOKEN = CONFIG.BOOTSTRAP_TOKEN + # SECURITY WARNING: don't run with debug turned on in production! DEBUG = CONFIG.DEBUG @@ -499,9 +502,11 @@ USER_GUIDE_URL = "" SWAGGER_SETTINGS = { + 'DEFAULT_AUTO_SCHEMA_CLASS': 'jumpserver.swagger.CustomSwaggerAutoSchema', 'SECURITY_DEFINITIONS': { 'basic': { 'type': 'basic' } }, } + diff --git a/apps/jumpserver/swagger.py b/apps/jumpserver/swagger.py new file mode 100644 index 000000000..bd7662b46 --- /dev/null +++ b/apps/jumpserver/swagger.py @@ -0,0 +1,35 @@ +from drf_yasg.inspectors import SwaggerAutoSchema + +from rest_framework import permissions +from drf_yasg.views import get_schema_view +from drf_yasg import openapi + + +class CustomSwaggerAutoSchema(SwaggerAutoSchema): + def get_tags(self, operation_keys): + if len(operation_keys) > 2 and operation_keys[1].startswith('v'): + return [operation_keys[2]] + return super().get_tags(operation_keys) + + +def get_swagger_view(version='v1'): + from .urls import api_v1_patterns, api_v2_patterns + if version == "v2": + patterns = api_v2_patterns + else: + patterns = api_v1_patterns + schema_view = get_schema_view( + openapi.Info( + title="Jumpserver API Docs", + default_version=version, + description="Jumpserver Restful api docs", + terms_of_service="https://www.jumpserver.org", + contact=openapi.Contact(email="support@fit2cloud.com"), + license=openapi.License(name="GPLv2 License"), + ), + public=True, + patterns=patterns, + permission_classes=(permissions.AllowAny,), + ) + return schema_view + diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index 2319d81e5..8ddf5cae4 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -1,70 +1,34 @@ # ~*~ coding: utf-8 ~*~ from __future__ import unicode_literals -import re -import os from django.urls import path, include, re_path from django.conf import settings from django.conf.urls.static import static from django.conf.urls.i18n import i18n_patterns from django.views.i18n import JavaScriptCatalog -from rest_framework.response import Response -from django.views.decorators.csrf import csrf_exempt -from django.http import HttpResponse -from django.utils.encoding import iri_to_uri -from rest_framework import permissions -from drf_yasg.views import get_schema_view -from drf_yasg import openapi from .views import IndexView, LunaView, I18NView - -schema_view = get_schema_view( - openapi.Info( - title="Jumpserver API Docs", - default_version='v1', - description="Jumpserver Restful api docs", - terms_of_service="https://www.jumpserver.org", - contact=openapi.Contact(email="support@fit2cloud.com"), - license=openapi.License(name="GPLv2 License"), - ), - public=True, - permission_classes=(permissions.AllowAny,), -) -api_url_pattern = re.compile(r'^/api/(?P\w+)/(?P\w+)/(?P.*)$') +from .swagger import get_swagger_view -class HttpResponseTemporaryRedirect(HttpResponse): - status_code = 307 +api_v1_patterns = [ + path('api/', include([ + path('users/v1/', include('users.urls.api_urls', namespace='api-users')), + path('assets/v1/', include('assets.urls.api_urls', namespace='api-assets')), + path('perms/v1/', include('perms.urls.api_urls', namespace='api-perms')), + path('terminal/v1/', include('terminal.urls.api_urls', namespace='api-terminal')), + path('ops/v1/', include('ops.urls.api_urls', namespace='api-ops')), + path('audits/v1/', include('audits.urls.api_urls', namespace='api-audits')), + path('orgs/v1/', include('orgs.urls.api_urls', namespace='api-orgs')), + path('common/v1/', include('common.urls.api_urls', namespace='api-common')), + ])) +] - def __init__(self, redirect_to): - HttpResponse.__init__(self) - self['Location'] = iri_to_uri(redirect_to) - - -@csrf_exempt -def redirect_format_api(request, *args, **kwargs): - _path, query = request.path, request.GET.urlencode() - matched = api_url_pattern.match(_path) - if matched: - version, app, extra = matched.groups() - _path = '/api/{app}/{version}/{extra}?{query}'.format(**{ - "app": app, "version": version, "extra": extra, - "query": query - }) - return HttpResponseTemporaryRedirect(_path) - else: - return Response({"msg": "Redirect url failed: {}".format(_path)}, status=404) - - -v1_api_patterns = [ - path('users/v1/', include('users.urls.api_urls', namespace='api-users')), - path('assets/v1/', include('assets.urls.api_urls', namespace='api-assets')), - path('perms/v1/', include('perms.urls.api_urls', namespace='api-perms')), - path('terminal/v1/', include('terminal.urls.api_urls', namespace='api-terminal')), - path('ops/v1/', include('ops.urls.api_urls', namespace='api-ops')), - path('audits/v1/', include('audits.urls.api_urls', namespace='api-audits')), - path('orgs/v1/', include('orgs.urls.api_urls', namespace='api-orgs')), - path('common/v1/', include('common.urls.api_urls', namespace='api-common')), +api_v2_patterns = [ + path('api/', include([ + path('terminal/v2/', include('terminal.urls.api_urls_v2', namespace='api-terminal-v2')), + path('users/v2/', include('users.urls.api_urls_v2', namespace='api-users-v2')), + ])) ] app_view_patterns = [ @@ -78,6 +42,7 @@ app_view_patterns = [ path('auth/', include('authentication.urls.view_urls'), name='auth'), ] + if settings.XPACK_ENABLED: app_view_patterns.append(path('xpack/', include('xpack.urls', namespace='xpack'))) @@ -87,12 +52,13 @@ js_i18n_patterns = i18n_patterns( urlpatterns = [ path('', IndexView.as_view(), name='index'), + path('', include(api_v2_patterns)), + path('', include(api_v1_patterns)), path('luna/', LunaView.as_view(), name='luna-error'), path('i18n//', I18NView.as_view(), name='i18n-switch'), path('settings/', include('common.urls.view_urls', namespace='settings')), path('common/', include('common.urls.view_urls', namespace='common')), - path('api/v1/', redirect_format_api), - path('api/', include(v1_api_patterns)), + # path('api/v2/', include(api_v2_patterns)), # External apps url path('captcha/', include('captcha.urls')), @@ -104,7 +70,13 @@ urlpatterns += js_i18n_patterns if settings.DEBUG: urlpatterns += [ - re_path('swagger(?P\.json|\.yaml)$', schema_view.without_ui(cache_timeout=None), name='schema-json'), - path('docs/', schema_view.with_ui('swagger', cache_timeout=None), name="docs"), - path('redoc/', schema_view.with_ui('redoc', cache_timeout=None), name='redoc'), + re_path('^swagger(?P\.json|\.yaml)$', + get_swagger_view().without_ui(cache_timeout=1), name='schema-json'), + path('docs/', get_swagger_view().with_ui('swagger', cache_timeout=1), name="docs"), + path('redoc/', get_swagger_view().with_ui('redoc', cache_timeout=1), name='redoc'), + + re_path('^v2/swagger(?P\.json|\.yaml)$', + get_swagger_view().without_ui(cache_timeout=1), name='schema-json'), + path('docs/v2/', get_swagger_view("v2").with_ui('swagger', cache_timeout=1), name="docs"), + path('redoc/v2/', get_swagger_view("v2").with_ui('redoc', cache_timeout=1), name='redoc'), ] diff --git a/apps/jumpserver/views.py b/apps/jumpserver/views.py index d70225582..52fa31273 100644 --- a/apps/jumpserver/views.py +++ b/apps/jumpserver/views.py @@ -1,4 +1,5 @@ import datetime +import re from django.http import HttpResponse, HttpResponseRedirect from django.conf import settings @@ -8,6 +9,10 @@ from django.utils.translation import ugettext_lazy as _ from django.db.models import Count from django.shortcuts import redirect from django.contrib.auth.mixins import LoginRequiredMixin +from rest_framework.response import Response +from django.views.decorators.csrf import csrf_exempt +from django.http import HttpResponse +from django.utils.encoding import iri_to_uri from users.models import User from assets.models import Asset @@ -188,3 +193,29 @@ class I18NView(View): response = HttpResponseRedirect(referer_url) response.set_cookie(settings.LANGUAGE_COOKIE_NAME, lang) return response + + +api_url_pattern = re.compile(r'^/api/(?P\w+)/(?P\w+)/(?P.*)$') + + +class HttpResponseTemporaryRedirect(HttpResponse): + status_code = 307 + + def __init__(self, redirect_to): + HttpResponse.__init__(self) + self['Location'] = iri_to_uri(redirect_to) + + +@csrf_exempt +def redirect_format_api(request, *args, **kwargs): + _path, query = request.path, request.GET.urlencode() + matched = api_url_pattern.match(_path) + if matched: + version, app, extra = matched.groups() + _path = '/api/{app}/{version}/{extra}?{query}'.format(**{ + "app": app, "version": version, "extra": extra, + "query": query + }) + return HttpResponseTemporaryRedirect(_path) + else: + return Response({"msg": "Redirect url failed: {}".format(_path)}, status=404) diff --git a/apps/ops/api.py b/apps/ops/api.py index 4fd1111d7..f052abf4f 100644 --- a/apps/ops/api.py +++ b/apps/ops/api.py @@ -19,13 +19,13 @@ class TaskViewSet(viewsets.ModelViewSet): queryset = Task.objects.all() serializer_class = TaskSerializer permission_classes = (IsOrgAdmin,) - label = None - help_text = '' + # label = None + # help_text = '' class TaskRun(generics.RetrieveAPIView): queryset = Task.objects.all() - serializer_class = TaskViewSet + # serializer_class = TaskViewSet permission_classes = (IsOrgAdmin,) def retrieve(self, request, *args, **kwargs): diff --git a/apps/terminal/api.py b/apps/terminal/api.py deleted file mode 100644 index 1a2a35184..000000000 --- a/apps/terminal/api.py +++ /dev/null @@ -1,374 +0,0 @@ -# -*- coding: utf-8 -*- -# -from collections import OrderedDict -import logging -import os -import uuid - -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 -from django.http.response import HttpResponseRedirectBase -from django.http import HttpResponseNotFound -from django.conf import settings - -import jms_storage - -from rest_framework.pagination import LimitOffsetPagination -from rest_framework import viewsets -from rest_framework.views import APIView, Response -from rest_framework.permissions import AllowAny -from rest_framework_bulk import BulkModelViewSet - -from common.utils import get_object_or_none, is_uuid -from .hands import SystemUser -from .models import Terminal, Status, Session, Task -from .serializers import TerminalSerializer, StatusSerializer, \ - SessionSerializer, TaskSerializer, ReplaySerializer -from common.permissions import IsAppUser, IsOrgAdminOrAppUser -from .backends import get_command_storage, get_multi_command_storage, \ - SessionCommandSerializer - -logger = logging.getLogger(__file__) - - -class TerminalViewSet(viewsets.ModelViewSet): - queryset = Terminal.objects.filter(is_deleted=False) - serializer_class = TerminalSerializer - permission_classes = (AllowAny,) - - def create(self, request, *args, **kwargs): - name = request.data.get('name') - remote_ip = request.META.get('REMOTE_ADDR') - 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, is_deleted=False) - if terminal: - msg = 'Terminal name %s already used' % name - return Response({'msg': msg}, status=409) - - serializer = self.serializer_class(data={ - 'name': name, 'remote_addr': remote_addr - }) - - if serializer.is_valid(): - terminal = serializer.save() - - # 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 - logger.error("Register terminal error: {}".format(data)) - return Response(data, status=400) - - def get_permissions(self): - if self.action == "create": - self.permission_classes = (AllowAny,) - 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 - permission_classes = (IsOrgAdminOrAppUser,) - session_serializer_class = SessionSerializer - task_serializer_class = TaskSerializer - - def create(self, request, *args, **kwargs): - from_gua = self.request.query_params.get("from_guacamole", None) - if not from_gua: - self.handle_sessions() - super().create(request, *args, **kwargs) - tasks = self.request.user.terminal.task_set.filter(is_finished=False) - serializer = self.task_serializer_class(tasks, many=True) - return Response(serializer.data, status=201) - - def handle_sessions(self): - sessions_active = [] - for session_data in self.request.data.get("sessions", []): - self.create_or_update_session(session_data) - if not session_data["is_finished"]: - sessions_active.append(session_data["id"]) - - sessions_in_db_active = Session.objects.filter( - is_finished=False, - terminal=self.request.user.terminal.id - ) - - for session in sessions_in_db_active: - if str(session.id) not in sessions_active: - session.is_finished = True - session.date_end = timezone.now() - session.save() - - def create_or_update_session(self, session_data): - session_data["terminal"] = self.request.user.terminal.id - _id = session_data["id"] - session = get_object_or_none(Session, id=_id) - if session: - serializer = SessionSerializer( - data=session_data, instance=session - ) - else: - serializer = SessionSerializer(data=session_data) - - if serializer.is_valid(): - session = serializer.save() - return session - else: - msg = "session data is not valid {}: {}".format( - serializer.errors, str(serializer.data) - ) - logger.error(msg) - return None - - def get_queryset(self): - terminal_id = self.kwargs.get("terminal", None) - if terminal_id: - terminal = get_object_or_404(Terminal, id=terminal_id) - self.queryset = terminal.status_set.all() - return self.queryset - - def perform_create(self, serializer): - serializer.validated_data["terminal"] = self.request.user.terminal - return super().perform_create(serializer) - - def get_permissions(self): - if self.action == "create": - self.permission_classes = (IsAppUser,) - return super().get_permissions() - - -class SessionViewSet(BulkModelViewSet): - queryset = Session.objects.all() - serializer_class = SessionSerializer - pagination_class = LimitOffsetPagination - permission_classes = (IsOrgAdminOrAppUser,) - - def get_queryset(self): - terminal_id = self.kwargs.get("terminal", None) - if terminal_id: - terminal = get_object_or_404(Terminal, id=terminal_id) - self.queryset = terminal.session_set.all() - return self.queryset.all() - - def perform_create(self, serializer): - if hasattr(self.request.user, 'terminal'): - serializer.validated_data["terminal"] = self.request.user.terminal - sid = serializer.validated_data["system_user"] - if is_uuid(sid): - _system_user = SystemUser.get_system_user_by_id_or_cached(sid) - if _system_user: - serializer.validated_data["system_user"] = _system_user.name - return super().perform_create(serializer) - - -class TaskViewSet(BulkModelViewSet): - queryset = Task.objects.all() - serializer_class = TaskSerializer - permission_classes = (IsOrgAdminOrAppUser,) - - -class KillSessionAPI(APIView): - permission_classes = (IsOrgAdminOrAppUser,) - model = Task - - def post(self, request, *args, **kwargs): - validated_session = [] - for session_id in request.data: - session = get_object_or_none(Session, id=session_id) - if session and not session.is_finished: - validated_session.append(session_id) - self.model.objects.create( - name="kill_session", args=session.id, - terminal=session.terminal, - ) - return Response({"ok": validated_session}) - - -class CommandViewSet(viewsets.ViewSet): - """接受app发送来的command log, 格式如下 - { - "user": "admin", - "asset": "localhost", - "system_user": "web", - "session": "xxxxxx", - "input": "whoami", - "output": "d2hvbWFp", # base64.b64encode(s) - "timestamp": 1485238673.0 - } - - """ - command_store = get_command_storage() - multi_command_storage = get_multi_command_storage() - serializer_class = SessionCommandSerializer - permission_classes = (IsOrgAdminOrAppUser,) - - def get_queryset(self): - self.command_store.filter(**dict(self.request.query_params)) - - def create(self, request, *args, **kwargs): - serializer = self.serializer_class(data=request.data, many=True) - if serializer.is_valid(): - ok = self.command_store.bulk_save(serializer.validated_data) - if ok: - return Response("ok", status=201) - else: - return Response("Save error", status=500) - else: - msg = "Command not valid: {}".format(serializer.errors) - logger.error(msg) - return Response({"msg": msg}, status=401) - - def list(self, request, *args, **kwargs): - queryset = self.multi_command_storage.filter() - serializer = self.serializer_class(queryset, many=True) - return Response(serializer.data) - - -class SessionReplayViewSet(viewsets.ViewSet): - serializer_class = ReplaySerializer - permission_classes = (IsOrgAdminOrAppUser,) - session = None - upload_to = 'replay' # 仅添加到本地存储中 - - def get_session_path(self, version=2): - """ - 获取session日志的文件路径 - :param version: 原来后缀是 .gz,为了统一新版本改为 .replay.gz - :return: - """ - suffix = '.replay.gz' - if version == 1: - suffix = '.gz' - date = self.session.date_start.strftime('%Y-%m-%d') - return os.path.join(date, str(self.session.id) + suffix) - - def get_local_path(self, version=2): - session_path = self.get_session_path(version=version) - if version == 2: - local_path = os.path.join(self.upload_to, session_path) - else: - local_path = session_path - return local_path - - def save_to_storage(self, f): - local_path = self.get_local_path() - try: - name = default_storage.save(local_path, f) - return name, None - except OSError as e: - return None, e - - def create(self, request, *args, **kwargs): - session_id = kwargs.get('pk') - self.session = get_object_or_404(Session, id=session_id) - serializer = self.serializer_class(data=request.data) - - if serializer.is_valid(): - file = serializer.validated_data['file'] - name, err = self.save_to_storage(file) - if not name: - msg = "Failed save replay `{}`: {}".format(session_id, err) - logger.error(msg) - return Response({'msg': str(err)}, status=400) - url = default_storage.url(name) - return Response({'url': url}, status=201) - else: - msg = 'Upload data invalid: {}'.format(serializer.errors) - logger.error(msg) - return Response({'msg': serializer.errors}, status=401) - - def retrieve(self, request, *args, **kwargs): - session_id = kwargs.get('pk') - self.session = get_object_or_404(Session, id=session_id) - - # 新版本和老版本的文件后缀不同 - session_path = self.get_session_path() # 存在外部存储上的路径 - local_path = self.get_local_path() - local_path_v1 = self.get_local_path(version=1) - - # 去default storage中查找 - for _local_path in (local_path, local_path_v1, session_path): - if default_storage.exists(_local_path): - url = default_storage.url(_local_path) - return redirect(url) - - # 去定义的外部storage查找 - configs = settings.TERMINAL_REPLAY_STORAGE - configs = {k: v for k, v in configs.items() if v['TYPE'] != 'server'} - if not configs: - return HttpResponseNotFound() - - target_path = os.path.join(default_storage.base_location, local_path) # 保存到storage的路径 - target_dir = os.path.dirname(target_path) - if not os.path.isdir(target_dir): - os.makedirs(target_dir, exist_ok=True) - storage = jms_storage.get_multi_object_storage(configs) - ok, err = storage.download(session_path, target_path) - if not ok: - logger.error("Failed download replay file: {}".format(err)) - return HttpResponseNotFound() - return redirect(default_storage.url(local_path)) - - -class SessionReplayV2ViewSet(SessionReplayViewSet): - serializer_class = ReplaySerializer - permission_classes = (IsOrgAdminOrAppUser,) - session = None - - def retrieve(self, request, *args, **kwargs): - response = super().retrieve(request, *args, **kwargs) - data = { - 'type': 'guacamole' if self.session.protocol == 'rdp' else 'json', - 'src': '', - } - if isinstance(response, HttpResponseRedirectBase): - data['src'] = response.url - return Response(data) - return HttpResponseNotFound() - - -class TerminalConfig(APIView): - permission_classes = (IsAppUser,) - - def get(self, request): - user = request.user - terminal = user.terminal - configs = terminal.config - return Response(configs, status=200) diff --git a/apps/terminal/api/__init__.py b/apps/terminal/api/__init__.py new file mode 100644 index 000000000..ec51c5a2b --- /dev/null +++ b/apps/terminal/api/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# diff --git a/apps/terminal/api/v1/__init__.py b/apps/terminal/api/v1/__init__.py new file mode 100644 index 000000000..21e78a2d7 --- /dev/null +++ b/apps/terminal/api/v1/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# +from .terminal import * +from .session import * +from .task import * diff --git a/apps/terminal/api/v1/session.py b/apps/terminal/api/v1/session.py new file mode 100644 index 000000000..de3e09a55 --- /dev/null +++ b/apps/terminal/api/v1/session.py @@ -0,0 +1,184 @@ +# -*- coding: utf-8 -*- +# +import logging +import os + +from django.shortcuts import get_object_or_404 +from django.core.files.storage import default_storage +from django.http import HttpResponseNotFound +from django.conf import settings +from rest_framework.pagination import LimitOffsetPagination +from rest_framework import viewsets +from rest_framework.views import Response +from rest_framework_bulk import BulkModelViewSet +import jms_storage + + +from common.utils import is_uuid +from common.permissions import IsOrgAdminOrAppUser +from ...hands import SystemUser +from ...models import Terminal, Session +from ...serializers import v1 as serializers +from ...backends import get_command_storage, get_multi_command_storage, \ + SessionCommandSerializer + +__all__ = ['SessionViewSet', 'SessionReplayViewSet', 'CommandViewSet'] +logger = logging.getLogger(__file__) + + +class SessionViewSet(BulkModelViewSet): + queryset = Session.objects.all() + serializer_class = serializers.SessionSerializer + pagination_class = LimitOffsetPagination + permission_classes = (IsOrgAdminOrAppUser,) + + def get_queryset(self): + terminal_id = self.kwargs.get("terminal", None) + if terminal_id: + terminal = get_object_or_404(Terminal, id=terminal_id) + self.queryset = terminal.session_set.all() + return self.queryset.all() + + def perform_create(self, serializer): + if hasattr(self.request.user, 'terminal'): + serializer.validated_data["terminal"] = self.request.user.terminal + sid = serializer.validated_data["system_user"] + if is_uuid(sid): + _system_user = SystemUser.get_system_user_by_id_or_cached(sid) + if _system_user: + serializer.validated_data["system_user"] = _system_user.name + return super().perform_create(serializer) + + +class CommandViewSet(viewsets.ViewSet): + """接受app发送来的command log, 格式如下 + { + "user": "admin", + "asset": "localhost", + "system_user": "web", + "session": "xxxxxx", + "input": "whoami", + "output": "d2hvbWFp", # base64.b64encode(s) + "timestamp": 1485238673.0 + } + + """ + command_store = get_command_storage() + multi_command_storage = get_multi_command_storage() + serializer_class = SessionCommandSerializer + permission_classes = (IsOrgAdminOrAppUser,) + + def get_queryset(self): + self.command_store.filter(**dict(self.request.query_params)) + + def create(self, request, *args, **kwargs): + serializer = self.serializer_class(data=request.data, many=True) + if serializer.is_valid(): + ok = self.command_store.bulk_save(serializer.validated_data) + if ok: + return Response("ok", status=201) + else: + return Response("Save error", status=500) + else: + msg = "Command not valid: {}".format(serializer.errors) + logger.error(msg) + return Response({"msg": msg}, status=401) + + def list(self, request, *args, **kwargs): + queryset = self.multi_command_storage.filter() + serializer = self.serializer_class(queryset, many=True) + return Response(serializer.data) + + +class SessionReplayViewSet(viewsets.ViewSet): + serializer_class = serializers.ReplaySerializer + permission_classes = (IsOrgAdminOrAppUser,) + session = None + upload_to = 'replay' # 仅添加到本地存储中 + + def get_session_path(self, version=2): + """ + 获取session日志的文件路径 + :param version: 原来后缀是 .gz,为了统一新版本改为 .replay.gz + :return: + """ + suffix = '.replay.gz' + if version == 1: + suffix = '.gz' + date = self.session.date_start.strftime('%Y-%m-%d') + return os.path.join(date, str(self.session.id) + suffix) + + def get_local_path(self, version=2): + session_path = self.get_session_path(version=version) + if version == 2: + local_path = os.path.join(self.upload_to, session_path) + else: + local_path = session_path + return local_path + + def save_to_storage(self, f): + local_path = self.get_local_path() + try: + name = default_storage.save(local_path, f) + return name, None + except OSError as e: + return None, e + + def create(self, request, *args, **kwargs): + session_id = kwargs.get('pk') + self.session = get_object_or_404(Session, id=session_id) + serializer = self.serializer_class(data=request.data) + + if serializer.is_valid(): + file = serializer.validated_data['file'] + name, err = self.save_to_storage(file) + if not name: + msg = "Failed save replay `{}`: {}".format(session_id, err) + logger.error(msg) + return Response({'msg': str(err)}, status=400) + url = default_storage.url(name) + return Response({'url': url}, status=201) + else: + msg = 'Upload data invalid: {}'.format(serializer.errors) + logger.error(msg) + return Response({'msg': serializer.errors}, status=401) + + def retrieve(self, request, *args, **kwargs): + session_id = kwargs.get('pk') + self.session = get_object_or_404(Session, id=session_id) + + data = { + 'type': 'guacamole' if self.session.protocol == 'rdp' else 'json', + 'src': '', + } + + # 新版本和老版本的文件后缀不同 + session_path = self.get_session_path() # 存在外部存储上的路径 + local_path = self.get_local_path() + local_path_v1 = self.get_local_path(version=1) + + # 去default storage中查找 + for _local_path in (local_path, local_path_v1, session_path): + if default_storage.exists(_local_path): + url = default_storage.url(_local_path) + data['src'] = url + return Response(data) + + # 去定义的外部storage查找 + configs = settings.TERMINAL_REPLAY_STORAGE + configs = {k: v for k, v in configs.items() if v['TYPE'] != 'server'} + if not configs: + return HttpResponseNotFound() + + target_path = os.path.join(default_storage.base_location, local_path) # 保存到storage的路径 + target_dir = os.path.dirname(target_path) + if not os.path.isdir(target_dir): + os.makedirs(target_dir, exist_ok=True) + storage = jms_storage.get_multi_object_storage(configs) + ok, err = storage.download(session_path, target_path) + if not ok: + logger.error("Failed download replay file: {}".format(err)) + return HttpResponseNotFound() + data['src'] = default_storage.url(local_path) + return Response(data) + diff --git a/apps/terminal/api/v1/task.py b/apps/terminal/api/v1/task.py new file mode 100644 index 000000000..f94096595 --- /dev/null +++ b/apps/terminal/api/v1/task.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# +import logging +from rest_framework.views import APIView, Response +from rest_framework_bulk import BulkModelViewSet + +from common.utils import get_object_or_none +from common.permissions import IsOrgAdminOrAppUser +from ...models import Session, Task +from ...serializers import v1 as serializers + + +__all__ = ['TaskViewSet', 'KillSessionAPI'] +logger = logging.getLogger(__file__) + + +class TaskViewSet(BulkModelViewSet): + queryset = Task.objects.all() + serializer_class = serializers.TaskSerializer + permission_classes = (IsOrgAdminOrAppUser,) + + +class KillSessionAPI(APIView): + permission_classes = (IsOrgAdminOrAppUser,) + model = Task + + def post(self, request, *args, **kwargs): + validated_session = [] + for session_id in request.data: + session = get_object_or_none(Session, id=session_id) + if session and not session.is_finished: + validated_session.append(session_id) + self.model.objects.create( + name="kill_session", args=session.id, + terminal=session.terminal, + ) + return Response({"ok": validated_session}) diff --git a/apps/terminal/api/v1/terminal.py b/apps/terminal/api/v1/terminal.py new file mode 100644 index 000000000..744aa933c --- /dev/null +++ b/apps/terminal/api/v1/terminal.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +# +from collections import OrderedDict +import logging +import uuid + +from django.core.cache import cache +from django.shortcuts import get_object_or_404, redirect +from django.utils import timezone +from rest_framework import viewsets +from rest_framework.views import APIView, Response +from rest_framework.permissions import AllowAny + + +from common.utils import get_object_or_none +from common.permissions import IsAppUser, IsOrgAdminOrAppUser, IsSuperUser +from ...models import Terminal, Status, Session +from ...serializers import v1 as serializers + +__all__ = [ + 'TerminalViewSet', 'TerminalTokenApi', 'StatusViewSet', 'TerminalConfig', +] +logger = logging.getLogger(__file__) + + +class TerminalViewSet(viewsets.ModelViewSet): + queryset = Terminal.objects.filter(is_deleted=False) + serializer_class = serializers.TerminalSerializer + permission_classes = (IsSuperUser,) + + def create(self, request, *args, **kwargs): + name = request.data.get('name') + remote_ip = request.META.get('REMOTE_ADDR') + 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, is_deleted=False) + if terminal: + msg = 'Terminal name %s already used' % name + return Response({'msg': msg}, status=409) + + serializer = self.serializer_class(data={ + 'name': name, 'remote_addr': remote_addr + }) + + if serializer.is_valid(): + terminal = serializer.save() + + # 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 + logger.error("Register terminal error: {}".format(data)) + return Response(data, status=400) + + def get_permissions(self): + if self.action == "create": + self.permission_classes = (AllowAny,) + 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 = serializers.StatusSerializer + permission_classes = (IsOrgAdminOrAppUser,) + session_serializer_class = serializers.SessionSerializer + task_serializer_class = serializers.TaskSerializer + + def create(self, request, *args, **kwargs): + from_gua = self.request.query_params.get("from_guacamole", None) + if not from_gua: + self.handle_sessions() + super().create(request, *args, **kwargs) + tasks = self.request.user.terminal.task_set.filter(is_finished=False) + serializer = self.task_serializer_class(tasks, many=True) + return Response(serializer.data, status=201) + + def handle_sessions(self): + sessions_active = [] + for session_data in self.request.data.get("sessions", []): + self.create_or_update_session(session_data) + if not session_data["is_finished"]: + sessions_active.append(session_data["id"]) + + sessions_in_db_active = Session.objects.filter( + is_finished=False, + terminal=self.request.user.terminal.id + ) + + for session in sessions_in_db_active: + if str(session.id) not in sessions_active: + session.is_finished = True + session.date_end = timezone.now() + session.save() + + def create_or_update_session(self, session_data): + session_data["terminal"] = self.request.user.terminal.id + _id = session_data["id"] + session = get_object_or_none(Session, id=_id) + if session: + serializer = serializers.SessionSerializer( + data=session_data, instance=session + ) + else: + serializer = serializers.SessionSerializer(data=session_data) + + if serializer.is_valid(): + session = serializer.save() + return session + else: + msg = "session data is not valid {}: {}".format( + serializer.errors, str(serializer.data) + ) + logger.error(msg) + return None + + def get_queryset(self): + terminal_id = self.kwargs.get("terminal", None) + if terminal_id: + terminal = get_object_or_404(Terminal, id=terminal_id) + self.queryset = terminal.status_set.all() + return self.queryset + + def perform_create(self, serializer): + serializer.validated_data["terminal"] = self.request.user.terminal + return super().perform_create(serializer) + + def get_permissions(self): + if self.action == "create": + self.permission_classes = (IsAppUser,) + return super().get_permissions() + + +class TerminalConfig(APIView): + permission_classes = (IsAppUser,) + + def get(self, request): + user = request.user + terminal = user.terminal + configs = terminal.config + return Response(configs, status=200) \ No newline at end of file diff --git a/apps/terminal/api/v2/__init__.py b/apps/terminal/api/v2/__init__.py new file mode 100644 index 000000000..0dfa92ec1 --- /dev/null +++ b/apps/terminal/api/v2/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +# +from .terminal import * diff --git a/apps/terminal/api/v2/terminal.py b/apps/terminal/api/v2/terminal.py new file mode 100644 index 000000000..3fd42a6ab --- /dev/null +++ b/apps/terminal/api/v2/terminal.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# +from rest_framework import viewsets + +from common.permissions import IsSuperUser, WithBootstrapToken +from ...models import Terminal +from ...serializers import v2 as serializers + +__all__ = ['TerminalViewSet', 'TerminalRegistrationViewSet'] + + +class TerminalViewSet(viewsets.ModelViewSet): + queryset = Terminal.objects.filter(is_deleted=False) + serializer_class = serializers.TerminalSerializer + permission_classes = [IsSuperUser] + + +class TerminalRegistrationViewSet(viewsets.ModelViewSet): + queryset = Terminal.objects.filter(is_deleted=False) + serializer_class = serializers.TerminalRegistrationSerializer + permission_classes = [WithBootstrapToken] + http_method_names = ['post'] diff --git a/apps/terminal/models.py b/apps/terminal/models.py index 27d5af402..661b4a57d 100644 --- a/apps/terminal/models.py +++ b/apps/terminal/models.py @@ -16,7 +16,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, verbose_name=_('Name')) - remote_addr = models.CharField(max_length=128, verbose_name=_('Remote Address')) + remote_addr = models.CharField(max_length=128, blank=True, verbose_name=_('Remote Address')) ssh_port = models.IntegerField(verbose_name=_('SSH Port'), default=2222) http_port = models.IntegerField(verbose_name=_('HTTP Port'), default=5000) command_storage = models.CharField(max_length=128, verbose_name=_("Command storage"), default='default') @@ -68,6 +68,10 @@ class Terminal(models.Model): }) return configs + @property + def service_account(self): + return self.user + def create_app_user(self): random = uuid.uuid4().hex[:6] user, access_key = User.create_app_user( diff --git a/apps/terminal/serializers/__init__.py b/apps/terminal/serializers/__init__.py new file mode 100644 index 000000000..ec51c5a2b --- /dev/null +++ b/apps/terminal/serializers/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# diff --git a/apps/terminal/serializers.py b/apps/terminal/serializers/v1.py similarity index 93% rename from apps/terminal/serializers.py rename to apps/terminal/serializers/v1.py index 6933e2896..25cd0cd1e 100644 --- a/apps/terminal/serializers.py +++ b/apps/terminal/serializers/v1.py @@ -1,14 +1,12 @@ # -*- coding: utf-8 -*- # - from django.core.cache import cache -from django.utils import timezone from rest_framework import serializers from rest_framework_bulk.serializers import BulkListSerializer from common.mixins import BulkSerializerMixin -from .models import Terminal, Status, Session, Task -from .backends import get_multi_command_storage +from ..models import Terminal, Status, Session, Task +from ..backends import get_multi_command_storage class TerminalSerializer(serializers.ModelSerializer): @@ -33,6 +31,8 @@ class TerminalSerializer(serializers.ModelSerializer): return cache.get(key) + + class SessionSerializer(BulkSerializerMixin, serializers.ModelSerializer): command_amount = serializers.SerializerMethodField() command_store = get_multi_command_storage() @@ -71,3 +71,4 @@ class TaskSerializer(BulkSerializerMixin, serializers.ModelSerializer): class ReplaySerializer(serializers.Serializer): file = serializers.FileField() + diff --git a/apps/terminal/serializers/v2.py b/apps/terminal/serializers/v2.py new file mode 100644 index 000000000..7e0365800 --- /dev/null +++ b/apps/terminal/serializers/v2.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# +from rest_framework import serializers + +from common.utils import get_request_ip +from users.serializers.v2 import ServiceAccountRegistrationSerializer +from ..models import Terminal + + +__all__ = ['TerminalSerializer', 'TerminalRegistrationSerializer'] + + +class TerminalSerializer(serializers.ModelSerializer): + class Meta: + model = Terminal + fields = [ + 'id', 'name', 'remote_addr', 'comment', + ] + read_only_fields = ['id', 'remote_addr'] + + +class TerminalRegistrationSerializer(serializers.ModelSerializer): + service_account = ServiceAccountRegistrationSerializer(read_only=True) + service_account_serializer = None + + class Meta: + model = Terminal + fields = [ + 'id', 'name', 'remote_addr', 'comment', 'service_account' + ] + read_only_fields = ['id', 'remote_addr', 'service_account'] + + def validate(self, attrs): + self.service_account_serializer = ServiceAccountRegistrationSerializer(data=attrs) + self.service_account_serializer.is_valid(raise_exception=True) + return attrs + + def create(self, validated_data): + request = self.context.get('request') + sa = self.service_account_serializer.save() + instance = super().create(validated_data) + instance.is_accepted = True + instance.user = sa + instance.remote_addr = get_request_ip(request) + instance.save() + return instance diff --git a/apps/terminal/urls/api_urls.py b/apps/terminal/urls/api_urls.py index bdb39b432..50be22a8c 100644 --- a/apps/terminal/urls/api_urls.py +++ b/apps/terminal/urls/api_urls.py @@ -2,10 +2,10 @@ # -*- coding: utf-8 -*- # -from django.urls import path +from django.urls import path, include from rest_framework_bulk.routes import BulkRouter -from .. import api +from ..api import v1 as api app_name = 'terminal' @@ -20,7 +20,7 @@ router.register(r'status', api.StatusViewSet, 'status') urlpatterns = [ path('sessions//replay/', - api.SessionReplayV2ViewSet.as_view({'get': 'retrieve', 'post': 'create'}), + api.SessionReplayViewSet.as_view({'get': 'retrieve', 'post': 'create'}), name='session-replay'), path('tasks/kill-session/', api.KillSessionAPI.as_view(), name='kill-session'), path('terminal//access-key/', api.TerminalTokenApi.as_view(), @@ -33,3 +33,6 @@ urlpatterns = [ ] urlpatterns += router.urls + + + diff --git a/apps/terminal/urls/api_urls_v2.py b/apps/terminal/urls/api_urls_v2.py new file mode 100644 index 000000000..600ad3611 --- /dev/null +++ b/apps/terminal/urls/api_urls_v2.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# + +from django.urls import path +from rest_framework_bulk.routes import BulkRouter + +from ..api import v2 as api + +app_name = 'terminal' + +router = BulkRouter() +router.register(r'terminal', api.TerminalViewSet, 'terminal') +router.register(r'terminal-registrations', api.TerminalRegistrationViewSet, 'terminal-registration') + + +urlpatterns = [ +] + +urlpatterns += router.urls diff --git a/apps/users/api/auth.py b/apps/users/api/auth.py index 4f9d7cb1d..c4ed7ff3a 100644 --- a/apps/users/api/auth.py +++ b/apps/users/api/auth.py @@ -17,8 +17,8 @@ from orgs.mixins import RootOrgViewMixin from ..serializers import UserSerializer from ..tasks import write_login_log_async from ..models import User, LoginLog -from ..utils import check_user_valid, generate_token, \ - check_otp_code, increase_login_failed_count, is_block_login, \ +from ..utils import check_user_valid, check_otp_code, \ + increase_login_failed_count, is_block_login, \ clean_failed_count from ..hands import Asset, SystemUser @@ -79,7 +79,7 @@ class UserAuthApi(RootOrgViewMixin, APIView): self.write_login_log(request, data) # 登陆成功,清除原来的缓存计数 clean_failed_count(username, ip) - token = generate_token(request, user) + token = user.create_bearer_token(request) return Response( {'token': token, 'user': self.serializer_class(user).data} ) @@ -123,7 +123,6 @@ class UserAuthApi(RootOrgViewMixin, APIView): 'user_agent': user_agent, } data.update(tmp_data) - write_login_log_async.delay(**data) @@ -185,7 +184,7 @@ class UserToken(APIView): user = request.user msg = None if user: - token = generate_token(request, user) + token = user.create_bearer_token(request) return Response({'Token': token, 'Keyword': 'Bearer'}, status=200) else: return Response({'error': msg}, status=406) @@ -223,7 +222,7 @@ class UserOtpAuthApi(RootOrgViewMixin, APIView): 'status': True } self.write_login_log(request, data) - token = generate_token(request, user) + token = user.create_bearer_token(request) return Response( { 'token': token, diff --git a/apps/users/api/user.py b/apps/users/api/user.py index 6bc0d4622..ca0e5a837 100644 --- a/apps/users/api/user.py +++ b/apps/users/api/user.py @@ -11,13 +11,14 @@ from rest_framework.permissions import IsAuthenticated from rest_framework_bulk import BulkModelViewSet from rest_framework.pagination import LimitOffsetPagination +from common.permissions import IsOrgAdmin, IsCurrentUserOrReadOnly, \ + IsOrgAdminOrAppUser +from common.mixins import IDInFilterMixin +from common.utils import get_logger +from orgs.utils import current_org from ..serializers import UserSerializer, UserPKUpdateSerializer, \ UserUpdateGroupSerializer, ChangeUserPasswordSerializer from ..models import User -from orgs.utils import current_org -from common.permissions import IsOrgAdmin, IsCurrentUserOrReadOnly, IsOrgAdminOrAppUser -from common.mixins import IDInFilterMixin -from common.utils import get_logger logger = get_logger(__name__) @@ -31,15 +32,16 @@ __all__ = [ class UserViewSet(IDInFilterMixin, BulkModelViewSet): filter_fields = ('username', 'email', 'name', 'id') search_fields = filter_fields - queryset = User.objects.exclude(role="App") + queryset = User.objects.all() serializer_class = UserSerializer permission_classes = (IsOrgAdmin,) pagination_class = LimitOffsetPagination def get_queryset(self): queryset = super().get_queryset() - org_users = current_org.get_org_users() - queryset = queryset.filter(id__in=org_users) + if current_org.is_real(): + org_users = current_org.get_org_users() + queryset = queryset.filter(id__in=org_users) return queryset def get_permissions(self): diff --git a/apps/users/api/v2/__init__.py b/apps/users/api/v2/__init__.py new file mode 100644 index 000000000..3c486a98c --- /dev/null +++ b/apps/users/api/v2/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# + +from .user import * diff --git a/apps/users/api/v2/user.py b/apps/users/api/v2/user.py new file mode 100644 index 000000000..6c883861e --- /dev/null +++ b/apps/users/api/v2/user.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# +from rest_framework import viewsets + +from common.permissions import WithBootstrapToken +from ...serializers import v2 as serializers + + +class ServiceAccountRegistrationViewSet(viewsets.ModelViewSet): + serializer_class = serializers.ServiceAccountRegistrationSerializer + permission_classes = (WithBootstrapToken,) + http_method_names = ['post'] diff --git a/apps/users/authentication.py b/apps/users/authentication.py index 647f9a776..5faa7bb60 100644 --- a/apps/users/authentication.py +++ b/apps/users/authentication.py @@ -1,9 +1,7 @@ # -*- coding: utf-8 -*- # -import base64 import uuid -import hashlib import time from django.core.cache import cache @@ -12,11 +10,10 @@ from django.utils.translation import ugettext as _ from django.utils.six import text_type from django.utils.translation import ugettext_lazy as _ from rest_framework import HTTP_HEADER_ENCODING -from rest_framework import authentication, exceptions, permissions +from rest_framework import authentication, exceptions from rest_framework.authentication import CSRFCheck from common.utils import get_object_or_none, make_signature, http_to_unixtime -from .utils import refresh_token from .models import User, AccessKey, PrivateToken @@ -144,7 +141,6 @@ class AccessTokenAuthentication(authentication.BaseAuthentication): if not user: msg = _('Invalid token or cache refreshed.') raise exceptions.AuthenticationFailed(msg) - refresh_token(token, user) return user, None diff --git a/apps/users/models/authentication.py b/apps/users/models/authentication.py index cd5d27c4c..9bf1fdcb6 100644 --- a/apps/users/models/authentication.py +++ b/apps/users/models/authentication.py @@ -17,7 +17,7 @@ class AccessKey(models.Model): secret = models.UUIDField(verbose_name='AccessKeySecret', default=uuid.uuid4, editable=False) user = models.ForeignKey(User, verbose_name='User', - on_delete=models.CASCADE, related_name='access_key') + on_delete=models.CASCADE, related_name='access_keys') def get_id(self): return str(self.id) @@ -25,6 +25,9 @@ class AccessKey(models.Model): def get_secret(self): return str(self.secret) + def get_full_value(self): + return '{}:{}'.format(self.id, self.secret) + def __str__(self): return str(self.id) diff --git a/apps/users/models/user.py b/apps/users/models/user.py index f0e0355a3..18781db6d 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -2,19 +2,20 @@ # -*- coding: utf-8 -*- # import uuid +import base64 from collections import OrderedDict from django.conf import settings from django.contrib.auth.hashers import make_password -from django.contrib.auth.models import AbstractUser, UserManager +from django.contrib.auth.models import AbstractUser from django.core import signing +from django.core.cache import cache from django.db import models from django.utils.translation import ugettext_lazy as _ from django.utils import timezone from django.shortcuts import reverse from common.utils import get_signer, date_expired_default -from orgs.mixins import OrgManager from orgs.utils import current_org @@ -274,15 +275,38 @@ class User(AbstractUser): token = PrivateToken.objects.create(user=self) return token.key + def refresh_private_token(self): + from .authentication import PrivateToken + PrivateToken.objects.filter(user=self).delete() + return PrivateToken.objects.create(user=self) + + def create_bearer_token(self, request=None): + expiration = settings.TOKEN_EXPIRATION or 3600 + if request: + remote_addr = request.META.get('REMOTE_ADDR', '') + else: + remote_addr = '0.0.0.0' + if not isinstance(remote_addr, bytes): + remote_addr = remote_addr.encode("utf-8") + remote_addr = base64.b16encode(remote_addr) # .replace(b'=', '') + token = cache.get('%s_%s' % (self.id, remote_addr)) + if not token: + token = uuid.uuid4().hex + cache.set(token, self.id, expiration) + cache.set('%s_%s' % (self.id, remote_addr), token, expiration) + return token + + def refresh_bearer_token(self, token): + pass + def create_access_key(self): from . import AccessKey access_key = AccessKey.objects.create(user=self) return access_key - def refresh_private_token(self): - from .authentication import PrivateToken - PrivateToken.objects.filter(user=self).delete() - return PrivateToken.objects.create(user=self) + @property + def access_key(self): + return self.access_keys.first() def is_member_of(self, user_group): if user_group in self.groups.all(): @@ -345,7 +369,8 @@ class User(AbstractUser): 'phone': self.phone, 'otp_level': self.otp_level, 'comment': self.comment, - 'date_expired': self.date_expired.strftime('%Y-%m-%d %H:%M:%S') if self.date_expired is not None else None + 'date_expired': self.date_expired.strftime('%Y-%m-%d %H:%M:%S') \ + if self.date_expired is not None else None }) @classmethod diff --git a/apps/users/serializers/__init__.py b/apps/users/serializers/__init__.py new file mode 100644 index 000000000..94ef71f28 --- /dev/null +++ b/apps/users/serializers/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +# +from .v1 import * \ No newline at end of file diff --git a/apps/users/serializers.py b/apps/users/serializers/v1.py similarity index 93% rename from apps/users/serializers.py rename to apps/users/serializers/v1.py index bef37d91f..cf0e75d1f 100644 --- a/apps/users/serializers.py +++ b/apps/users/serializers/v1.py @@ -7,14 +7,16 @@ from rest_framework_bulk import BulkListSerializer from common.utils import get_signer, validate_ssh_public_key from common.mixins import BulkSerializerMixin -from .models import User, UserGroup +from ..models import User, UserGroup signer = get_signer() class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer): groups_display = serializers.SerializerMethodField() - groups = serializers.PrimaryKeyRelatedField(many=True, queryset = UserGroup.objects.all(), required=False) + groups = serializers.PrimaryKeyRelatedField( + many=True, queryset=UserGroup.objects.all(), required=False + ) class Meta: model = User diff --git a/apps/users/serializers/v2.py b/apps/users/serializers/v2.py new file mode 100644 index 000000000..f9931d5dc --- /dev/null +++ b/apps/users/serializers/v2.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# +from rest_framework import serializers +from ..models import User, AccessKey + + +class AccessKeySerializer(serializers.ModelSerializer): + + class Meta: + model = AccessKey + fields = ['id', 'secret'] + read_only_fields = ['id', 'secret'] + + +class ServiceAccountRegistrationSerializer(serializers.ModelSerializer): + access_key = AccessKeySerializer(read_only=True) + + class Meta: + model = User + fields = ['id', 'name', 'access_key'] + read_only_fields = ['id', 'access_key'] + + def get_username(self): + return self.initial_data.get('name') + + def get_email(self): + name = self.initial_data.get('name') + return '{}@serviceaccount.local'.format(name) + + def validate_name(self, name): + email = self.get_email() + username = self.get_username() + if User.objects.filter(email=email) or \ + User.objects.filter(username=username): + raise serializers.ValidationError('name not unique', code='unique') + return name + + def create(self, validated_data): + validated_data['email'] = self.get_email() + validated_data['username'] = self.get_username() + validated_data['role'] = User.ROLE_APP + instance = super().create(validated_data) + instance.create_access_key() + return instance diff --git a/apps/users/urls/api_urls.py b/apps/users/urls/api_urls.py index 7e798e5ef..5a4ca80e3 100644 --- a/apps/users/urls/api_urls.py +++ b/apps/users/urls/api_urls.py @@ -15,8 +15,7 @@ router.register(r'groups', api.UserGroupViewSet, 'user-group') urlpatterns = [ - # path(r'', api.UserListView.as_view()), - path('token/', api.UserToken.as_view(), name='user-token'), + # path('token/', api.UserToken.as_view(), name='user-token'), path('connection-token/', api.UserConnectionTokenApi.as_view(), name='connection-token'), path('profile/', api.UserProfileApi.as_view(), name='user-profile'), path('auth/', api.UserAuthApi.as_view(), name='user-auth'), @@ -31,5 +30,6 @@ urlpatterns = [ path('users//groups/', api.UserUpdateGroupApi.as_view(), name='user-update-group'), path('groups//users/', api.UserGroupUpdateUserApi.as_view(), name='user-group-update-user'), ] - urlpatterns += router.urls + + diff --git a/apps/users/urls/api_urls_v2.py b/apps/users/urls/api_urls_v2.py new file mode 100644 index 000000000..f2a42729c --- /dev/null +++ b/apps/users/urls/api_urls_v2.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +# ~*~ coding: utf-8 ~*~ +# +from __future__ import absolute_import + +from django.urls import path, include +from rest_framework_bulk.routes import BulkRouter +from ..api import v2 as api + +app_name = 'users' + +router = BulkRouter() +router.register(r'service-account-registrations', + api.ServiceAccountRegistrationViewSet, + 'service-account-registration') + + +urlpatterns = [ + # path('token/', api.UserToken.as_view(), name='user-token'), +] +urlpatterns += router.urls + + diff --git a/apps/users/utils.py b/apps/users/utils.py index 171f0a651..39ef7c34f 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -202,24 +202,6 @@ def check_user_valid(**kwargs): return None, _('Password or SSH public key invalid') -def refresh_token(token, user, expiration=settings.TOKEN_EXPIRATION or 3600): - cache.set(token, user.id, expiration) - - -def generate_token(request, user): - expiration = settings.TOKEN_EXPIRATION or 3600 - remote_addr = request.META.get('REMOTE_ADDR', '') - if not isinstance(remote_addr, bytes): - remote_addr = remote_addr.encode("utf-8") - remote_addr = base64.b16encode(remote_addr) # .replace(b'=', '') - token = cache.get('%s_%s' % (user.id, remote_addr)) - if not token: - token = uuid.uuid4().hex - cache.set(token, user.id, expiration) - cache.set('%s_%s' % (user.id, remote_addr), token, expiration) - return token - - def validate_ip(ip): try: ipaddress.ip_address(ip) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 901a55aba..66a86114d 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -76,3 +76,4 @@ aliyun-python-sdk-core-v3==2.9.1 aliyun-python-sdk-ecs==4.10.1 python-keycloak==0.13.3 python-keycloak-client==0.1.3 +rest_condition==1.0.3 diff --git a/utils/create_assets_user/admin_users.txt b/utils/create_assets_user/admin_users.txt new file mode 100644 index 000000000..8d2e0cf87 --- /dev/null +++ b/utils/create_assets_user/admin_users.txt @@ -0,0 +1,3 @@ +# 名称 用户名 密码 +test123 testq12 test123123123 + diff --git a/utils/create_assets_user/bulk_create_user.py b/utils/create_assets_user/bulk_create_user.py new file mode 100644 index 000000000..4a0a60e81 --- /dev/null +++ b/utils/create_assets_user/bulk_create_user.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python + +import requests +import sys + +admin_username = 'admin' +admin_password = 'admin' +domain_url = 'http://localhost:8080' + + +class UserCreation: + headers = {} + + def __init__(self, username, password, domain): + self.username = username + self.password = password + self.domain = domain + + def auth(self): + url = "{}/api/users/v1/token/".format(self.domain) + data = {"username": self.username, "password": self.password} + resp = requests.post(url, data=data) + if resp.status_code == 200: + data = resp.json() + self.headers.update({ + 'Authorization': '{} {}'.format(data['Keyword'], data['Token']) + }) + else: + print("用户名 或 密码 或 地址 不对") + sys.exit(2) + + def get_user_detail(self, name, url): + resp = requests.get(url, headers=self.headers) + if resp.status_code == 200: + data = resp.json() + if len(data) < 1: + return None + for d in data: + if d['name'] == name: + return d + return None + return None + + def get_system_user_detail(self, name): + url = '{}/api/assets/v1/system-user/?name={}'.format(self.domain, name) + return self.get_user_detail(name, url) + + def create_system_user(self, info): + system_user = self.get_system_user_detail(info.get('name')) + if system_user: + return system_user + url = '{}/api/assets/v1/system-user/'.format(self.domain) + resp = requests.post(url, data=info, headers=self.headers, json=False) + if resp.status_code == 201: + return resp.json() + else: + print("创建系统用户失败: {} {}".format(info['name'], resp.content)) + return None + + def set_system_user_auth(self, system_user, info): + url = '{}/api/assets/v1/system-user/{}/auth-info/'.format( + self.domain, system_user['id'] + ) + data = {'password': info.get('password')} + resp = requests.patch(url, data=data, headers=self.headers) + if resp.status_code > 300: + print("设置系统用户密码失败: {} {}".format( + system_user.get('name'), resp.content.decode() + )) + else: + return True + + def get_admin_user_detail(self, name): + url = '{}/api/assets/v1/admin-user/?name={}'.format(self.domain, name) + return self.get_user_detail(name, url) + + def create_admin_user(self, info): + admin_user = self.get_admin_user_detail(info.get('name')) + if admin_user: + return admin_user + url = '{}/api/assets/v1/admin-user/'.format(self.domain) + resp = requests.post(url, data=info, headers=self.headers, json=False) + if resp.status_code == 201: + return resp.json() + else: + print("创建管理用户失败: {} {}".format(info['name'], resp.content.decode())) + return None + + def set_admin_user_auth(self, admin_user, info): + url = '{}/api/assets/v1/admin-user/{}/auth/'.format( + self.domain, admin_user['id'] + ) + data = {'password': info.get('password')} + resp = requests.patch(url, data=data, headers=self.headers) + if resp.status_code > 300: + print("设置管理用户密码失败: {} {}".format( + admin_user.get('name'), resp.content.decode() + )) + else: + return True + + def create_system_users(self): + print("#"*10, " 开始创建系统用户 ", "#"*10) + users = [] + f = open('system_users.txt') + for line in f: + line = line.strip() + if not line or line.startswith('#'): + continue + name, username, password, protocol, auto_push = line.split()[:5] + info = { + "name": name, + "username": username, + "password": password, + "protocol": protocol, + "auto_push": bool(int(auto_push)), + "login_mode": "auto" + } + users.append(info) + + for i, info in enumerate(users, start=1): + system_user = self.create_system_user(info) + if system_user and self.set_system_user_auth(system_user, info): + print("[{}] 创建系统用户成功: {}".format(i, system_user['name'])) + + def create_admin_users(self): + print("\n", "#"*10, " 开始创建管理用户 ", "#"*10) + users = [] + f = open('admin_users.txt') + for line in f: + line = line.strip() + if not line or line.startswith('#'): + continue + name, username, password = line.split()[:3] + info = { + "name": name, + "username": username, + "password": password, + } + users.append(info) + for i, info in enumerate(users, start=1): + admin_user = self.create_admin_user(info) + if admin_user and self.set_admin_user_auth(admin_user, info): + print("[{}] 创建管理用户成功: {}".format(i, admin_user['name'])) + + +def main(): + api = UserCreation(username=admin_username, + password=admin_password, + domain=domain_url) + api.auth() + api.create_system_users() + api.create_admin_users() + + +if __name__ == '__main__': + main() + + diff --git a/utils/create_assets_user/system_users.txt b/utils/create_assets_user/system_users.txt new file mode 100644 index 000000000..b556908d7 --- /dev/null +++ b/utils/create_assets_user/system_users.txt @@ -0,0 +1,4 @@ +# 名称 用户名 密码 协议[ssh,rdp] 自动推送[0不推送,1自动推送] +test123 test123 test123123123 ssh 0 +test1323 test123 test123123123 ssh 0 + diff --git a/utils/create_assets_user/使用说明.txt b/utils/create_assets_user/使用说明.txt new file mode 100644 index 000000000..bae7431ce --- /dev/null +++ b/utils/create_assets_user/使用说明.txt @@ -0,0 +1,21 @@ +1. 安装依赖包 +$ pip install requests + +2. 设置账号密码和地址 +$ vim bulk_create_user.py # 设置为正确的值 +admin_username = 'admin' +admin_password = 'admin' +domain_url = 'http://localhost:8081' + +3. 配置需要添加的系统用户 +$ vim system_users.txt +# 名称 用户名 密码 +test123 testq12 test123123123 + +3. 配置需要添加的系统用户 +$ vim system_users.txt +# 名称 用户名 密码 协议[ssh,rdp] 自动推送[0不推送,1自动推送] +test123 test123 test123123123 ssh 0 + +4. 运行 +$ python bulk_create_user.py \ No newline at end of file