Merge branch 'lina' of github.com:jumpserver/jumpserver into lina

pull/3986/head
ibuler 2020-05-09 14:51:47 +08:00
commit 0bdc425c55
16 changed files with 853 additions and 1104 deletions

View File

@ -2,27 +2,34 @@
# #
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import ListModelMixin from rest_framework.mixins import ListModelMixin
from django.db.models import Q
from common.mixins.api import CommonApiMixin from common.mixins.api import CommonApiMixin
from common.permissions import IsOrgAdminOrAppUser, IsOrgAuditor, IsOrgAdmin from common.permissions import IsOrgAdminOrAppUser, IsOrgAuditor, IsOrgAdmin
from common.drf.filters import DatetimeRangeFilter from common.drf.filters import DatetimeRangeFilter, current_user_filter
from orgs.mixins.api import OrgModelViewSet from common.api import CommonGenericViewSet
from orgs.mixins.api import OrgGenericViewSet
from orgs.utils import current_org from orgs.utils import current_org
from .models import FTPLog, UserLoginLog from ops.models import CommandExecution
from .serializers import FTPLogSerializer, UserLoginLogSerializer from .models import FTPLog, UserLoginLog, OperateLog, PasswordChangeLog
from .serializers import FTPLogSerializer, UserLoginLogSerializer, CommandExecutionSerializer
from .serializers import OperateLogSerializer, PasswordChangeLogSerializer
from .filters import CurrentOrgMembersFilter
class FTPLogViewSet(OrgModelViewSet): class FTPLogViewSet(ListModelMixin, OrgGenericViewSet):
model = FTPLog model = FTPLog
serializer_class = FTPLogSerializer serializer_class = FTPLogSerializer
permission_classes = (IsOrgAdminOrAppUser | IsOrgAuditor,) permission_classes = (IsOrgAdminOrAppUser | IsOrgAuditor,)
http_method_names = ['get', 'post', 'head', 'options'] extra_filter_backends = [DatetimeRangeFilter]
date_range_filter_fields = [
('date_start', ('date_from', 'date_to'))
]
filterset_fields = ['user', 'asset', 'system_user']
search_fields = ['filename']
class UserLoginLogViewSet(CommonApiMixin, class UserLoginLogViewSet(ListModelMixin,
ListModelMixin, CommonGenericViewSet):
GenericViewSet):
queryset = UserLoginLog.objects.all() queryset = UserLoginLog.objects.all()
permission_classes = [IsOrgAdmin | IsOrgAuditor] permission_classes = [IsOrgAdmin | IsOrgAuditor]
serializer_class = UserLoginLogSerializer serializer_class = UserLoginLogSerializer
@ -44,3 +51,47 @@ class UserLoginLogViewSet(CommonApiMixin,
users = self.get_org_members() users = self.get_org_members()
queryset = queryset.filter(username__in=users) queryset = queryset.filter(username__in=users)
return queryset return queryset
class OperateLogViewSet(ListModelMixin, OrgGenericViewSet):
model = OperateLog
serializer_class = OperateLogSerializer
permission_classes = [IsOrgAdmin | IsOrgAuditor]
extra_filter_backends = [DatetimeRangeFilter]
date_range_filter_fields = [
('datetime', ('date_from', 'date_to'))
]
filterset_fields = ['user', 'action', 'resource_type']
search_fields = ['filename']
ordering_fields = ['-datetime']
class PasswordChangeLogViewSet(ListModelMixin, CommonGenericViewSet):
queryset = PasswordChangeLog.objects.all()
permission_classes = [IsOrgAdmin | IsOrgAuditor]
serializer_class = PasswordChangeLogSerializer
extra_filter_backends = [DatetimeRangeFilter]
date_range_filter_fields = [
('datetime', ('date_from', 'date_to'))
]
filterset_fields = ['user']
ordering_fields = ['-datetime']
def get_queryset(self):
users = current_org.get_org_members()
queryset = super().get_queryset().filter(
user__in=[user.__str__() for user in users]
)
return queryset
class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet):
model = CommandExecution
serializer_class = CommandExecutionSerializer
permission_classes = [IsOrgAdmin | IsOrgAuditor]
extra_filter_backends = [DatetimeRangeFilter, current_user_filter(), CurrentOrgMembersFilter]
date_range_filter_fields = [
('date_start', ('date_from', 'date_to'))
]
search_fields = ['command']
ordering_fields = ['-date_created']

32
apps/audits/filters.py Normal file
View File

