服务账号注册机制更改 (#2079)

* [Update] 服务账号注册

* [Update] 修改settings配置

* [Update] 修改settings

* [Update] 整理terminal api

* [Update] 修改terminal api

* [Update] 修改terminal注册机制
pull/2081/head^2
老广 2018-11-23 10:25:35 +08:00 committed by GitHub
parent 363985ee7a
commit 9cfcadc2f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 958 additions and 495 deletions

View File

@ -1,12 +1,10 @@
import json import json
import ldap
from django.db import models from django.db import models
from django.core.cache import cache from django.core.cache import cache
from django.db.utils import ProgrammingError, OperationalError from django.db.utils import ProgrammingError, OperationalError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.conf import settings from django.conf import settings
from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion
from .utils import get_signer from .utils import get_signer

View File

@ -5,6 +5,7 @@ from rest_framework import permissions
from django.contrib.auth.mixins import UserPassesTestMixin from django.contrib.auth.mixins import UserPassesTestMixin
from django.shortcuts import redirect from django.shortcuts import redirect
from django.http.response import HttpResponseForbidden from django.http.response import HttpResponseForbidden
from django.conf import settings
from orgs.utils import current_org from orgs.utils import current_org
@ -96,3 +97,12 @@ class SuperUserRequiredMixin(UserPassesTestMixin):
def test_func(self): def test_func(self):
if self.request.user.is_authenticated and self.request.user.is_superuser: if self.request.user.is_authenticated and self.request.user.is_superuser:
return True 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

View File

@ -30,6 +30,9 @@ CONFIG = load_user_config()
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = CONFIG.SECRET_KEY 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! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = CONFIG.DEBUG DEBUG = CONFIG.DEBUG
@ -499,9 +502,11 @@ USER_GUIDE_URL = ""
SWAGGER_SETTINGS = { SWAGGER_SETTINGS = {
'DEFAULT_AUTO_SCHEMA_CLASS': 'jumpserver.swagger.CustomSwaggerAutoSchema',
'SECURITY_DEFINITIONS': { 'SECURITY_DEFINITIONS': {
'basic': { 'basic': {
'type': 'basic' 'type': 'basic'
} }
}, },
} }

View File

@ -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

View File

@ -1,62 +1,18 @@
# ~*~ coding: utf-8 ~*~ # ~*~ coding: utf-8 ~*~
from __future__ import unicode_literals from __future__ import unicode_literals
import re
import os
from django.urls import path, include, re_path from django.urls import path, include, re_path
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from django.conf.urls.i18n import i18n_patterns from django.conf.urls.i18n import i18n_patterns
from django.views.i18n import JavaScriptCatalog 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 from .views import IndexView, LunaView, I18NView
from .swagger import get_swagger_view
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<version>\w+)/(?P<app>\w+)/(?P<extra>.*)$')
class HttpResponseTemporaryRedirect(HttpResponse): api_v1_patterns = [
status_code = 307 path('api/', include([
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('users/v1/', include('users.urls.api_urls', namespace='api-users')),
path('assets/v1/', include('assets.urls.api_urls', namespace='api-assets')), path('assets/v1/', include('assets.urls.api_urls', namespace='api-assets')),
path('perms/v1/', include('perms.urls.api_urls', namespace='api-perms')), path('perms/v1/', include('perms.urls.api_urls', namespace='api-perms')),
@ -65,6 +21,14 @@ v1_api_patterns = [
path('audits/v1/', include('audits.urls.api_urls', namespace='api-audits')), path('audits/v1/', include('audits.urls.api_urls', namespace='api-audits')),
path('orgs/v1/', include('orgs.urls.api_urls', namespace='api-orgs')), path('orgs/v1/', include('orgs.urls.api_urls', namespace='api-orgs')),
path('common/v1/', include('common.urls.api_urls', namespace='api-common')), 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 = [ app_view_patterns = [
@ -78,6 +42,7 @@ app_view_patterns = [
path('auth/', include('authentication.urls.view_urls'), name='auth'), path('auth/', include('authentication.urls.view_urls'), name='auth'),
] ]
if settings.XPACK_ENABLED: if settings.XPACK_ENABLED:
app_view_patterns.append(path('xpack/', include('xpack.urls', namespace='xpack'))) app_view_patterns.append(path('xpack/', include('xpack.urls', namespace='xpack')))
@ -87,12 +52,13 @@ js_i18n_patterns = i18n_patterns(
urlpatterns = [ urlpatterns = [
path('', IndexView.as_view(), name='index'), 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('luna/', LunaView.as_view(), name='luna-error'),
path('i18n/<str:lang>/', I18NView.as_view(), name='i18n-switch'), path('i18n/<str:lang>/', I18NView.as_view(), name='i18n-switch'),
path('settings/', include('common.urls.view_urls', namespace='settings')), path('settings/', include('common.urls.view_urls', namespace='settings')),
path('common/', include('common.urls.view_urls', namespace='common')), path('common/', include('common.urls.view_urls', namespace='common')),
path('api/v1/', redirect_format_api), # path('api/v2/', include(api_v2_patterns)),
path('api/', include(v1_api_patterns)),
# External apps url # External apps url
path('captcha/', include('captcha.urls')), path('captcha/', include('captcha.urls')),
@ -104,7 +70,13 @@ urlpatterns += js_i18n_patterns
if settings.DEBUG: if settings.DEBUG:
urlpatterns += [ urlpatterns += [
re_path('swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=None), name='schema-json'), re_path('^swagger(?P<format>\.json|\.yaml)$',
path('docs/', schema_view.with_ui('swagger', cache_timeout=None), name="docs"), get_swagger_view().without_ui(cache_timeout=1), name='schema-json'),
path('redoc/', schema_view.with_ui('redoc', cache_timeout=None), name='redoc'), 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<format>\.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'),
] ]

View File

@ -1,4 +1,5 @@
import datetime import datetime
import re
from django.http import HttpResponse, HttpResponseRedirect from django.http import HttpResponse, HttpResponseRedirect
from django.conf import settings 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.db.models import Count
from django.shortcuts import redirect from django.shortcuts import redirect
from django.contrib.auth.mixins import LoginRequiredMixin 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 users.models import User
from assets.models import Asset from assets.models import Asset
@ -188,3 +193,29 @@ class I18NView(View):
response = HttpResponseRedirect(referer_url) response = HttpResponseRedirect(referer_url)
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, lang) response.set_cookie(settings.LANGUAGE_COOKIE_NAME, lang)
return response return response
api_url_pattern = re.compile(r'^/api/(?P<version>\w+)/(?P<app>\w+)/(?P<extra>.*)$')
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)

View File

@ -19,13 +19,13 @@ class TaskViewSet(viewsets.ModelViewSet):
queryset = Task.objects.all() queryset = Task.objects.all()
serializer_class = TaskSerializer serializer_class = TaskSerializer
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
label = None # label = None
help_text = '' # help_text = ''
class TaskRun(generics.RetrieveAPIView): class TaskRun(generics.RetrieveAPIView):
queryset = Task.objects.all() queryset = Task.objects.all()
serializer_class = TaskViewSet # serializer_class = TaskViewSet
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):

View File

@ -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)

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
#

View File

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
#
from .terminal import *
from .session import *
from .task import *

View File

@ -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)

View File

@ -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})

View File

@ -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)

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
#
from .terminal import *

View File

@ -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']

View File

@ -16,7 +16,7 @@ from .backends.command.models import AbstractSessionCommand
class Terminal(models.Model): class Terminal(models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=32, verbose_name=_('Name')) 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) ssh_port = models.IntegerField(verbose_name=_('SSH Port'), default=2222)
http_port = models.IntegerField(verbose_name=_('HTTP Port'), default=5000) http_port = models.IntegerField(verbose_name=_('HTTP Port'), default=5000)
command_storage = models.CharField(max_length=128, verbose_name=_("Command storage"), default='default') command_storage = models.CharField(max_length=128, verbose_name=_("Command storage"), default='default')
@ -68,6 +68,10 @@ class Terminal(models.Model):
}) })
return configs return configs
@property
def service_account(self):
return self.user
def create_app_user(self): def create_app_user(self):
random = uuid.uuid4().hex[:6] random = uuid.uuid4().hex[:6]
user, access_key = User.create_app_user( user, access_key = User.create_app_user(

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
#

View File

@ -1,14 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.core.cache import cache from django.core.cache import cache
from django.utils import timezone
from rest_framework import serializers from rest_framework import serializers
from rest_framework_bulk.serializers import BulkListSerializer from rest_framework_bulk.serializers import BulkListSerializer
from common.mixins import BulkSerializerMixin from common.mixins import BulkSerializerMixin
from .models import Terminal, Status, Session, Task from ..models import Terminal, Status, Session, Task
from .backends import get_multi_command_storage from ..backends import get_multi_command_storage
class TerminalSerializer(serializers.ModelSerializer): class TerminalSerializer(serializers.ModelSerializer):
@ -33,6 +31,8 @@ class TerminalSerializer(serializers.ModelSerializer):
return cache.get(key) return cache.get(key)
class SessionSerializer(BulkSerializerMixin, serializers.ModelSerializer): class SessionSerializer(BulkSerializerMixin, serializers.ModelSerializer):
command_amount = serializers.SerializerMethodField() command_amount = serializers.SerializerMethodField()
command_store = get_multi_command_storage() command_store = get_multi_command_storage()
@ -71,3 +71,4 @@ class TaskSerializer(BulkSerializerMixin, serializers.ModelSerializer):
class ReplaySerializer(serializers.Serializer): class ReplaySerializer(serializers.Serializer):
file = serializers.FileField() file = serializers.FileField()

View File

@ -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

View File

@ -2,10 +2,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.urls import path from django.urls import path, include
from rest_framework_bulk.routes import BulkRouter from rest_framework_bulk.routes import BulkRouter
from .. import api from ..api import v1 as api
app_name = 'terminal' app_name = 'terminal'
@ -20,7 +20,7 @@ router.register(r'status', api.StatusViewSet, 'status')
urlpatterns = [ urlpatterns = [
path('sessions/<uuid:pk>/replay/', path('sessions/<uuid:pk>/replay/',
api.SessionReplayV2ViewSet.as_view({'get': 'retrieve', 'post': 'create'}), api.SessionReplayViewSet.as_view({'get': 'retrieve', 'post': 'create'}),
name='session-replay'), name='session-replay'),
path('tasks/kill-session/', api.KillSessionAPI.as_view(), name='kill-session'), path('tasks/kill-session/', api.KillSessionAPI.as_view(), name='kill-session'),
path('terminal/<uuid:terminal>/access-key/', api.TerminalTokenApi.as_view(), path('terminal/<uuid:terminal>/access-key/', api.TerminalTokenApi.as_view(),
@ -33,3 +33,6 @@ urlpatterns = [
] ]
urlpatterns += router.urls urlpatterns += router.urls

View File

@ -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

View File

@ -17,8 +17,8 @@ from orgs.mixins import RootOrgViewMixin
from ..serializers import UserSerializer from ..serializers import UserSerializer
from ..tasks import write_login_log_async from ..tasks import write_login_log_async
from ..models import User, LoginLog from ..models import User, LoginLog
from ..utils import check_user_valid, generate_token, \ from ..utils import check_user_valid, check_otp_code, \
check_otp_code, increase_login_failed_count, is_block_login, \ increase_login_failed_count, is_block_login, \
clean_failed_count clean_failed_count
from ..hands import Asset, SystemUser from ..hands import Asset, SystemUser
@ -79,7 +79,7 @@ class UserAuthApi(RootOrgViewMixin, APIView):
self.write_login_log(request, data) self.write_login_log(request, data)
# 登陆成功,清除原来的缓存计数 # 登陆成功,清除原来的缓存计数
clean_failed_count(username, ip) clean_failed_count(username, ip)
token = generate_token(request, user) token = user.create_bearer_token(request)
return Response( return Response(
{'token': token, 'user': self.serializer_class(user).data} {'token': token, 'user': self.serializer_class(user).data}
) )
@ -123,7 +123,6 @@ class UserAuthApi(RootOrgViewMixin, APIView):
'user_agent': user_agent, 'user_agent': user_agent,
} }
data.update(tmp_data) data.update(tmp_data)
write_login_log_async.delay(**data) write_login_log_async.delay(**data)
@ -185,7 +184,7 @@ class UserToken(APIView):
user = request.user user = request.user
msg = None msg = None
if user: if user:
token = generate_token(request, user) token = user.create_bearer_token(request)
return Response({'Token': token, 'Keyword': 'Bearer'}, status=200) return Response({'Token': token, 'Keyword': 'Bearer'}, status=200)
else: else:
return Response({'error': msg}, status=406) return Response({'error': msg}, status=406)
@ -223,7 +222,7 @@ class UserOtpAuthApi(RootOrgViewMixin, APIView):
'status': True 'status': True
} }
self.write_login_log(request, data) self.write_login_log(request, data)
token = generate_token(request, user) token = user.create_bearer_token(request)
return Response( return Response(
{ {
'token': token, 'token': token,

View File

@ -11,13 +11,14 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework_bulk import BulkModelViewSet from rest_framework_bulk import BulkModelViewSet
from rest_framework.pagination import LimitOffsetPagination 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, \ from ..serializers import UserSerializer, UserPKUpdateSerializer, \
UserUpdateGroupSerializer, ChangeUserPasswordSerializer UserUpdateGroupSerializer, ChangeUserPasswordSerializer
from ..models import User 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__) logger = get_logger(__name__)
@ -31,13 +32,14 @@ __all__ = [
class UserViewSet(IDInFilterMixin, BulkModelViewSet): class UserViewSet(IDInFilterMixin, BulkModelViewSet):
filter_fields = ('username', 'email', 'name', 'id') filter_fields = ('username', 'email', 'name', 'id')
search_fields = filter_fields search_fields = filter_fields
queryset = User.objects.exclude(role="App") queryset = User.objects.all()
serializer_class = UserSerializer serializer_class = UserSerializer
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
pagination_class = LimitOffsetPagination pagination_class = LimitOffsetPagination
def get_queryset(self): def get_queryset(self):
queryset = super().get_queryset() queryset = super().get_queryset()
if current_org.is_real():
org_users = current_org.get_org_users() org_users = current_org.get_org_users()
queryset = queryset.filter(id__in=org_users) queryset = queryset.filter(id__in=org_users)
return queryset return queryset

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
#
from .user import *

12
apps/users/api/v2/user.py Normal file
View File

@ -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']

View File

@ -1,9 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import base64
import uuid import uuid
import hashlib
import time import time
from django.core.cache import cache 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.six import text_type
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import HTTP_HEADER_ENCODING 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 rest_framework.authentication import CSRFCheck
from common.utils import get_object_or_none, make_signature, http_to_unixtime from common.utils import get_object_or_none, make_signature, http_to_unixtime
from .utils import refresh_token
from .models import User, AccessKey, PrivateToken from .models import User, AccessKey, PrivateToken
@ -144,7 +141,6 @@ class AccessTokenAuthentication(authentication.BaseAuthentication):
if not user: if not user:
msg = _('Invalid token or cache refreshed.') msg = _('Invalid token or cache refreshed.')
raise exceptions.AuthenticationFailed(msg) raise exceptions.AuthenticationFailed(msg)
refresh_token(token, user)
return user, None return user, None

View File

@ -17,7 +17,7 @@ class AccessKey(models.Model):
secret = models.UUIDField(verbose_name='AccessKeySecret', secret = models.UUIDField(verbose_name='AccessKeySecret',
default=uuid.uuid4, editable=False) default=uuid.uuid4, editable=False)
user = models.ForeignKey(User, verbose_name='User', 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): def get_id(self):
return str(self.id) return str(self.id)
@ -25,6 +25,9 @@ class AccessKey(models.Model):
def get_secret(self): def get_secret(self):
return str(self.secret) return str(self.secret)
def get_full_value(self):
return '{}:{}'.format(self.id, self.secret)
def __str__(self): def __str__(self):
return str(self.id) return str(self.id)

View File

@ -2,19 +2,20 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import uuid import uuid
import base64
from collections import OrderedDict from collections import OrderedDict
from django.conf import settings from django.conf import settings
from django.contrib.auth.hashers import make_password 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 import signing
from django.core.cache import cache
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils import timezone from django.utils import timezone
from django.shortcuts import reverse from django.shortcuts import reverse
from common.utils import get_signer, date_expired_default from common.utils import get_signer, date_expired_default
from orgs.mixins import OrgManager
from orgs.utils import current_org from orgs.utils import current_org
@ -274,15 +275,38 @@ class User(AbstractUser):
token = PrivateToken.objects.create(user=self) token = PrivateToken.objects.create(user=self)
return token.key 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): def create_access_key(self):
from . import AccessKey from . import AccessKey
access_key = AccessKey.objects.create(user=self) access_key = AccessKey.objects.create(user=self)
return access_key return access_key
def refresh_private_token(self): @property
from .authentication import PrivateToken def access_key(self):
PrivateToken.objects.filter(user=self).delete() return self.access_keys.first()
return PrivateToken.objects.create(user=self)
def is_member_of(self, user_group): def is_member_of(self, user_group):
if user_group in self.groups.all(): if user_group in self.groups.all():
@ -345,7 +369,8 @@ class User(AbstractUser):
'phone': self.phone, 'phone': self.phone,
'otp_level': self.otp_level, 'otp_level': self.otp_level,
'comment': self.comment, '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 @classmethod

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
#
from .v1 import *

View File

@ -7,14 +7,16 @@ from rest_framework_bulk import BulkListSerializer
from common.utils import get_signer, validate_ssh_public_key from common.utils import get_signer, validate_ssh_public_key
from common.mixins import BulkSerializerMixin from common.mixins import BulkSerializerMixin
from .models import User, UserGroup from ..models import User, UserGroup
signer = get_signer() signer = get_signer()
class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer): class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer):
groups_display = serializers.SerializerMethodField() 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: class Meta:
model = User model = User

View File

@ -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

View File

@ -15,8 +15,7 @@ router.register(r'groups', api.UserGroupViewSet, 'user-group')
urlpatterns = [ 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('connection-token/', api.UserConnectionTokenApi.as_view(), name='connection-token'),
path('profile/', api.UserProfileApi.as_view(), name='user-profile'), path('profile/', api.UserProfileApi.as_view(), name='user-profile'),
path('auth/', api.UserAuthApi.as_view(), name='user-auth'), path('auth/', api.UserAuthApi.as_view(), name='user-auth'),
@ -31,5 +30,6 @@ urlpatterns = [
path('users/<uuid:pk>/groups/', api.UserUpdateGroupApi.as_view(), name='user-update-group'), path('users/<uuid:pk>/groups/', api.UserUpdateGroupApi.as_view(), name='user-update-group'),
path('groups/<uuid:pk>/users/', api.UserGroupUpdateUserApi.as_view(), name='user-group-update-user'), path('groups/<uuid:pk>/users/', api.UserGroupUpdateUserApi.as_view(), name='user-group-update-user'),
] ]
urlpatterns += router.urls urlpatterns += router.urls

View File

@ -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

View File

@ -202,24 +202,6 @@ def check_user_valid(**kwargs):
return None, _('Password or SSH public key invalid') 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): def validate_ip(ip):
try: try:
ipaddress.ip_address(ip) ipaddress.ip_address(ip)

View File

@ -76,3 +76,4 @@ aliyun-python-sdk-core-v3==2.9.1
aliyun-python-sdk-ecs==4.10.1 aliyun-python-sdk-ecs==4.10.1
python-keycloak==0.13.3 python-keycloak==0.13.3
python-keycloak-client==0.1.3 python-keycloak-client==0.1.3
rest_condition==1.0.3

View File

@ -0,0 +1,3 @@
# 名称 用户名 密码
test123 testq12 test123123123

View File

@ -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()

View File

@ -0,0 +1,4 @@
# 名称 用户名 密码 协议[ssh,rdp] 自动推送[0不推送1自动推送]
test123 test123 test123123123 ssh 0
test1323 test123 test123123123 ssh 0

View File

@ -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