diff --git a/apps/assets/models.py b/apps/assets/models.py index b4a40fe64..5ceca9647 100644 --- a/apps/assets/models.py +++ b/apps/assets/models.py @@ -19,7 +19,7 @@ class IDC(models.Model): address = models.CharField(max_length=128, blank=True, verbose_name=_("Address")) intranet = models.TextField(blank=True, verbose_name=_('Intranet')) extranet = models.TextField(blank=True, verbose_name=_('Extranet')) - date_created = models.DateTimeField(auto_now=True, null=True, verbose_name=_('Date added')) + date_created = models.DateTimeField(auto_now_add=True, null=True, verbose_name=_('Date added')) operator = models.CharField(max_length=32, blank=True, verbose_name=_('Operator')) created_by = models.CharField(max_length=32, blank=True, verbose_name=_('Created by')) comment = models.TextField(blank=True, verbose_name=_('Comment')) @@ -62,7 +62,7 @@ class AssetExtend(models.Model): key = models.CharField(max_length=64, verbose_name=_('KEY')) value = models.CharField(max_length=64, verbose_name=_('VALUE')) created_by = models.CharField(max_length=32, blank=True, verbose_name=_("Created by")) - date_created = models.DateTimeField(auto_now=True, null=True) + date_created = models.DateTimeField(auto_now_add=True, null=True) comment = models.TextField(blank=True, verbose_name=_('Comment')) def __unicode__(self): @@ -98,7 +98,7 @@ class AdminUser(models.Model): _public_key = models.CharField(max_length=4096, blank=True, verbose_name=_('SSH public key')) as_default = models.BooleanField(default=False, verbose_name=_('As default')) comment = models.TextField(blank=True, verbose_name=_('Comment')) - date_created = models.DateTimeField(auto_now=True, null=True) + date_created = models.DateTimeField(auto_now_add=True, null=True) created_by = models.CharField(max_length=32, null=True, verbose_name=_('Created by')) def __unicode__(self): @@ -169,7 +169,7 @@ class SystemUser(models.Model): shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell')) home = models.CharField(max_length=64, blank=True, verbose_name=_('Home')) uid = models.IntegerField(null=True, blank=True, verbose_name=_('Uid')) - date_created = models.DateTimeField(auto_now=True) + date_created = models.DateTimeField(auto_now_add=True) created_by = models.CharField(max_length=32, blank=True, verbose_name=_('Created by')) comment = models.TextField(max_length=128, blank=True, verbose_name=_('Comment')) @@ -243,7 +243,7 @@ class AssetGroup(models.Model): name = models.CharField(max_length=64, unique=True, verbose_name=_('Name')) system_users = models.ManyToManyField(SystemUser, related_name='asset_groups', blank=True) created_by = models.CharField(max_length=32, blank=True, verbose_name=_('Created by')) - date_created = models.DateTimeField(auto_now=True, null=True, verbose_name=_('Date added')) + date_created = models.DateTimeField(auto_now_add=True, null=True, verbose_name=_('Date added')) comment = models.TextField(blank=True, verbose_name=_('Comment')) def __unicode__(self): @@ -321,7 +321,7 @@ class Asset(models.Model): sn = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Serial number')) created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by')) is_active = models.BooleanField(default=True, verbose_name=_('Is active')) - date_created = models.DateTimeField(auto_now=True, null=True, blank=True, verbose_name=_('Date added')) + date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date added')) comment = models.TextField(max_length=128, null=True, blank=True, verbose_name=_('Comment')) tags = models.ManyToManyField('Tag', verbose_name='标签集合', blank=True) @@ -365,15 +365,15 @@ class Asset(models.Model): class Tag(models.Model): - name = models.CharField('标签名', max_length=64,unique=True) - created_time = models.DateTimeField('创建时间', auto_now_add=True) + name = models.CharField(max_length=64, unique=True, verbose_name=_('Name')) + created_time = models.DateTimeField(auto_now_add=True, verbose_name=_('Create time')) created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by')) - def __str__(self): - return self.name def __unicode__(self): return self.name + __str__ = __unicode__ + class Meta: db_table = 'tag' diff --git a/apps/audits/api.py b/apps/audits/api.py index 33186e0cc..4801c2e5e 100644 --- a/apps/audits/api.py +++ b/apps/audits/api.py @@ -6,18 +6,46 @@ from rest_framework import generics import serializers -from .models import ProxyLog +from .models import ProxyLog, CommandLog +from .hands import IsSuperUserOrTerminalUser, Terminal class ProxyLogListCreateApi(generics.ListCreateAPIView): + """User proxy to backend server need call this api. + + params: { + "username": "", + "name": "", + "hostname": "", + "ip": "", + "terminal", "", + "login_type": "", + "system_user": "", + "was_failed": "", + "date_start": "" + } + + some params we need generate: { + "log_file", "", # No use now, may be think more about monitor and record + } + """ + queryset = ProxyLog.objects.all() serializer_class = serializers.ProxyLogSerializer + permission_classes = (IsSuperUserOrTerminalUser,) + + def perform_create(self, serializer): + # Todo: May be save log_file + super(ProxyLogListCreateApi, self).perform_create(serializer) class ProxyLogDetailApi(generics.RetrieveUpdateDestroyAPIView): queryset = ProxyLog.objects.all() serializer_class = serializers.ProxyLogSerializer + permission_classes = (IsSuperUserOrTerminalUser,) -class CommandLogCreateApi(generics.CreateAPIView): +class CommandLogCreateApi(generics.ListCreateAPIView): + queryset = CommandLog.objects.all() serializer_class = serializers.CommandLogSerializer + permission_classes = (IsSuperUserOrTerminalUser,) diff --git a/apps/audits/hands.py b/apps/audits/hands.py new file mode 100644 index 000000000..f530b5e78 --- /dev/null +++ b/apps/audits/hands.py @@ -0,0 +1,5 @@ +# ~*~ coding: utf-8 ~*~ +# + +from users.backends import IsSuperUserOrTerminalUser +from terminal.models import Terminal diff --git a/apps/audits/models.py b/apps/audits/models.py index eb6e5baeb..29cd7b981 100644 --- a/apps/audits/models.py +++ b/apps/audits/models.py @@ -9,17 +9,20 @@ from django.utils.translation import ugettext_lazy as _ class LoginLog(models.Model): LOGIN_TYPE_CHOICE = ( - ('S', 'ssh'), - ('W', 'web'), + ('W', 'Web'), + ('S', 'SSH Terminal'), + ('WT', 'Web Terminal') ) username = models.CharField(max_length=20, verbose_name=_('Username')) name = models.CharField(max_length=20, blank=True, verbose_name=_('Name')) - login_type = models.CharField(choices=LOGIN_TYPE_CHOICE, max_length=1, verbose_name=_('Login type')) + login_type = models.CharField(choices=LOGIN_TYPE_CHOICE, max_length=2, verbose_name=_('Login type')) + terminal = models.CharField(max_length=32, verbose_name=_('Terminal')) login_ip = models.GenericIPAddressField(verbose_name=_('Login ip')) login_city = models.CharField(max_length=100, blank=True, null=True, verbose_name=_('Login city')) user_agent = models.CharField(max_length=100, blank=True, null=True, verbose_name=_('User agent')) - date_login = models.DateTimeField(auto_now=True, verbose_name=_('Date login')) + from_terminal = models.ForeignKey + date_login = models.DateTimeField(auto_now_add=True, verbose_name=_('Date login')) date_logout = models.DateTimeField(null=True, verbose_name=_('Date logout')) class Meta: @@ -29,8 +32,8 @@ class LoginLog(models.Model): class ProxyLog(models.Model): LOGIN_TYPE_CHOICE = ( - ('S', 'ssh'), - ('W', 'web'), + ('S', 'SSH Terminal'), + ('WT', 'Web Terminal'), ) username = models.CharField(max_length=20, verbose_name=_('Username')) @@ -38,11 +41,13 @@ class ProxyLog(models.Model): hostname = models.CharField(max_length=128, blank=True, verbose_name=_('Hostname')) ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP')) system_user = models.CharField(max_length=20, verbose_name=_('System user')) - login_type = models.CharField(choices=LOGIN_TYPE_CHOICE, max_length=1, verbose_name=_('Login type')) + login_type = models.CharField(choices=LOGIN_TYPE_CHOICE, max_length=2, blank=True, + null=True, verbose_name=_('Login type')) + terminal = models.CharField(max_length=32, blank=True, null=True, verbose_name=_('Terminal')) log_file = models.CharField(max_length=1000, blank=True, null=True) was_failed = models.BooleanField(default=False, verbose_name=_('Did connect failed')) is_finished = models.BooleanField(default=False, verbose_name=_('Is finished')) - date_start = models.DateTimeField(auto_now=True, verbose_name=_('Date start')) + date_start = models.DateTimeField(verbose_name=_('Date start')) date_finished = models.DateTimeField(null=True, verbose_name=_('Date finished')) def __unicode__(self): @@ -55,14 +60,14 @@ class ProxyLog(models.Model): class CommandLog(models.Model): proxy_log = models.ForeignKey(ProxyLog, on_delete=models.CASCADE, related_name='command_log') + command_no = models.IntegerField() command = models.CharField(max_length=1000, blank=True) output = models.TextField(blank=True) - date_start = models.DateTimeField(null=True) - date_finished = models.DateTimeField(null=True) + datetime = models.DateTimeField(null=True) def __unicode__(self): return '%s: %s' % (self.id, self.command) class Meta: db_table = 'command_log' - ordering = ['-date_start', 'command'] + ordering = ['command_no', 'command'] diff --git a/apps/audits/serializers.py b/apps/audits/serializers.py index 74adc8ee0..97be972ae 100644 --- a/apps/audits/serializers.py +++ b/apps/audits/serializers.py @@ -16,5 +16,4 @@ class CommandLogSerializer(serializers.ModelSerializer): model = models.CommandLog -if __name__ == '__main__': - pass + diff --git a/apps/common/utils.py b/apps/common/utils.py index da83cfca7..352903097 100644 --- a/apps/common/utils.py +++ b/apps/common/utils.py @@ -7,14 +7,18 @@ from itertools import chain import string import logging +from itsdangerous import Signer, TimedJSONWebSignatureSerializer, JSONWebSignatureSerializer, TimestampSigner, \ + BadSignature, SignatureExpired from django.shortcuts import reverse as dj_reverse from django.conf import settings from django.core import signing from django.utils import timezone +SECRET_KEY = settings.SECRET_KEY -def reverse(viewname, urlconf=None, args=None, kwargs=None, current_app=None, external=False): - url = dj_reverse(viewname, urlconf=urlconf, args=args, kwargs=kwargs, current_app=current_app) + +def reverse(view_name, urlconf=None, args=None, kwargs=None, current_app=None, external=False): + url = dj_reverse(view_name, urlconf=urlconf, args=args, kwargs=kwargs, current_app=current_app) if external: url = settings.SITE_URL.strip('/') + url @@ -43,13 +47,29 @@ def decrypt(*args, **kwargs): return '' +def sign(value, secret_key=SECRET_KEY): + signer = TimestampSigner(secret_key) + return signer.sign(value) + + +def unsign(value, max_age=3600, secret_key=SECRET_KEY): + signer = TimestampSigner(secret_key) + try: + return signer.unsign(value, max_age=max_age) + except (BadSignature, SignatureExpired): + return '' + + def date_expired_default(): try: years = int(settings.CONFIG.DEFAULT_EXPIRED_YEARS) except TypeError: years = 70 + return timezone.now() + timezone.timedelta(days=365*years) - return timezone.now() + timezone.timedelta(days=365 * years) + +def sign(value): + return SIGNER.sign(value) def combine_seq(s1, s2, callback=None): diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index b57a1f07a..0b2adfa0c 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -33,7 +33,7 @@ except ImportError: # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = CONFIG.SECRET_KEY or '2vym+ky!997d5kkcc64mnz06y1mmui3lut#(^wd=%s_qj$1%x' +SECRET_KEY = CONFIG.SECRET_KEY # SECURITY WARNING: don't run with debug turned on in production! DEBUG = CONFIG.DEBUG or False @@ -54,10 +54,10 @@ INSTALLED_APPS = [ 'users.apps.UsersConfig', 'assets.apps.AssetsConfig', 'perms.apps.PermsConfig', - # 'terminal.apps.TerminalConfig', 'ops.apps.OpsConfig', 'audits.apps.AuditsConfig', 'common.apps.CommonConfig', + 'terminal.apps.TerminalConfig', 'rest_framework', 'rest_framework.authtoken', 'bootstrapform', @@ -68,7 +68,6 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'ws4redis', ] @@ -264,12 +263,14 @@ REST_FRAMEWORK = { # Use Django's standard `django.contrib.auth` permissions, # or allow read-only access for unauthenticated users. 'DEFAULT_PERMISSION_CLASSES': ( - 'rest_framework.permissions.IsAdminUser', + # 'rest_framework.permissions.IsAuthenticated', + 'users.backends.IsValidUser', ), 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.BasicAuthentication', 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.TokenAuthentication', + 'users.backends.TerminalAuthentication', ), } # This setting is required to override the Django's main loop, when running in diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index f4222aa49..ba7611e2f 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -26,6 +26,7 @@ urlpatterns = [ url(r'^assets/', include('assets.urls')), url(r'^perms/', include('perms.urls')), url(r'^(api/)?audits/', include('audits.urls')), + url(r'^(api/)?terminal/', include('terminal.urls')), ] diff --git a/apps/perms/models.py b/apps/perms/models.py index fd4189d87..b771d086d 100644 --- a/apps/perms/models.py +++ b/apps/perms/models.py @@ -27,7 +27,7 @@ class AssetPermission(models.Model): is_active = models.BooleanField(default=True, verbose_name=_('Active')) date_expired = models.DateTimeField(default=date_expired_default, verbose_name=_('Date expired')) created_by = models.CharField(max_length=128, blank=True, verbose_name=_('Created by')) - date_created = models.DateTimeField(auto_now=True, verbose_name=_('Date created')) + date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created')) comment = models.TextField(verbose_name=_('Comment'), blank=True) def __unicode__(self): diff --git a/apps/static/js/plugins/highcharts/highcharts.src.js b/apps/static/js/plugins/highcharts/highcharts.src.js index 381650b0d..1bb0a4bc7 100644 --- a/apps/static/js/plugins/highcharts/highcharts.src.js +++ b/apps/static/js/plugins/highcharts/highcharts.src.js @@ -4447,7 +4447,7 @@ extend(SVGRenderer.prototype, { * START OF INTERNET EXPLORER <= 8 SPECIFIC CODE * * * * For applications and websites that don't need IE support, like platform * - * targeted mobile apps and web apps, this code can be removed. * + * targeted mobile terminal and web terminal, this code can be removed. * * * *****************************************************************************/ diff --git a/apps/static/js/term.js b/apps/static/js/term.js index c29cad454..beb326f14 100644 --- a/apps/static/js/term.js +++ b/apps/static/js/term.js @@ -4217,19 +4217,19 @@ Terminal.prototype.setMode = function(params) { // focusout: ^[[O this.sendFocus = true; break; - case 1005: // utf8 ext mode mouse + case 1005: // utf8 terminal mode mouse this.utfMouse = true; // for wide terminals // simply encodes large values as utf8 characters break; - case 1006: // sgr ext mode mouse + case 1006: // sgr terminal mode mouse this.sgrMouse = true; // for wide terminals // does not add 32 to fields // press: ^[[ {% trans 'Asset permission' %} -{#
  • #} -{# {% trans 'User group perm' %}#} -{#
  • #} +
  • + + {% trans 'Terminal' %} + +
  • {% trans 'Audits' %} diff --git a/apps/terminal/__init__.py b/apps/terminal/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/terminal/admin.py b/apps/terminal/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/apps/terminal/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/terminal/api.py b/apps/terminal/api.py new file mode 100644 index 000000000..c052eac85 --- /dev/null +++ b/apps/terminal/api.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# + +from rest_framework.generics import ListCreateAPIView, CreateAPIView +from rest_framework.views import APIView, Response +from rest_framework.permissions import AllowAny + +from common.utils import unsign, get_object_or_none +from .models import Terminal, TerminalHeatbeat +from .serializers import TerminalSerializer, TerminalHeatbeatSerializer +from .hands import IsSuperUserOrTerminalUser + + +class TerminalApi(ListCreateAPIView): + queryset = Terminal.objects.all() + serializer_class = TerminalSerializer + permission_classes = (AllowAny,) + + def post(self, request, *args, **kwargs): + name = unsign(request.data.get('name', '')) + if name: + terminal = get_object_or_none(Terminal, name=name) + if terminal: + if terminal.is_accepted and terminal.is_active: + return Response(data={'data': {'name': name, 'id': terminal.id}, + 'msg': 'Success'}, + status=200) + else: + return Response(data={'data': {'name': name, 'ip': terminal.ip}, + 'msg': 'Need admin accept or active it'}, + status=203) + + else: + ip = request.META.get('X-Real-IP') or request.META.get('REMOTE_ADDR') + terminal = Terminal.objects.create(name=name, ip=ip) + return Response(data={'data': {'name': name, 'ip': terminal.ip}, + 'msg': 'Need admin accept or active it'}, + status=204) + else: + return Response(data={'msg': 'Secrete key invalid'}, status=401) + + +class TerminalHeatbeatApi(CreateAPIView): + model = TerminalHeatbeat + serializer_class = TerminalHeatbeatSerializer + permission_classes = (IsSuperUserOrTerminalUser,) diff --git a/apps/terminal/apps.py b/apps/terminal/apps.py new file mode 100644 index 000000000..c81fa232b --- /dev/null +++ b/apps/terminal/apps.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class TerminalConfig(AppConfig): + name = 'terminal' diff --git a/apps/terminal/hands.py b/apps/terminal/hands.py new file mode 100644 index 000000000..baab7f3ad --- /dev/null +++ b/apps/terminal/hands.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# + +from users.backends import IsSuperUserOrTerminalUser + diff --git a/apps/terminal/migrations/__init__.py b/apps/terminal/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/terminal/models.py b/apps/terminal/models.py new file mode 100644 index 000000000..0a8b47819 --- /dev/null +++ b/apps/terminal/models.py @@ -0,0 +1,47 @@ +from __future__ import unicode_literals + +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from users.models import User + + +class Terminal(models.Model): + TYPE_CHOICES = ( + ('S', 'SSH Terminal'), + ('WT', 'Web Terminal') + ) + name = models.CharField(max_length=30, unique=True, verbose_name=_('Name')) + ip = models.GenericIPAddressField(verbose_name=_('From ip')) + is_active = models.BooleanField(default=False, verbose_name=_('Is active')) + is_bound_ip = models.BooleanField(default=False, verbose_name=_('Is bound ip')) + heatbeat_interval = models.IntegerField(default=60, verbose_name=_('Heatbeat interval')) + type = models.CharField(choices=TYPE_CHOICES, max_length=2, verbose_name=_('Terminal type')) + url = models.CharField(max_length=100, verbose_name=_('URL to login')) + mail_to = models.ManyToManyField(User, verbose_name=_('Mail to')) + is_accepted = models.BooleanField(default=False, verbose_name=_('Is accepted')) + date_created = models.DateTimeField(auto_now_add=True) + comment = models.TextField(verbose_name=_('Comment')) + + def is_valid(self): + return self.is_active and self.is_accepted + + @property + def is_superuser(self): + return False + + @property + def is_terminal(self): + return True + + class Meta: + db_table = 'terminal' + ordering = ['name'] + + +class TerminalHeatbeat(models.Model): + terminal = models.ForeignKey(Terminal, on_delete=models.CASCADE) + date_created = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = 'terminal_heatbeat' diff --git a/apps/terminal/serializers.py b/apps/terminal/serializers.py new file mode 100644 index 000000000..cc40938cd --- /dev/null +++ b/apps/terminal/serializers.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# + +from rest_framework import serializers + +from .models import Terminal, TerminalHeatbeat + + +class TerminalSerializer(serializers.ModelSerializer): + class Meta: + model = Terminal + fields = ['name', 'ip', 'type', 'url', 'comment', 'is_active', 'is_accepted', + 'get_type_display'] + + +class TerminalHeatbeatSerializer(serializers.ModelSerializer): + class Meta: + model = TerminalHeatbeat + fields = ['terminal'] + + +if __name__ == '__main__': + pass diff --git a/apps/terminal/templates/terminal/terminal_list.html b/apps/terminal/templates/terminal/terminal_list.html new file mode 100644 index 000000000..dc4b084bf --- /dev/null +++ b/apps/terminal/templates/terminal/terminal_list.html @@ -0,0 +1,79 @@ +{% extends '_base_list.html' %} +{% load i18n static %} +{% block custom_head_css_js %} +{{ block.super }} + +{% endblock %} +{% block table_search %}{% endblock %} +{% block table_container %} +{##} + + + + + + + + + + + + + + +
    +
    + +
    +
    {% trans 'Name' %}{% trans 'IP' %}{% trans 'Type' %}{% trans 'url' %}{% trans 'Active' %}{% trans 'Action' %}
    +{% endblock %} +{% block content_bottom_left %}{% endblock %} +{% block custom_foot_js %} + + +{% endblock %} diff --git a/apps/terminal/tests.py b/apps/terminal/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/apps/terminal/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/terminal/urls.py b/apps/terminal/urls.py new file mode 100644 index 000000000..e85bc82c9 --- /dev/null +++ b/apps/terminal/urls.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# + +from django.conf.urls import url + +import views +import api + +app_name = 'terminal' + +urlpatterns = [ + url(r'^terminal$', views.TerminalListView.as_view(), name='terminal-list'), +] + +urlpatterns += [ + url(r'^v1/terminal/$', api.TerminalApi.as_view(), name='terminal-list-create-api'), + url(r'^v1/terminal-heatbeat/$', api.TerminalHeatbeatApi.as_view(), name='terminal-heatbeat-api'), +] diff --git a/apps/terminal/views.py b/apps/terminal/views.py new file mode 100644 index 000000000..c7a76b2bc --- /dev/null +++ b/apps/terminal/views.py @@ -0,0 +1,17 @@ +# ~*~ coding: utf-8 ~*~ +# + +from django.views.generic import ListView +from django.utils.translation import ugettext as _ + +from .models import Terminal + + +class TerminalListView(ListView): + model = Terminal + template_name = 'terminal/terminal_list.html' + + def get_context_data(self, **kwargs): + context = super(TerminalListView, self).get_context_data(**kwargs) + context.update({'app': _('Terminal'), 'action': _('Terminal list')}) + return context diff --git a/apps/users/api.py b/apps/users/api.py index 4302cec6e..af7b4f0d8 100644 --- a/apps/users/api.py +++ b/apps/users/api.py @@ -7,11 +7,12 @@ from rest_framework import generics, status from rest_framework.response import Response from rest_framework_bulk import ListBulkCreateUpdateDestroyAPIView +from common.mixins import BulkDeleteApiMixin +from common.utils import get_logger from .models import User, UserGroup from .serializers import UserDetailSerializer, UserAndGroupSerializer, \ GroupDetailSerializer, UserPKUpdateSerializer, UserBulkUpdateSerializer, GroupBulkUpdateSerializer -from common.mixins import BulkDeleteApiMixin -from common.utils import get_logger +from .backends import IsSuperUser, IsTerminalUser, IsValidUser, IsSuperUserOrTerminalUser logger = get_logger(__name__) @@ -20,11 +21,13 @@ logger = get_logger(__name__) class UserDetailApi(generics.RetrieveUpdateDestroyAPIView): queryset = User.objects.all() serializer_class = UserDetailSerializer + permission_classes = (IsSuperUser,) class UserAndGroupEditApi(generics.RetrieveUpdateAPIView): queryset = User.objects.all() serializer_class = UserAndGroupSerializer + permission_classes = (IsSuperUser,) class UserResetPasswordApi(generics.UpdateAPIView): @@ -84,6 +87,10 @@ class GroupDetailApi(generics.RetrieveUpdateDestroyAPIView): class UserListUpdateApi(BulkDeleteApiMixin, ListBulkCreateUpdateDestroyAPIView): queryset = User.objects.all() serializer_class = UserBulkUpdateSerializer + permission_classes = (IsSuperUserOrTerminalUser,) + + # def get(self, request, *args, **kwargs): + # return super(UserListUpdateApi, self).get(request, *args, **kwargs) class GroupListUpdateApi(BulkDeleteApiMixin, ListBulkCreateUpdateDestroyAPIView): @@ -104,3 +111,23 @@ class DeleteUserFromGroupApi(generics.DestroyAPIView): user_id = kwargs.get('uid') user = get_object_or_404(User, id=user_id) instance.users.remove(user) + + +class AppUserRegisterApi(generics.CreateAPIView): + """App send a post request to register a app user + + request params contains `username_signed`, You can unsign it, + username = unsign(username_signed), if you get the username, + It's present it's a valid request, or return (401, Invalid request), + then your should check if the user exist or not. If exist, + return (200, register success), If not, you should be save it, and + notice admin user, The user default is not active before admin user + unblock it. + + Save fields: + username: + name: name + request.ip + email: username + '@app.org' + role: App + """ + pass diff --git a/apps/users/backends.py b/apps/users/backends.py new file mode 100644 index 000000000..07c5c2f70 --- /dev/null +++ b/apps/users/backends.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +# + +from rest_framework import authentication, exceptions, permissions +from rest_framework.compat import is_authenticated +from django.utils.translation import ugettext as _ + +from common.utils import unsign, get_object_or_none +from .hands import Terminal + + +class TerminalAuthentication(authentication.BaseAuthentication): + keyword = 'Sign' + model = Terminal + + def authenticate(self, request): + auth = authentication.get_authorization_header(request).split() + + if not auth or auth[0].lower() != self.keyword.lower().encode(): + return None + + if len(auth) == 1: + msg = _('Invalid sign header. No credentials provided.') + raise exceptions.AuthenticationFailed(msg) + elif len(auth) > 2: + msg = _('Invalid sign header. Sign string should not contain spaces.') + raise exceptions.AuthenticationFailed(msg) + + try: + sign = auth[1].decode() + except UnicodeError: + msg = _('Invalid token header. Sign string should not contain invalid characters.') + raise exceptions.AuthenticationFailed(msg) + return self.authenticate_credentials(sign) + + def authenticate_credentials(self, sign): + name = unsign(sign, max_age=300) + if name: + terminal = get_object_or_none(self.model, name=name) + else: + raise exceptions.AuthenticationFailed(_('Invalid sign.')) + + if not terminal.is_active: + raise exceptions.AuthenticationFailed(_('Terminal inactive or deleted.')) + terminal.is_authenticated = True + return terminal, None + + +class IsValidUser(permissions.IsAuthenticated, permissions.BasePermission): + """Allows access to valid user, is active and not expired""" + + def has_permission(self, request, view): + return super(IsValidUser, self).has_permission(request, view) \ + and request.user.is_valid + + +class IsTerminalUser(IsValidUser, permissions.BasePermission): + """Allows access only to app user """ + + def has_permission(self, request, view): + return super(IsTerminalUser, self).has_permission(request, view) \ + and isinstance(request.user, Terminal) + + +class IsSuperUser(IsValidUser, permissions.BasePermission): + """Allows access only to superuser""" + + def has_permission(self, request, view): + return super(IsSuperUser, self).has_permission(request, view) \ + and request.user.is_superuser + + +class IsSuperUserOrTerminalUser(IsValidUser, permissions.BasePermission): + """Allows access between superuser and app user""" + + def has_permission(self, request, view): + return super(IsSuperUserOrTerminalUser, self).has_permission(request, view) \ + and (request.user.is_superuser or request.user.is_terminal) + + +if __name__ == '__main__': + pass diff --git a/apps/users/hands.py b/apps/users/hands.py index 5f0eb93be..f7159bf26 100644 --- a/apps/users/hands.py +++ b/apps/users/hands.py @@ -12,3 +12,4 @@ from perms.models import AssetPermission from perms.utils import get_user_granted_assets, get_user_granted_asset_groups +from terminal.models import Terminal \ No newline at end of file diff --git a/apps/users/models.py b/apps/users/models.py index ebe45afaa..98bcd3454 100644 --- a/apps/users/models.py +++ b/apps/users/models.py @@ -141,6 +141,10 @@ class User(AbstractUser): else: return False + @property + def is_terminal(self): + return False + @is_superuser.setter def is_superuser(self, value): if value is True: @@ -150,7 +154,7 @@ class User(AbstractUser): @property def is_staff(self): - if self.is_authenticated and self.is_active and not self.is_expired and self.is_superuser: + if self.is_authenticated and self.is_valid: return True else: return False @@ -178,21 +182,20 @@ class User(AbstractUser): token = Token.objects.get(user=self) except Token.DoesNotExist: token = Token.objects.create(user=self) - return token.key def refresh_private_token(self): Token.objects.filter(user=self).delete() return Token.objects.create(user=self) - def generate_reset_token(self): - return signing.dumps({'reset': self.id, 'email': self.email}) - def is_member_of(self, user_group): if user_group in self.groups.all(): return True return False + def generate_reset_token(self): + return signing.dumps({'reset': self.id, 'email': self.email}) + @classmethod def validate_reset_token(cls, token, max_age=3600): try: diff --git a/apps/users/serializers.py b/apps/users/serializers.py index cfce66ab7..fa1e57b79 100644 --- a/apps/users/serializers.py +++ b/apps/users/serializers.py @@ -5,23 +5,23 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from rest_framework_bulk import BulkListSerializer, BulkSerializerMixin +from common.utils import unsign from .models import User, UserGroup class UserDetailSerializer(serializers.ModelSerializer): - class Meta: model = User fields = ['avatar', 'wechat', 'phone', 'enable_otp', 'comment', 'is_active', 'name'] class UserPKUpdateSerializer(serializers.ModelSerializer): - class Meta: model = User fields = ['id', '_public_key'] - def validate__public_key(self, value): + @staticmethod + def validate__public_key(value): from sshpubkeys import SSHKey from sshpubkeys.exceptions import InvalidKeyException ssh = SSHKey(value) @@ -45,7 +45,6 @@ class UserAndGroupSerializer(serializers.ModelSerializer): class GroupDetailSerializer(serializers.ModelSerializer): - class Meta: model = UserGroup fields = ['id', 'name', 'comment', 'date_created', 'created_by', 'users'] @@ -63,16 +62,17 @@ class UserBulkUpdateSerializer(BulkSerializerMixin, serializers.ModelSerializer) 'enable_otp', 'comment', 'groups', 'get_role_display', 'group_display', 'active_display'] - def get_group_display(self, obj): + @staticmethod + def get_group_display(obj): return " ".join([group.name for group in obj.groups.all()]) - def get_active_display(self, obj): - # TODO: user ative state + @staticmethod + def get_active_display(obj): + # TODO: user active state return not (obj.is_expired and obj.is_active) class GroupBulkUpdateSerializer(BulkSerializerMixin, serializers.ModelSerializer): - user_amount = serializers.SerializerMethodField() class Meta: @@ -80,5 +80,18 @@ class GroupBulkUpdateSerializer(BulkSerializerMixin, serializers.ModelSerializer list_serializer_class = BulkListSerializer fields = ['id', 'name', 'comment', 'user_amount'] - def get_user_amount(self, obj): + @staticmethod + def get_user_amount(obj): return obj.users.count() + + +class AppUserRegisterSerializer(serializers.Serializer): + username = serializers.CharField(max_length=20) + + def create(self, validated_data): + sign = validated_data('username', '') + username = unsign(sign) + pass + + def update(self, instance, validated_data): + pass diff --git a/apps/users/templates/users/user_list.html b/apps/users/templates/users/user_list.html index b08a44b53..af2247855 100644 --- a/apps/users/templates/users/user_list.html +++ b/apps/users/templates/users/user_list.html @@ -23,12 +23,12 @@ div.dataTables_wrapper div.dataTables_filter {
    - {% trans 'Name' %} - {% trans 'Username' %} + {% trans 'Name' %} + {% trans 'Username' %} {% trans 'Role' %} {% trans 'User group' %} {% trans 'Asset num' %} - {% trans 'Active' %} + {% trans 'Active' %} {% trans 'Action' %} @@ -165,7 +165,7 @@ $(document).ready(function(){ var fail = function() { var msg = "{% trans 'User Deleting failed.' %}"; swal("{% trans 'User Delete' %}", msg, "error"); - } + }; APIUpdateAttr({ url: the_url, body: JSON.stringify(body), @@ -208,15 +208,15 @@ $(document).ready(function(){ post_list.push(content); }); if (post_list === []) { - return false; - }; + return false + } var the_url = "{% url 'users:user-bulk-update-api' %}"; var success = function() { var msg = "{% trans 'The selected users has been updated successfully.' %}"; swal("{% trans 'User Updated' %}", msg, "success"); $('#user_list_table').DataTable().ajax.reload(); jumpserver.checked = false; - } + }; APIUpdateAttr({url: the_url, method: 'PATCH', body: JSON.stringify(post_list), success: success}); $('#user_bulk_update_modal').modal('hide'); }).on('click', '#btn_user_import', function() {