@ -0,0 +1,32 @@
from rest_framework import filters
from rest_framework.compat import coreapi, coreschema
from orgs.utils import current_org
__all__ = ['CurrentOrgMembersFilter']
class CurrentOrgMembersFilter(filters.BaseFilterBackend):
def get_schema_fields(self, view):
return [
coreapi.Field(
name='user', location='query', required=False, type='string',
schema=coreschema.String(
title='user',
description='user'
)
)
]
def _get_user_list(self):
users = current_org.get_org_members(exclude=('Auditor',))
return users
def filter_queryset(self, request, queryset, view):
user_id = request.GET.get('user')
if user_id:
queryset = queryset.filter(user=user_id)
else:
queryset = queryset.filter(user__in=self._get_user_list())
return queryset

View File

@ -0,0 +1,28 @@
# Generated by Django 2.2.10 on 2020-05-08 13:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('audits', '0007_auto_20191202_1010'),
]
operations = [
migrations.AlterField(
model_name='ftplog',
name='date_start',
field=models.DateTimeField(auto_now_add=True, verbose_name='Date start'),
),
migrations.AlterField(
model_name='operatelog',
name='datetime',
field=models.DateTimeField(auto_now=True, verbose_name='Datetime'),
),
migrations.AlterField(
model_name='passwordchangelog',
name='datetime',
field=models.DateTimeField(auto_now=True, verbose_name='Datetime'),
),
]

View File

@ -22,7 +22,7 @@ class FTPLog(OrgModelMixin):
operate = models.CharField(max_length=16, verbose_name=_("Operate")) operate = models.CharField(max_length=16, verbose_name=_("Operate"))
filename = models.CharField(max_length=1024, verbose_name=_("Filename")) filename = models.CharField(max_length=1024, verbose_name=_("Filename"))
is_success = models.BooleanField(default=True, verbose_name=_("Success")) is_success = models.BooleanField(default=True, verbose_name=_("Success"))
date_start = models.DateTimeField(auto_now_add=True) date_start = models.DateTimeField(auto_now_add=True, verbose_name=_('Date start'))
class OperateLog(OrgModelMixin): class OperateLog(OrgModelMixin):
@ -40,7 +40,7 @@ class OperateLog(OrgModelMixin):
resource_type = models.CharField(max_length=64, verbose_name=_("Resource Type")) resource_type = models.CharField(max_length=64, verbose_name=_("Resource Type"))
resource = models.CharField(max_length=128, verbose_name=_("Resource")) resource = models.CharField(max_length=128, verbose_name=_("Resource"))
remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True)
datetime = models.DateTimeField(auto_now=True) datetime = models.DateTimeField(auto_now=True, verbose_name=_('Datetime'))
def __str__(self): def __str__(self):
return "<{}> {} <{}>".format(self.user, self.action, self.resource) return "<{}> {} <{}>".format(self.user, self.action, self.resource)
@ -51,7 +51,7 @@ class PasswordChangeLog(models.Model):
user = models.CharField(max_length=128, verbose_name=_('User')) user = models.CharField(max_length=128, verbose_name=_('User'))
change_by = models.CharField(max_length=128, verbose_name=_("Change by")) change_by = models.CharField(max_length=128, verbose_name=_("Change by"))
remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True)
datetime = models.DateTimeField(auto_now=True) datetime = models.DateTimeField(auto_now=True, verbose_name=_('Datetime'))
def __str__(self): def __str__(self):
return "{} change {}'s password".format(self.change_by, self.user) return "{} change {}'s password".format(self.change_by, self.user)

View File

@ -1,9 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from terminal.models import Session from terminal.models import Session
from ops.models import CommandExecution
from . import models from . import models
@ -11,31 +12,40 @@ class FTPLogSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.FTPLog model = models.FTPLog
fields = '__all__' fields = (
'user', 'remote_addr', 'asset', 'system_user',
'operate', 'filename', 'is_success', 'date_start'
)
class UserLoginLogSerializer(serializers.ModelSerializer): class UserLoginLogSerializer(serializers.ModelSerializer):
type_display = serializers.ReadOnlyField(source='get_type_display') type_display = serializers.ReadOnlyField(source='get_type_display')
status_display = serializers.ReadOnlyField(source='get_status_display') status_display = serializers.ReadOnlyField(source='get_status_display')
mfa_display = serializers.ReadOnlyField(source='get_mfa_display')
class Meta: class Meta:
model = models.UserLoginLog model = models.UserLoginLog
fields = ( fields = (
'username', 'type', 'type_display', 'ip', 'city', 'user_agent', 'username', 'type', 'type_display', 'ip', 'city', 'user_agent',
'mfa', 'reason', 'status', 'status_display', 'datetime' 'mfa', 'reason', 'status', 'status_display', 'datetime', 'mfa_display'
) )
class OperateLogSerializer(serializers.ModelSerializer): class OperateLogSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.OperateLog model = models.OperateLog
fields = '__all__' fields = (
'user', 'action', 'resource_type', 'resource',
'remote_addr', 'datetime'
)
class PasswordChangeLogSerializer(serializers.ModelSerializer): class PasswordChangeLogSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.PasswordChangeLog model = models.PasswordChangeLog
fields = '__all__' fields = (
'user', 'change_by', 'remote_addr', 'datetime'
)
class SessionAuditSerializer(serializers.ModelSerializer): class SessionAuditSerializer(serializers.ModelSerializer):
@ -43,3 +53,15 @@ class SessionAuditSerializer(serializers.ModelSerializer):
model = Session model = Session
fields = '__all__' fields = '__all__'
class CommandExecutionSerializer(serializers.ModelSerializer):
class Meta:
model = CommandExecution
fields = (
'hosts', 'run_as', 'command', 'user', 'is_finished',
'date_start', 'result', 'is_success'
)
extra_kwargs = {
'result': {'label': _('Result')},
'is_success': {'label': _('Is success')},
}

