mirror of https://github.com/jumpserver/jumpserver
Merge branch 'audits'
commit
45dcb26123
|
@ -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'
|
||||
|
||||
|
|
|
@ -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,)
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
#
|
||||
|
||||
from users.backends import IsSuperUserOrTerminalUser
|
||||
from terminal.models import Terminal
|
|
@ -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']
|
||||
|
|
|
@ -16,5 +16,4 @@ class CommandLogSerializer(serializers.ModelSerializer):
|
|||
model = models.CommandLog
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pass
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')),
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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. *
|
||||
* *
|
||||
*****************************************************************************/
|
||||
|
||||
|
|
|
@ -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: ^[[<b;x;yM
|
||||
// release: ^[[<b;x;ym
|
||||
break;
|
||||
case 1015: // urxvt ext mode mouse
|
||||
case 1015: // urxvt terminal mode mouse
|
||||
this.urxvtMouse = true;
|
||||
// for wide terminals
|
||||
// numbers for fields
|
||||
|
@ -4406,13 +4406,13 @@ Terminal.prototype.resetMode = function(params) {
|
|||
case 1004: // send focusin/focusout events
|
||||
this.sendFocus = false;
|
||||
break;
|
||||
case 1005: // utf8 ext mode mouse
|
||||
case 1005: // utf8 terminal mode mouse
|
||||
this.utfMouse = false;
|
||||
break;
|
||||
case 1006: // sgr ext mode mouse
|
||||
case 1006: // sgr terminal mode mouse
|
||||
this.sgrMouse = false;
|
||||
break;
|
||||
case 1015: // urxvt ext mode mouse
|
||||
case 1015: // urxvt terminal mode mouse
|
||||
this.urxvtMouse = false;
|
||||
break;
|
||||
case 25: // hide cursor
|
||||
|
@ -6030,7 +6030,7 @@ Terminal.charsets = {};
|
|||
|
||||
// DEC Special Character and Line Drawing Set.
|
||||
// http://vt100.net/docs/vt102-ug/table5-13.html
|
||||
// A lot of curses apps use this if they see TERM=xterm.
|
||||
// A lot of curses terminal use this if they see TERM=xterm.
|
||||
// testing: echo -e '\e(0a\e(B'
|
||||
// The xterm output sometimes seems to conflict with the
|
||||
// reference above. xterm seems in line with the reference
|
||||
|
|
|
@ -32,11 +32,13 @@
|
|||
<li id="asset-permission">
|
||||
<a href="{% url 'perms:asset-permission-list' %}">{% trans 'Asset permission' %}</a>
|
||||
</li>
|
||||
{# <li id="user-group">#}
|
||||
{# <a href="">{% trans 'User group perm' %}</a>#}
|
||||
{# </li>#}
|
||||
</ul>
|
||||
</li>
|
||||
<li id="">
|
||||
<a href="{% url 'terminal:terminal-list' %}">
|
||||
<i class="fa fa-desktop"></i><span class="nav-label">{% trans 'Terminal' %}</span><span class="label label-info pull-right"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li id="">
|
||||
<a href="">
|
||||
<i class="fa fa-files-o"></i><span class="nav-label">{% trans 'Audits' %}</span><span class="label label-info pull-right"></span>
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
|
@ -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,)
|
|
@ -0,0 +1,7 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class TerminalConfig(AppConfig):
|
||||
name = 'terminal'
|
|
@ -0,0 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from users.backends import IsSuperUserOrTerminalUser
|
||||
|
|
@ -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'
|
|
@ -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
|
|
@ -0,0 +1,79 @@
|
|||
{% extends '_base_list.html' %}
|
||||
{% load i18n static %}
|
||||
{% block custom_head_css_js %}
|
||||
{{ block.super }}
|
||||
<style>
|
||||
div.dataTables_wrapper div.dataTables_filter,
|
||||
.dataTables_length {
|
||||
float: right !important;
|
||||
}
|
||||
|
||||
div.dataTables_wrapper div.dataTables_filter {
|
||||
margin-left: 15px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block table_search %}{% endblock %}
|
||||
{% block table_container %}
|
||||
{#<div class="uc pull-left m-l-5 m-r-5"><a href="{% url "users:user-create" %}" class="btn btn-sm btn-primary"> {% trans "Create user" %} </a></div>#}
|
||||
<table class="table table-striped table-bordered table-hover " id="terminal_list_table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
<div class="checkbox checkbox-default">
|
||||
<input type="checkbox" class="ipt_check_all">
|
||||
</div>
|
||||
</th>
|
||||
<th class="text-center">{% trans 'Name' %}</th>
|
||||
<th class="text-center">{% trans 'IP' %}</th>
|
||||
<th class="text-center">{% trans 'Type' %}</th>
|
||||
<th class="text-center">{% trans 'url' %}</th>
|
||||
<th class="text-center">{% trans 'Active' %}</th>
|
||||
<th class="text-center">{% trans 'Action' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% block content_bottom_left %}{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script src="{% static 'js/jquery.form.min.js' %}"></script>
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
var options = {
|
||||
ele: $('#terminal_list_table'),
|
||||
{# columnDefs: [#}
|
||||
{# {targets: 1, createdCell: function (td, cellData, rowData) {#}
|
||||
{# var detail_btn = '<a href="{% url "users:user-detail" pk=99991937 %}">' + cellData + '</a>';#}
|
||||
{# $(td).html(detail_btn.replace('99991937', rowData.id));#}
|
||||
{# }}#}
|
||||
{# {targets: 4, createdCell: function (td, cellData) {#}
|
||||
{# var innerHtml = cellData.length > 8 ? cellData.substring(0, 8) + '...': cellData;#}
|
||||
{# $(td).html('<a href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</a>');#}
|
||||
{# }},#}
|
||||
{# {targets: 6, createdCell: function (td, cellData) {#}
|
||||
{# if (!cellData) {#}
|
||||
{# $(td).html('<i class="fa fa-times text-danger"></i>')#}
|
||||
{# } else {#}
|
||||
{# $(td).html('<i class="fa fa-check text-navy"></i>')#}
|
||||
{# }#}
|
||||
{# }},#}
|
||||
{# {targets: 7, createdCell: function (td, cellData, rowData) {#}
|
||||
{# var update_btn = '<a href="{% url "users:user-update" pk=99991937 %}" class="btn btn-xs btn-info">{% trans "Update" %}</a>'.replace('99991937', cellData);#}
|
||||
{# var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn_user_delete" data-uid="99991937">{% trans "Delete" %}</a>'.replace('99991937', cellData);#}
|
||||
{# if (rowData.id === 1) {#}
|
||||
{# $(td).html(update_btn)#}
|
||||
{# } else {#}
|
||||
{# $(td).html(update_btn + del_btn)#}
|
||||
{# }}],#}
|
||||
{# ],#}
|
||||
ajax_url: '{% url "terminal:terminal-list-create-api" %}',
|
||||
columns: [{data: function(){return ""}}, {data: "name" }, {data: "ip" }, {data: "get_type_display" }, {data: "url" },
|
||||
{data: "is_active" }, {data: "ip"}],
|
||||
op_html: $('#actions').html()
|
||||
};
|
||||
jumpserver.initDataTable(options);
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -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'),
|
||||
]
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -23,12 +23,12 @@ div.dataTables_wrapper div.dataTables_filter {
|
|||
<th class="text-center">
|
||||
<div class="checkbox checkbox-default"><input id="" type="checkbox" class="ipt_check_all"><label></label></div>
|
||||
</th>
|
||||
<th class="text-center">{% trans 'Name' %}</a></th>
|
||||
<th class="text-center">{% trans 'Username' %}</a></th>
|
||||
<th class="text-center">{% trans 'Name' %}</th>
|
||||
<th class="text-center">{% trans 'Username' %}</th>
|
||||
<th class="text-center">{% trans 'Role' %}</th>
|
||||
<th class="text-center">{% trans 'User group' %}</th>
|
||||
<th class="text-center">{% trans 'Asset num' %}</th>
|
||||
<th class="text-center">{% trans 'Active' %}</a></th>
|
||||
<th class="text-center">{% trans 'Active' %}</th>
|
||||
<th class="text-center">{% trans 'Action' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -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() {
|
||||
|
|
Loading…
Reference in New Issue