View File

@ -13,6 +13,9 @@ app_name = "audits"
router = DefaultRouter() router = DefaultRouter()
router.register(r'ftp-logs', api.FTPLogViewSet, 'ftp-log') router.register(r'ftp-logs', api.FTPLogViewSet, 'ftp-log')
router.register(r'login-logs', api.UserLoginLogViewSet, 'login-log') router.register(r'login-logs', api.UserLoginLogViewSet, 'login-log')
router.register(r'operate-logs', api.OperateLogViewSet, 'operate-log')
router.register(r'password-change-logs', api.PasswordChangeLogViewSet, 'password-change-log')
router.register(r'command-execution-logs', api.CommandExecutionViewSet, 'command-execution-log')
urlpatterns = [ urlpatterns = [
] ]

View File

@ -9,10 +9,12 @@ from django.views.decorators.csrf import csrf_exempt
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import generics, serializers from rest_framework import generics, serializers
from rest_framework.viewsets import GenericViewSet
from .http import HttpResponseTemporaryRedirect from .http import HttpResponseTemporaryRedirect
from .const import KEY_CACHE_RESOURCES_ID from .const import KEY_CACHE_RESOURCES_ID
from .utils import get_logger from .utils import get_logger
from .mixins import CommonApiMixin
__all__ = [ __all__ = [
'LogTailApi', 'ResourcesIDCacheApi', 'LogTailApi', 'ResourcesIDCacheApi',
@ -100,3 +102,7 @@ def redirect_plural_name_api(request, *args, **kwargs):
full_path = org_full_path.replace(resource, resource+"s", 1) full_path = org_full_path.replace(resource, resource+"s", 1)
logger.debug("Redirect {} => {}".format(org_full_path, full_path)) logger.debug("Redirect {} => {}".format(org_full_path, full_path))
return HttpResponseTemporaryRedirect(full_path) return HttpResponseTemporaryRedirect(full_path)
class CommonGenericViewSet(CommonApiMixin, GenericViewSet):
pass

View File

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import coreapi
from rest_framework import filters from rest_framework import filters
from rest_framework.fields import DateTimeField from rest_framework.fields import DateTimeField
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
@ -146,3 +145,10 @@ class CustomFilter(filters.BaseFilterBackend):
def filter_queryset(self, request, queryset, view): def filter_queryset(self, request, queryset, view):
return queryset return queryset
def current_user_filter(user_field='user'):
class CurrentUserFilter(filters.BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
return queryset.filter(**{user_field: request.user})
return CurrentUserFilter

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,50 @@
# Generated by Django 2.2.10 on 2020-05-08 13:05
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('ops', '0017_auto_20200306_1747'),
]
operations = [
migrations.AlterField(
model_name='commandexecution',
name='date_created',
field=models.DateTimeField(auto_now_add=True, verbose_name='Date created'),
),
migrations.AlterField(
model_name='commandexecution',
name='date_finished',
field=models.DateTimeField(null=True, verbose_name='Date finished'),
),
migrations.AlterField(
model_name='commandexecution',
name='date_start',
field=models.DateTimeField(null=True, verbose_name='Date start'),
),
migrations.AlterField(
model_name='commandexecution',
name='hosts',
field=models.ManyToManyField(to='assets.Asset', verbose_name='Hosts'),
),
migrations.AlterField(
model_name='commandexecution',
name='is_finished',
field=models.BooleanField(default=False, verbose_name='Is finished'),
),
migrations.AlterField(
model_name='commandexecution',
name='run_as',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.SystemUser', verbose_name='Run as'),
),
migrations.AlterField(
model_name='commandexecution',
name='user',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User'),
),
]

View File

@ -18,15 +18,15 @@ from ..inventory import JMSInventory
class CommandExecution(OrgModelMixin): class CommandExecution(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
hosts = models.ManyToManyField('assets.Asset') hosts = models.ManyToManyField('assets.Asset', verbose_name=_('Hosts'))
run_as = models.ForeignKey('assets.SystemUser', on_delete=models.CASCADE) run_as = models.ForeignKey('assets.SystemUser', on_delete=models.CASCADE, verbose_name=_('Run as'))
command = models.TextField(verbose_name=_("Command")) command = models.TextField(verbose_name=_("Command"))
_result = models.TextField(blank=True, null=True, verbose_name=_('Result')) _result = models.TextField(blank=True, null=True, verbose_name=_('Result'))
user = models.ForeignKey('users.User', on_delete=models.CASCADE, null=True) user = models.ForeignKey('users.User', on_delete=models.CASCADE, null=True, verbose_name=_('User'))
is_finished = models.BooleanField(default=False) is_finished = models.BooleanField(default=False, verbose_name=_('Is finished'))
date_created = models.DateTimeField(auto_now_add=True) date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created'))
date_start = models.DateTimeField(null=True) date_start = models.DateTimeField(null=True, verbose_name=_('Date start'))
date_finished = models.DateTimeField(null=True) date_finished = models.DateTimeField(null=True, verbose_name=_('Date finished'))
def __str__(self): def __str__(self):
return self.command[:10] return self.command[:10]

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet, GenericViewSet
from rest_framework_bulk import BulkModelViewSet from rest_framework_bulk import BulkModelViewSet
from common.mixins import CommonApiMixin from common.mixins import CommonApiMixin
@ -44,6 +44,10 @@ class OrgModelViewSet(CommonApiMixin, OrgQuerySetMixin, ModelViewSet):
pass pass
class OrgGenericViewSet(CommonApiMixin, OrgQuerySetMixin, GenericViewSet):
pass
class OrgBulkModelViewSet(CommonApiMixin, OrgQuerySetMixin, BulkModelViewSet): class OrgBulkModelViewSet(CommonApiMixin, OrgQuerySetMixin, BulkModelViewSet):
def allow_bulk_destroy(self, qs, filtered): def allow_bulk_destroy(self, qs, filtered):
qs_count = qs.count() qs_count = qs.count()

View File

@ -72,13 +72,13 @@ class MailTestingAPI(APIView):
continue continue
else: else:
break break
return Response({"error": str(resp)}, status=401) return Response({"error": str(resp)}, status=400)
except Exception as e: except Exception as e:
print(e) print(e)
return Response({"error": str(e)}, status=401) return Response({"error": str(e)}, status=400)
return Response({"msg": self.success_message.format(email_recipient)}) return Response({"msg": self.success_message.format(email_recipient)})
else: else:
return Response({"error": str(serializer.errors)}, status=401) return Response({"error": str(serializer.errors)}, status=400)
class LDAPTestingConfigAPI(APIView): class LDAPTestingConfigAPI(APIView):
@ -88,10 +88,10 @@ class LDAPTestingConfigAPI(APIView):
def post(self, request): def post(self, request):
serializer = self.serializer_class(data=request.data) serializer = self.serializer_class(data=request.data)
if not serializer.is_valid(): if not serializer.is_valid():
return Response({"error": str(serializer.errors)}, status=401) return Response({"error": str(serializer.errors)}, status=400)
config = self.get_ldap_config(serializer) config = self.get_ldap_config(serializer)
ok, msg = LDAPTestUtil(config).test_config() ok, msg = LDAPTestUtil(config).test_config()
status = 200 if ok else 401 status = 200 if ok else 400
return Response(msg, status=status) return Response(msg, status=status)
@staticmethod @staticmethod
@ -124,11 +124,11 @@ class LDAPTestingLoginAPI(APIView):
def post(self, request): def post(self, request):
serializer = self.serializer_class(data=request.data) serializer = self.serializer_class(data=request.data)
if not serializer.is_valid(): if not serializer.is_valid():
return Response({"error": str(serializer.errors)}, status=401) return Response({"error": str(serializer.errors)}, status=400)
username = serializer.validated_data['username'] username = serializer.validated_data['username']
password = serializer.validated_data['password'] password = serializer.validated_data['password']
ok, msg = LDAPTestUtil().test_login(username, password) ok, msg = LDAPTestUtil().test_login(username, password)
status = 200 if ok else 401 status = 200 if ok else 400
return Response(msg, status=status) return Response(msg, status=status)
@ -236,14 +236,14 @@ class LDAPUserImportAPI(APIView):
try: try:
users = self.get_ldap_users() users = self.get_ldap_users()
except Exception as e: except Exception as e:
return Response({'error': str(e)}, status=401) return Response({'error': str(e)}, status=400)
if users is None: if users is None:
return Response({'msg': _('Get ldap users is None')}, status=401) return Response({'msg': _('Get ldap users is None')}, status=400)
errors = LDAPImportUtil().perform_import(users) errors = LDAPImportUtil().perform_import(users)
if errors: if errors:
return Response({'errors': errors}, status=401) return Response({'errors': errors}, status=400)
count = users if users is None else len(users) count = users if users is None else len(users)
return Response({'msg': _('Imported {} users successfully').format(count)}) return Response({'msg': _('Imported {} users successfully').format(count)})
@ -276,6 +276,7 @@ class PublicSettingApi(generics.RetrieveAPIView):
class SettingsApi(generics.RetrieveUpdateAPIView): class SettingsApi(generics.RetrieveUpdateAPIView):
permission_classes = (IsSuperUser,)
serializer_class = SettingsSerializer serializer_class = SettingsSerializer
def get_object(self): def get_object(self):

View File

@ -42,7 +42,7 @@ class LdapSettingSerializer(serializers.Serializer):
AUTH_LDAP_BIND_PASSWORD = serializers.CharField(max_length=1024, write_only=True, required=False) AUTH_LDAP_BIND_PASSWORD = serializers.CharField(max_length=1024, write_only=True, required=False)
AUTH_LDAP_SEARCH_OU = serializers.CharField(max_length=1024, allow_blank=True, required=False) AUTH_LDAP_SEARCH_OU = serializers.CharField(max_length=1024, allow_blank=True, required=False)
AUTH_LDAP_SEARCH_FILTER = serializers.CharField(max_length=1024, required=True) AUTH_LDAP_SEARCH_FILTER = serializers.CharField(max_length=1024, required=True)
AUTH_LDAP_USER_ATTR_MAP = serializers.CharField(max_length=1024, required=True) AUTH_LDAP_USER_ATTR_MAP = serializers.DictField(required=True)
AUTH_LDAP = serializers.BooleanField(required=False) AUTH_LDAP = serializers.BooleanField(required=False)
@ -75,7 +75,7 @@ class SecuritySettingSerializer(serializers.Serializer):
SECURITY_SERVICE_ACCOUNT_REGISTRATION = serializers.BooleanField(required=True) SECURITY_SERVICE_ACCOUNT_REGISTRATION = serializers.BooleanField(required=True)
SECURITY_LOGIN_LIMIT_COUNT = serializers.IntegerField(min_value=3, max_value=99999, required=True) SECURITY_LOGIN_LIMIT_COUNT = serializers.IntegerField(min_value=3, max_value=99999, required=True)
SECURITY_LOGIN_LIMIT_TIME = serializers.IntegerField(min_value=5, max_value=99999, required=True) SECURITY_LOGIN_LIMIT_TIME = serializers.IntegerField(min_value=5, max_value=99999, required=True)
SECURITY_MAX_IDLE_TIME = serializers.IntegerField(min_value=5, max_value=99999, required=False) SECURITY_MAX_IDLE_TIME = serializers.IntegerField(min_value=1, max_value=99999, required=False)
SECURITY_PASSWORD_EXPIRATION_TIME = serializers.IntegerField(min_value=1, max_value=99999, required=True) SECURITY_PASSWORD_EXPIRATION_TIME = serializers.IntegerField(min_value=1, max_value=99999, required=True)
SECURITY_PASSWORD_MIN_LENGTH = serializers.IntegerField(min_value=6, max_value=30, required=True) SECURITY_PASSWORD_MIN_LENGTH = serializers.IntegerField(min_value=6, max_value=30, required=True)
SECURITY_PASSWORD_UPPER_CASE = serializers.BooleanField(required=False) SECURITY_PASSWORD_UPPER_CASE = serializers.BooleanField(required=False)

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.10 on 2020-05-08 13:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0025_auto_20200206_1216'),
]
operations = [
migrations.AlterField(
model_name='user',
name='source',
field=models.CharField(choices=[('local', 'Local'), ('ldap', 'LDAP/AD'), ('openid', 'OpenID'), ('radius', 'Radius'), ('cas', 'CAS')], default='ldap', max_length=30, verbose_name='Source'),
),
]