Compare commits

..

15 Commits

Author SHA1 Message Date
ibuler
7c08582e7d Merge branch 'master' into v2.0 2020-07-08 16:14:19 +08:00
ibuler
3555fe1c20 Merge branch 'v2.0' of github.com:jumpserver/jumpserver into v2.0 2020-07-08 16:14:06 +08:00
ibuler
4a1045ba81 ci(build&release): 自动化构建发布 2020-07-08 15:58:57 +08:00
ibuler
7fc3218010 添加example api 2020-07-08 15:58:57 +08:00
ibuler
0dd4c8adc2 Merge branch 'v2.0' of github.com:jumpserver/jumpserver into v2.0 2020-07-08 15:48:05 +08:00
xinwen
a9626c2b39 Merge pull request #4223 from jumpserver/update-add-filter
[Update] assets/gathered_user 添加过滤字段
2020-07-01 17:32:58 +08:00
xinwen
b265fad50f [Update] assets/gathered_user 添加过滤字段 2020-07-01 17:21:00 +08:00
BaiJiangJie
f40b50dddd Merge pull request #4209 from jumpserver/ftp_log_order
fix:修改 ftp 日志按开始日期排序
2020-07-01 15:02:44 +08:00
xinwen
0d6255b07f Merge pull request #4218 from jumpserver/system-user-add-filter
System user add filter
2020-07-01 15:01:07 +08:00
xinwen
06b02cfcfd [Update] 系统用户添加过滤字段 2020-07-01 14:57:52 +08:00
jym503558564
a7e1ed6f03 fix:修改 ftp 日志按开始日期排序 2020-07-01 11:04:42 +08:00
xinwen
1c6f89519a Merge pull request #4201 from jumpserver/fix-i18n
Fix i18n
2020-06-30 14:21:44 +08:00
xinwen
24d093747f [Fix] X-Pack/云管中心 i18n 2020-06-30 14:18:21 +08:00
ibuler
7548bb8976 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2020-06-17 11:33:57 +08:00
ibuler
8ffa5e0aec 添加example api 2020-06-16 20:11:50 +08:00
64 changed files with 396 additions and 1313 deletions

View File

@@ -1,12 +0,0 @@
on: [push, pull_request, release]
name: JumpServer repos generic handler
jobs:
generic_handler:
name: Run generic handler
runs-on: ubuntu-latest
steps:
- uses: jumpserver/action-generic-handler@master
env:
GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }}

View File

@@ -1,33 +1,19 @@
FROM registry.fit2cloud.com/public/python:v3 as stage-build
MAINTAINER Jumpserver Team <ibuler@qq.com>
ARG VERSION
ENV VERSION=$VERSION
WORKDIR /opt/jumpserver
ADD . .
RUN cd utils && bash -ixeu build.sh
FROM registry.fit2cloud.com/public/python:v3 FROM registry.fit2cloud.com/public/python:v3
ARG PIP_MIRROR=https://pypi.douban.com/simple MAINTAINER Jumpserver Team <ibuler@qq.com>
ENV PIP_MIRROR=$PIP_MIRROR
ARG MYSQL_MIRROR=https://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql57-community-el6/
ENV MYSQL_MIRROR=$MYSQL_MIRROR
WORKDIR /opt/jumpserver WORKDIR /opt/jumpserver
COPY ./requirements ./requirements
RUN useradd jumpserver RUN useradd jumpserver
RUN yum -y install epel-release && \
echo -e "[mysql]\nname=mysql\nbaseurl=${MYSQL_MIRROR}\ngpgcheck=0\nenabled=1" > /etc/yum.repos.d/mysql.repo
RUN yum -y install $(cat requirements/rpm_requirements.txt)
RUN pip install --upgrade pip setuptools wheel -i ${PIP_MIRROR} && \
pip config set global.index-url ${PIP_MIRROR}
RUN pip install -r requirements/requirements.txt || pip install -r requirements/requirements.txt
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver COPY ./requirements /tmp/requirements
RUN yum -y install epel-release && \
echo -e "[mysql]\nname=mysql\nbaseurl=https://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql57-community-el6/\ngpgcheck=0\nenabled=1" > /etc/yum.repos.d/mysql.repo
RUN cd /tmp/requirements && yum -y install $(cat rpm_requirements.txt)
RUN cd /tmp/requirements && pip install --upgrade pip setuptools && pip install wheel && \
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt || pip install -r requirements.txt
RUN mkdir -p /root/.ssh/ && echo -e "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config RUN mkdir -p /root/.ssh/ && echo -e "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config
COPY . /opt/jumpserver
RUN echo > config.yml RUN echo > config.yml
VOLUME /opt/jumpserver/data VOLUME /opt/jumpserver/data
VOLUME /opt/jumpserver/logs VOLUME /opt/jumpserver/logs

View File

@@ -14,7 +14,7 @@ from .. import serializers
from ..tasks import ( from ..tasks import (
update_asset_hardware_info_manual, test_asset_connectivity_manual update_asset_hardware_info_manual, test_asset_connectivity_manual
) )
from ..filters import AssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend from ..filters import AssetByNodeFilterBackend, LabelFilterBackend
logger = get_logger(__file__) logger = get_logger(__file__)
@@ -32,7 +32,7 @@ class AssetViewSet(OrgBulkModelViewSet):
model = Asset model = Asset
filter_fields = ( filter_fields = (
"hostname", "ip", "systemuser__id", "admin_user__id", "platform__base", "hostname", "ip", "systemuser__id", "admin_user__id", "platform__base",
"is_active", 'ip' "is_active"
) )
search_fields = ("hostname", "ip") search_fields = ("hostname", "ip")
ordering_fields = ("hostname", "ip", "port", "cpu_cores") ordering_fields = ("hostname", "ip", "port", "cpu_cores")
@@ -41,7 +41,7 @@ class AssetViewSet(OrgBulkModelViewSet):
'display': serializers.AssetDisplaySerializer, 'display': serializers.AssetDisplaySerializer,
} }
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
extra_filter_backends = [AssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend] extra_filter_backends = [AssetByNodeFilterBackend, LabelFilterBackend]
def set_assets_node(self, assets): def set_assets_node(self, assets):
if not isinstance(assets, list): if not isinstance(assets, list):

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from rest_framework.compat import coreapi, coreschema import coreapi
from rest_framework import filters from rest_framework import filters
from django.db.models import Q from django.db.models import Q
@@ -117,23 +117,3 @@ class AssetRelatedByNodeFilterBackend(AssetByNodeFilterBackend):
def perform_query(pattern, queryset): def perform_query(pattern, queryset):
return queryset.filter(asset__nodes__key__regex=pattern).distinct() return queryset.filter(asset__nodes__key__regex=pattern).distinct()
class IpInFilterBackend(filters.BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
ips = request.query_params.get('ips')
if not ips:
return queryset
ip_list = [i.strip() for i in ips.split(',')]
queryset = queryset.filter(ip__in=ip_list)
return queryset
def get_schema_fields(self, view):
return [
coreapi.Field(
name='ips', location='query', required=False, type='string',
schema=coreschema.String(
title='ips',
description='ip in filter'
)
)
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 2.2.10 on 2020-07-11 09:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0049_systemuser_sftp_root'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='created_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'),
),
]

View File

@@ -1,22 +0,0 @@
# Generated by Django 2.2.10 on 2020-07-13 03:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0050_auto_20200711_1740'),
]
operations = [
migrations.AlterField(
model_name='domain',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterUniqueTogether(
name='domain',
unique_together={('org_id', 'name')},
),
]

View File

@@ -1,22 +0,0 @@
# Generated by Django 2.2.10 on 2020-07-15 07:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0051_auto_20200713_1143'),
]
operations = [
migrations.AlterField(
model_name='commandfilter',
name='name',
field=models.CharField(max_length=64, verbose_name='Name'),
),
migrations.AlterUniqueTogether(
name='commandfilter',
unique_together={('org_id', 'name')},
),
]

View File

@@ -221,7 +221,7 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
hostname_raw = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Hostname raw')) hostname_raw = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Hostname raw'))
labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels")) labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels"))
created_by = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Created by')) created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by'))
date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created')) date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created'))
comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment')) comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment'))
@@ -244,6 +244,10 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
def platform_base(self): def platform_base(self):
return self.platform.base return self.platform.base
@lazyproperty
def admin_user_display(self):
return self.admin_user.name
@lazyproperty @lazyproperty
def admin_user_username(self): def admin_user_username(self):
"""求可连接性时直接用用户名去取避免再查一次admin user """求可连接性时直接用用户名去取避免再查一次admin user

View File

@@ -3,6 +3,7 @@
from django.db import models, transaction from django.db import models, transaction
from django.db.models import Max from django.db.models import Max
from django.core.cache import cache
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orgs.mixins.models import OrgManager from orgs.mixins.models import OrgManager
@@ -58,17 +59,19 @@ class AuthBook(BaseUser):
""" """
username = kwargs['username'] username = kwargs['username']
asset = kwargs['asset'] asset = kwargs['asset']
with transaction.atomic(): key_lock = 'KEY_LOCK_CREATE_AUTH_BOOK_{}_{}'.format(username, asset.id)
# 使用select_for_update限制并发创建相同的username、asset条目 with cache.lock(key_lock):
instances = cls.objects.select_for_update().filter(username=username, asset=asset) with transaction.atomic():
instances.filter(is_latest=True).update(is_latest=False) cls.objects.filter(
max_version = cls.get_max_version(username, asset) username=username, asset=asset, is_latest=True
kwargs.update({ ).update(is_latest=False)
'version': max_version + 1, max_version = cls.get_max_version(username, asset)
'is_latest': True kwargs.update({
}) 'version': max_version + 1,
obj = cls.objects.create(**kwargs) 'is_latest': True
return obj })
obj = cls.objects.create(**kwargs)
return obj
@property @property
def connectivity(self): def connectivity(self):

View File

@@ -18,7 +18,7 @@ __all__ = [
class CommandFilter(OrgModelMixin): class CommandFilter(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=64, verbose_name=_("Name")) name = models.CharField(max_length=64, unique=True, verbose_name=_("Name"))
is_active = models.BooleanField(default=True, verbose_name=_('Is active')) is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
comment = models.TextField(blank=True, default='', verbose_name=_("Comment")) comment = models.TextField(blank=True, default='', verbose_name=_("Comment"))
date_created = models.DateTimeField(auto_now_add=True) date_created = models.DateTimeField(auto_now_add=True)
@@ -29,7 +29,6 @@ class CommandFilter(OrgModelMixin):
return self.name return self.name
class Meta: class Meta:
unique_together = [('org_id', 'name')]
verbose_name = _("Command filter") verbose_name = _("Command filter")

View File

@@ -17,14 +17,13 @@ __all__ = ['Domain', 'Gateway']
class Domain(OrgModelMixin): class Domain(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, verbose_name=_('Name')) name = models.CharField(max_length=128, unique=True, verbose_name=_('Name'))
comment = models.TextField(blank=True, verbose_name=_('Comment')) comment = models.TextField(blank=True, verbose_name=_('Comment'))
date_created = models.DateTimeField(auto_now_add=True, null=True, date_created = models.DateTimeField(auto_now_add=True, null=True,
verbose_name=_('Date created')) verbose_name=_('Date created'))
class Meta: class Meta:
verbose_name = _("Domain") verbose_name = _("Domain")
unique_together = [('org_id', 'name')]
def __str__(self): def __str__(self):
return self.name return self.name

View File

@@ -199,20 +199,6 @@ class FamilyMixin:
) )
return child return child
def get_or_create_child(self, value, _id=None):
"""
:return: Node, bool (created)
"""
children = self.get_children()
exist = children.filter(value=value).exists()
if exist:
child = children.filter(value=value).first()
created = False
else:
child = self.create_child(value, _id)
created = True
return child, created
def get_next_child_key(self): def get_next_child_key(self):
mark = self.child_mark mark = self.child_mark
self.child_mark += 1 self.child_mark += 1

View File

@@ -67,9 +67,6 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
slug_field='name', queryset=Platform.objects.all(), label=_("Platform") slug_field='name', queryset=Platform.objects.all(), label=_("Platform")
) )
protocols = ProtocolsField(label=_('Protocols'), required=False) protocols = ProtocolsField(label=_('Protocols'), required=False)
domain_display = serializers.ReadOnlyField(source='domain.name')
admin_user_display = serializers.ReadOnlyField(source='admin_user.name')
""" """
资产的数据结构 资产的数据结构
""" """
@@ -85,7 +82,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
'created_by', 'date_created', 'hardware_info', 'created_by', 'date_created', 'hardware_info',
] ]
fields_fk = [ fields_fk = [
'admin_user', 'admin_user_display', 'domain', 'domain_display', 'platform' 'admin_user', 'admin_user_display', 'domain', 'platform'
] ]
fk_only_fields = { fk_only_fields = {
'platform': ['name'] 'platform': ['name']

View File

@@ -185,9 +185,7 @@ def on_asset_nodes_add(sender, instance=None, action='', model=None, pk_set=None
system_users_assets = defaultdict(set) system_users_assets = defaultdict(set)
for system_user in system_users: for system_user in system_users:
assets_has_set = system_user.assets.all().filter(id__in=assets).values_list('id', flat=True) system_users_assets[system_user].update(set(assets))
assets_remain = set(assets) - set(assets_has_set)
system_users_assets[system_user].update(assets_remain)
for system_user, _assets in system_users_assets.items(): for system_user, _assets in system_users_assets.items():
system_user.assets.add(*tuple(_assets)) system_user.assets.add(*tuple(_assets))

View File

@@ -85,7 +85,7 @@ def test_asset_user_connectivity_util(asset_user, task_name):
raw, summary = test_user_connectivity( raw, summary = test_user_connectivity(
task_name=task_name, asset=asset_user.asset, task_name=task_name, asset=asset_user.asset,
username=asset_user.username, password=asset_user.password, username=asset_user.username, password=asset_user.password,
private_key=asset_user.private_key_file private_key=asset_user.private_key
) )
except Exception as e: except Exception as e:
logger.warn("Failed run adhoc {}, {}".format(task_name, e)) logger.warn("Failed run adhoc {}, {}".format(task_name, e))

View File

@@ -64,7 +64,7 @@ GATHER_ASSET_USERS_TASKS = [
"action": { "action": {
"module": "shell", "module": "shell",
"args": "users=$(getent passwd | grep -v 'nologin' | " "args": "users=$(getent passwd | grep -v 'nologin' | "
"grep -v 'shudown' | awk -F: '{ print $1 }');for i in $users;do last -w -F $i -1 | " "grep -v 'shudown' | awk -F: '{ print $1 }');for i in $users;do last -F $i -1 | "
"head -1 | grep -v '^$' | awk '{ print $1\"@\"$3\"@\"$5,$6,$7,$8 }';done" "head -1 | grep -v '^$' | awk '{ print $1\"@\"$3\"@\"$5,$6,$7,$8 }';done"
} }
} }

View File

@@ -31,10 +31,7 @@ def test_system_user_connectivity_util(system_user, assets, task_name):
""" """
from ops.utils import update_or_create_ansible_task from ops.utils import update_or_create_ansible_task
# hosts = clean_ansible_task_hosts(assets, system_user=system_user) hosts = clean_ansible_task_hosts(assets, system_user=system_user)
# TODO: 这里不传递系统用户因为clean_ansible_task_hosts会通过system_user来判断是否可以推送
# 不符合测试可连接性逻辑, 后面需要优化此逻辑
hosts = clean_ansible_task_hosts(assets)
if not hosts: if not hosts:
return {} return {}
platform_hosts_map = {} platform_hosts_map = {}

View File

@@ -1,18 +0,0 @@
# Generated by Django 2.2.10 on 2020-06-24 08:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('audits', '0008_auto_20200508_2105'),
]
operations = [
migrations.AlterField(
model_name='ftplog',
name='operate',
field=models.CharField(choices=[('Delete', 'Delete'), ('Upload', 'Upload'), ('Download', 'Download'), ('Rmdir', 'Rmdir'), ('Rename', 'Rename'), ('Mkdir', 'Mkdir'), ('Symlink', 'Symlink')], max_length=16, verbose_name='Operate'),
),
]

View File

@@ -14,30 +14,12 @@ __all__ = [
class FTPLog(OrgModelMixin): class FTPLog(OrgModelMixin):
OPERATE_DELETE = 'Delete'
OPERATE_UPLOAD = 'Upload'
OPERATE_DOWNLOAD = 'Download'
OPERATE_RMDIR = 'Rmdir'
OPERATE_RENAME = 'Rename'
OPERATE_MKDIR = 'Mkdir'
OPERATE_SYMLINK = 'Symlink'
OPERATE_CHOICES = (
(OPERATE_DELETE, _('Delete')),
(OPERATE_UPLOAD, _('Upload')),
(OPERATE_DOWNLOAD, _('Download')),
(OPERATE_RMDIR, _('Rmdir')),
(OPERATE_RENAME, _('Rename')),
(OPERATE_MKDIR, _('Mkdir')),
(OPERATE_SYMLINK, _('Symlink'))
)
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
user = models.CharField(max_length=128, verbose_name=_('User')) user = models.CharField(max_length=128, verbose_name=_('User'))
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)
asset = models.CharField(max_length=1024, verbose_name=_("Asset")) asset = models.CharField(max_length=1024, verbose_name=_("Asset"))
system_user = models.CharField(max_length=128, verbose_name=_("System user")) system_user = models.CharField(max_length=128, verbose_name=_("System user"))
operate = models.CharField(max_length=16, verbose_name=_("Operate"), choices=OPERATE_CHOICES) 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, verbose_name=_('Date start')) date_start = models.DateTimeField(auto_now_add=True, verbose_name=_('Date start'))

View File

@@ -12,13 +12,12 @@ from . import models
class FTPLogSerializer(serializers.ModelSerializer): class FTPLogSerializer(serializers.ModelSerializer):
operate_display = serializers.ReadOnlyField(source='get_operate_display')
class Meta: class Meta:
model = models.FTPLog model = models.FTPLog
fields = ( fields = (
'id', 'user', 'remote_addr', 'asset', 'system_user', 'org_id', 'id', 'user', 'remote_addr', 'asset', 'system_user',
'operate', 'filename', 'is_success', 'date_start', 'operate_display' 'operate', 'filename', 'is_success', 'date_start'
) )
@@ -40,7 +39,7 @@ class OperateLogSerializer(serializers.ModelSerializer):
model = models.OperateLog model = models.OperateLog
fields = ( fields = (
'id', 'user', 'action', 'resource_type', 'resource', 'id', 'user', 'action', 'resource_type', 'resource',
'remote_addr', 'datetime', 'org_id' 'remote_addr', 'datetime'
) )
@@ -66,7 +65,7 @@ class CommandExecutionSerializer(serializers.ModelSerializer):
fields_mini = ['id'] fields_mini = ['id']
fields_small = fields_mini + [ fields_small = fields_mini + [
'run_as', 'command', 'user', 'is_finished', 'run_as', 'command', 'user', 'is_finished',
'date_start', 'result', 'is_success', 'org_id' 'date_start', 'result', 'is_success'
] ]
fields = fields_small + ['hosts', 'run_as_display', 'user_display'] fields = fields_small + ['hosts', 'run_as_display', 'user_display']
extra_kwargs = { extra_kwargs = {

View File

@@ -1,17 +1,17 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import traceback
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from radiusauth.backends import RADIUSBackend, RADIUSRealmBackend from radiusauth.backends import RADIUSBackend, RADIUSRealmBackend
from django.conf import settings from django.conf import settings
from pyrad.packet import AccessRequest
User = get_user_model() User = get_user_model()
class CreateUserMixin: class CreateUserMixin:
def get_django_user(self, username, password=None, *args, **kwargs): def get_django_user(self, username, password=None):
if isinstance(username, bytes): if isinstance(username, bytes):
username = username.decode() username = username.decode()
try: try:
@@ -27,23 +27,6 @@ class CreateUserMixin:
user.save() user.save()
return user return user
def _perform_radius_auth(self, client, packet):
# TODO: 等待官方库修复这个BUG
try:
return super()._perform_radius_auth(client, packet)
except UnicodeError as e:
import sys
tb = ''.join(traceback.format_exception(*sys.exc_info(), limit=2, chain=False))
if tb.find("cl.decode") != -1:
return [], False, False
return None
def authenticate(self, *args, **kwargs):
# 校验用户时会传入public_key参数父类authentication中不接受public_key参数所以要pop掉
# TODO:需要优化各backend的authenticate方法django进行调用前会检测各authenticate的参数
kwargs.pop('public_key', None)
return super().authenticate(*args, **kwargs)
class RadiusBackend(CreateUserMixin, RADIUSBackend): class RadiusBackend(CreateUserMixin, RADIUSBackend):
pass pass

View File

@@ -10,7 +10,6 @@ from users.utils import (
) )
reason_password_failed = 'password_failed' reason_password_failed = 'password_failed'
reason_password_decrypt_failed = 'password_decrypt_failed'
reason_mfa_failed = 'mfa_failed' reason_mfa_failed = 'mfa_failed'
reason_mfa_unset = 'mfa_unset' reason_mfa_unset = 'mfa_unset'
reason_user_not_exist = 'user_not_exist' reason_user_not_exist = 'user_not_exist'
@@ -20,7 +19,6 @@ reason_user_inactive = 'user_inactive'
reason_choices = { reason_choices = {
reason_password_failed: _('Username/password check failed'), reason_password_failed: _('Username/password check failed'),
reason_password_decrypt_failed: _('Password decrypt failed'),
reason_mfa_failed: _('MFA failed'), reason_mfa_failed: _('MFA failed'),
reason_mfa_unset: _('MFA unset'), reason_mfa_unset: _('MFA unset'),
reason_user_not_exist: _("Username does not exist"), reason_user_not_exist: _("Username does not exist"),

View File

@@ -10,7 +10,7 @@ class UserLoginForm(forms.Form):
username = forms.CharField(label=_('Username'), max_length=100) username = forms.CharField(label=_('Username'), max_length=100)
password = forms.CharField( password = forms.CharField(
label=_('Password'), widget=forms.PasswordInput, label=_('Password'), widget=forms.PasswordInput,
max_length=1024, strip=False max_length=128, strip=False
) )
def confirm_login_allowed(self, user): def confirm_login_allowed(self, user):

View File

@@ -7,7 +7,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<form id="form" class="m-t" role="form" method="post" action=""> <form class="m-t" role="form" method="post" action="">
{% csrf_token %} {% csrf_token %}
{% if form.non_field_errors %} {% if form.non_field_errors %}
<div style="line-height: 17px;"> <div style="line-height: 17px;">
@@ -26,7 +26,7 @@
{% endif %} {% endif %}
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="password" class="form-control" id="password" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required=""> <input type="password" class="form-control" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required="">
{% if form.errors.password %} {% if form.errors.password %}
<div class="help-block field-error"> <div class="help-block field-error">
<p class="red-fonts">{{ form.errors.password.as_text }}</p> <p class="red-fonts">{{ form.errors.password.as_text }}</p>
@@ -36,7 +36,7 @@
<div> <div>
{{ form.captcha }} {{ form.captcha }}
</div> </div>
<button type="submit" class="btn btn-primary block full-width m-b" onclick="doLogin();return false;">{% trans 'Login' %}</button> <button type="submit" class="btn btn-primary block full-width m-b">{% trans 'Login' %}</button>
{% if demo_mode %} {% if demo_mode %}
<p class="text-muted font-bold" style="color: red"> <p class="text-muted font-bold" style="color: red">
@@ -64,20 +64,4 @@
{% endif %} {% endif %}
</form> </form>
<script type="text/javascript" src="/static/js/plugins/jsencrypt/jsencrypt.min.js"></script>
<script>
function encryptLoginPassword(password, rsaPublicKey){
var jsencrypt = new JSEncrypt(); //加密对象
jsencrypt.setPublicKey(rsaPublicKey); // 设置密钥
return jsencrypt.encrypt(password); //加密
}
function doLogin() {
//公钥加密
var rsaPublicKey = "{{ rsa_public_key }}"
var password =$('#password').val(); //明文密码
var passwordEncrypted = encryptLoginPassword(password, rsaPublicKey)
$('#password').val(passwordEncrypted); //返回给密码输入input
$('#form').submit();//post提交
}
</script>
{% endblock %} {% endblock %}

View File

@@ -98,7 +98,7 @@
{% endif %} {% endif %}
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="password" class="form-control" id="password" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required=""> <input type="password" class="form-control" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required="">
{% if form.errors.password %} {% if form.errors.password %}
<div class="help-block field-error"> <div class="help-block field-error">
<p class="red-fonts">{{ form.errors.password.as_text }}</p> <p class="red-fonts">{{ form.errors.password.as_text }}</p>
@@ -109,7 +109,7 @@
{{ form.captcha }} {{ form.captcha }}
</div> </div>
<div class="form-group" style="margin-top: 10px"> <div class="form-group" style="margin-top: 10px">
<button type="submit" class="btn btn-transparent" onclick="doLogin();return false;">{% trans 'Login' %}</button> <button type="submit" class="btn btn-transparent">{% trans 'Login' %}</button>
</div> </div>
<div style="text-align: center"> <div style="text-align: center">
<a href="{% url 'authentication:forgot-password' %}"> <a href="{% url 'authentication:forgot-password' %}">
@@ -127,21 +127,4 @@
</div> </div>
</body> </body>
<script type="text/javascript" src="/static/js/plugins/jsencrypt/jsencrypt.min.js"></script>
<script>
function encryptLoginPassword(password, rsaPublicKey){
var jsencrypt = new JSEncrypt(); //加密对象
jsencrypt.setPublicKey(rsaPublicKey); // 设置密钥
return jsencrypt.encrypt(password); //加密
}
function doLogin() {
//公钥加密
var rsaPublicKey = "{{ rsa_public_key }}"
var password =$('#password').val(); //明文密码
var passwordEncrypted = encryptLoginPassword(password, rsaPublicKey)
$('#password').val(passwordEncrypted); //返回给密码输入input
$('#contact-form').submit();//post提交
}
</script>
</html> </html>

View File

@@ -1,15 +1 @@
from .utils import gen_key_pair, rsa_decrypt, rsa_encrypt
def test_rsa_encrypt_decrypt(message='test-password-$%^&*'):
""" 测试加密/解密 """
print('Need to encrypt message: {}'.format(message))
rsa_private_key, rsa_public_key = gen_key_pair()
print('RSA public key: \n{}'.format(rsa_public_key))
print('RSA private key: \n{}'.format(rsa_private_key))
message_encrypted = rsa_encrypt(message, rsa_public_key)
print('Encrypted message: {}'.format(message_encrypted))
message_decrypted = rsa_decrypt(message_encrypted, rsa_private_key)
print('Decrypted message: {}'.format(message_decrypted))

View File

@@ -1,47 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import base64
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
from Crypto import Random
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from common.utils import get_logger
from . import errors from . import errors
logger = get_logger(__file__)
def gen_key_pair():
""" 生成加密key
用于登录页面提交用户名/密码时,对密码进行加密(前端)/解密(后端)
"""
random_generator = Random.new().read
rsa = RSA.generate(1024, random_generator)
rsa_private_key = rsa.exportKey().decode()
rsa_public_key = rsa.publickey().exportKey().decode()
return rsa_private_key, rsa_public_key
def rsa_encrypt(message, rsa_public_key):
""" 加密登录密码 """
key = RSA.importKey(rsa_public_key)
cipher = PKCS1_v1_5.new(key)
cipher_text = base64.b64encode(cipher.encrypt(message.encode())).decode()
return cipher_text
def rsa_decrypt(cipher_text, rsa_private_key=None):
""" 解密登录密码 """
if rsa_private_key is None:
# rsa_private_key 为 None可以能是API请求认证不需要解密
return cipher_text
key = RSA.importKey(rsa_private_key)
cipher = PKCS1_v1_5.new(key)
message = cipher.decrypt(base64.b64decode(cipher_text.encode()), 'error').decode()
return message
def check_user_valid(**kwargs): def check_user_valid(**kwargs):
password = kwargs.pop('password', None) password = kwargs.pop('password', None)
@@ -49,16 +11,6 @@ def check_user_valid(**kwargs):
username = kwargs.pop('username', None) username = kwargs.pop('username', None)
request = kwargs.get('request') request = kwargs.get('request')
# 获取解密密钥,对密码进行解密
rsa_private_key = request.session.get('rsa_private_key')
if rsa_private_key is not None:
try:
password = rsa_decrypt(password, rsa_private_key)
except Exception as e:
logger.error(e, exc_info=True)
logger.error('Need decrypt password => {}'.format(password))
return None, errors.reason_password_decrypt_failed
user = authenticate(request, username=username, user = authenticate(request, username=username,
password=password, public_key=public_key) password=password, public_key=public_key)
if not user: if not user:

View File

@@ -22,7 +22,7 @@ from common.utils import get_request_ip, get_object_or_none
from users.utils import ( from users.utils import (
redirect_user_first_login_or_index redirect_user_first_login_or_index
) )
from .. import forms, mixins, errors, utils from .. import forms, mixins, errors
__all__ = [ __all__ = [
@@ -108,13 +108,9 @@ class UserLoginView(mixins.AuthMixin, FormView):
return self.form_class return self.form_class
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
# 生成加解密密钥对public_key传递给前端private_key存入session中供解密使用
rsa_private_key, rsa_public_key = utils.gen_key_pair()
self.request.session['rsa_private_key'] = rsa_private_key
context = { context = {
'demo_mode': os.environ.get("DEMO_MODE"), 'demo_mode': os.environ.get("DEMO_MODE"),
'AUTH_OPENID': settings.AUTH_OPENID, 'AUTH_OPENID': settings.AUTH_OPENID,
'rsa_public_key': rsa_public_key.replace('\n', '\\n')
} }
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)

View File

@@ -1,28 +0,0 @@
from django.db.models import Aggregate
class GroupConcat(Aggregate):
function = 'GROUP_CONCAT'
template = '%(function)s(%(distinct)s %(expressions)s %(order_by)s %(separator))'
allow_distinct = False
def __init__(self, expression, distinct=False, order_by=None, separator=',', **extra):
order_by_clause = ''
if order_by is not None:
order = 'ASC'
prefix, body = order_by[1], order_by[1:]
if prefix == '-':
order = 'DESC'
elif prefix == '+':
pass
else:
body = order_by
order_by_clause = f'ORDER BY {body} {order}'
super().__init__(
expression,
distinct='DISTINCT' if distinct else '',
order_by=order_by_clause,
separator=f'SEPARATOR {separator}',
**extra
)

View File

@@ -1,11 +0,0 @@
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from ..mixins.api import SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin
class JmsGenericViewSet(SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin, GenericViewSet):
pass
class JMSModelViewSet(SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin, ModelViewSet):
pass

View File

@@ -6,27 +6,18 @@ import chardet
import codecs import codecs
import unicodecsv import unicodecsv
from django.utils.translation import ugettext as _
from rest_framework.parsers import BaseParser from rest_framework.parsers import BaseParser
from rest_framework.exceptions import ParseError, APIException from rest_framework.exceptions import ParseError
from rest_framework import status
from common.utils import get_logger from common.utils import get_logger
logger = get_logger(__file__) logger = get_logger(__file__)
class CsvDataTooBig(APIException):
status_code = status.HTTP_400_BAD_REQUEST
default_code = 'csv_data_too_big'
default_detail = _('The max size of CSV is %d bytes')
class JMSCSVParser(BaseParser): class JMSCSVParser(BaseParser):
""" """
Parses CSV file to serializer data Parses CSV file to serializer data
""" """
CSV_UPLOAD_MAX_SIZE = 1024 * 1024 * 10
media_type = 'text/csv' media_type = 'text/csv'
@@ -47,39 +38,31 @@ class JMSCSVParser(BaseParser):
yield row yield row
@staticmethod @staticmethod
def _get_fields_map(serializer_cls): def _get_fields_map(serializer):
fields_map = {} fields_map = {}
fields = serializer_cls().fields fields = serializer.fields
fields_map.update({v.label: k for k, v in fields.items()}) fields_map.update({v.label: k for k, v in fields.items()})
fields_map.update({k: k for k, _ in fields.items()}) fields_map.update({k: k for k, _ in fields.items()})
return fields_map return fields_map
@staticmethod @staticmethod
def _replace_chinese_quot(str_): def _process_row(row):
trans_table = str.maketrans({
'': '"',
'': '"',
'': '"',
'': '"',
'\'': '"'
})
return str_.translate(trans_table)
@classmethod
def _process_row(cls, row):
""" """
构建json数据前的行处理 构建json数据前的行处理
""" """
_row = [] _row = []
for col in row: for col in row:
# 列表转换 # 列表转换
if isinstance(col, str) and col.startswith('[') and col.endswith(']'): if isinstance(col, str) and col.find("[") != -1 and col.find("]") != -1:
col = cls._replace_chinese_quot(col) # 替换中文格式引号
col = col.replace("", '"').replace("", '"').\
replace("", '"').replace('', '"').replace("'", '"')
col = json.loads(col) col = json.loads(col)
# 字典转换 # 字典转换
if isinstance(col, str) and col.startswith("{") and col.endswith("}"): if isinstance(col, str) and col.find("{") != -1 and col.find("}") != -1:
col = cls._replace_chinese_quot(col) # 替换中文格式引号
col = col.replace("", '"').replace("", '"'). \
replace("", '"').replace('', '"').replace("'", '"')
col = json.loads(col) col = json.loads(col)
_row.append(col) _row.append(col)
return _row return _row
@@ -99,19 +82,11 @@ class JMSCSVParser(BaseParser):
def parse(self, stream, media_type=None, parser_context=None): def parse(self, stream, media_type=None, parser_context=None):
parser_context = parser_context or {} parser_context = parser_context or {}
try: try:
view = parser_context['view'] serializer = parser_context["view"].get_serializer()
meta = view.request.META
serializer_cls = view.get_serializer_class()
except Exception as e: except Exception as e:
logger.debug(e, exc_info=True) logger.debug(e, exc_info=True)
raise ParseError('The resource does not support imports!') raise ParseError('The resource does not support imports!')
content_length = int(meta.get('CONTENT_LENGTH', meta.get('HTTP_CONTENT_LENGTH', 0)))
if content_length > self.CSV_UPLOAD_MAX_SIZE:
msg = CsvDataTooBig.default_detail % self.CSV_UPLOAD_MAX_SIZE
logger.error(msg)
raise CsvDataTooBig(msg)
try: try:
stream_data = stream.read() stream_data = stream.read()
stream_data = stream_data.strip(codecs.BOM_UTF8) stream_data = stream_data.strip(codecs.BOM_UTF8)
@@ -121,7 +96,7 @@ class JMSCSVParser(BaseParser):
rows = self._gen_rows(binary, charset=encoding) rows = self._gen_rows(binary, charset=encoding)
header = next(rows) header = next(rows)
fields_map = self._get_fields_map(serializer_cls) fields_map = self._get_fields_map(serializer)
header = [fields_map.get(name.strip('*'), '') for name in header] header = [fields_map.get(name.strip('*'), '') for name in header]
data = [] data = []

View File

@@ -1,5 +0,0 @@
from rest_framework.serializers import Serializer
class EmptySerializer(Serializer):
pass

View File

@@ -1,7 +1,3 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from rest_framework.exceptions import APIException
class JMSException(APIException):
pass

View File

@@ -4,7 +4,6 @@ import time
from hashlib import md5 from hashlib import md5
from threading import Thread from threading import Thread
from collections import defaultdict from collections import defaultdict
from itertools import chain
from django.db.models.signals import m2m_changed from django.db.models.signals import m2m_changed
from django.core.cache import cache from django.core.cache import cache
@@ -16,8 +15,8 @@ from common.drf.filters import IDSpmFilter, CustomFilter, IDInFilter
from ..utils import lazyproperty from ..utils import lazyproperty
__all__ = [ __all__ = [
'JSONResponseMixin', 'CommonApiMixin', 'AsyncApiMixin', 'RelationMixin', "JSONResponseMixin", "CommonApiMixin",
'SerializerMixin2', 'QuerySetMixin', 'ExtraFilterFieldsMixin' 'AsyncApiMixin', 'RelationMixin'
] ]
@@ -55,10 +54,9 @@ class ExtraFilterFieldsMixin:
def get_filter_backends(self): def get_filter_backends(self):
if self.filter_backends != self.__class__.filter_backends: if self.filter_backends != self.__class__.filter_backends:
return self.filter_backends return self.filter_backends
backends = list(chain( backends = list(self.filter_backends) + \
self.filter_backends, list(self.default_added_filters) + \
self.default_added_filters, list(self.extra_filter_backends)
self.extra_filter_backends))
return backends return backends
def filter_queryset(self, queryset): def filter_queryset(self, queryset):
@@ -235,32 +233,3 @@ class RelationMixin:
def perform_create(self, serializer): def perform_create(self, serializer):
instance = serializer.save() instance = serializer.save()
self.send_post_add_signal(instance) self.send_post_add_signal(instance)
class SerializerMixin2:
serializer_classes = {}
def get_serializer_class(self):
if self.serializer_classes:
serializer_class = self.serializer_classes.get(
self.action, self.serializer_classes.get('default')
)
if isinstance(serializer_class, dict):
serializer_class = serializer_class.get(
self.request.method.lower, serializer_class.get('default')
)
assert serializer_class, '`serializer_classes` config error'
return serializer_class
return super().get_serializer_class()
class QuerySetMixin:
def get_queryset(self):
queryset = super().get_queryset()
serializer_class = self.get_serializer_class()
if serializer_class and hasattr(serializer_class, 'setup_eager_loading'):
queryset = serializer_class.setup_eager_loading(queryset)
return queryset

View File

@@ -226,7 +226,6 @@ class Config(dict):
'TERMINAL_COMMAND_STORAGE': {}, 'TERMINAL_COMMAND_STORAGE': {},
'SECURITY_MFA_AUTH': False, 'SECURITY_MFA_AUTH': False,
'SECURITY_COMMAND_EXECUTION': True,
'SECURITY_SERVICE_ACCOUNT_REGISTRATION': True, 'SECURITY_SERVICE_ACCOUNT_REGISTRATION': True,
'SECURITY_VIEW_AUTH_NEED_MFA': True, 'SECURITY_VIEW_AUTH_NEED_MFA': True,
'SECURITY_LOGIN_LIMIT_COUNT': 7, 'SECURITY_LOGIN_LIMIT_COUNT': 7,

View File

@@ -92,7 +92,6 @@ CAS_LOGGED_MSG = None
CAS_LOGOUT_COMPLETELY = CONFIG.CAS_LOGOUT_COMPLETELY CAS_LOGOUT_COMPLETELY = CONFIG.CAS_LOGOUT_COMPLETELY
CAS_VERSION = CONFIG.CAS_VERSION CAS_VERSION = CONFIG.CAS_VERSION
CAS_ROOT_PROXIED_AS = CONFIG.CAS_ROOT_PROXIED_AS CAS_ROOT_PROXIED_AS = CONFIG.CAS_ROOT_PROXIED_AS
CAS_CHECK_NEXT = lambda: lambda _next_page: True
# Other setting # Other setting

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,6 @@
# #
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext as _
from rest_framework import status, generics from rest_framework import status, generics
from rest_framework.views import Response from rest_framework.views import Response
from rest_framework_bulk import BulkModelViewSet from rest_framework_bulk import BulkModelViewSet
@@ -23,8 +22,6 @@ logger = get_logger(__file__)
class OrgViewSet(BulkModelViewSet): class OrgViewSet(BulkModelViewSet):
filter_fields = ('name',)
search_fields = ('name', 'comment')
queryset = Organization.objects.all() queryset = Organization.objects.all()
serializer_class = OrgSerializer serializer_class = OrgSerializer
permission_classes = (IsSuperUserOrAppUser,) permission_classes = (IsSuperUserOrAppUser,)
@@ -54,12 +51,10 @@ class OrgViewSet(BulkModelViewSet):
for model in models: for model in models:
data = self.get_data_from_model(model) data = self.get_data_from_model(model)
if data: if data:
msg = _('Organization contains undeleted resources') return Response(status=status.HTTP_400_BAD_REQUEST)
return Response(data={'error': msg}, status=status.HTTP_403_FORBIDDEN)
else: else:
if str(current_org) == str(self.org): if str(current_org) == str(self.org):
msg = _('The current organization cannot be deleted') return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
return Response(data={'error': msg}, status=status.HTTP_403_FORBIDDEN)
self.org.delete() self.org.delete()
return Response({'msg': True}, status=status.HTTP_200_OK) return Response({'msg': True}, status=status.HTTP_200_OK)

View File

@@ -21,7 +21,7 @@ class UserPermissionMixin:
obj = None obj = None
def initial(self, *args, **kwargs): def initial(self, *args, **kwargs):
super().initial(*args, **kwargs) super().initial(*args, *kwargs)
self.obj = self.get_obj() self.obj = self.get_obj()
def get_obj(self): def get_obj(self):

View File

@@ -43,7 +43,4 @@ def on_create_set_created_by(sender, instance=None, **kwargs):
return return
if hasattr(instance, 'created_by') and not instance.created_by: if hasattr(instance, 'created_by') and not instance.created_by:
if current_request and current_request.user.is_authenticated: if current_request and current_request.user.is_authenticated:
user_name = current_request.user.name instance.created_by = current_request.user.name
if isinstance(user_name, str):
user_name = user_name[:30]
instance.created_by = user_name

File diff suppressed because one or more lines are too long

View File

@@ -55,17 +55,21 @@ class CommandQueryMixin:
q = self.request.query_params q = self.request.query_params
multi_command_storage = get_multi_command_storage() multi_command_storage = get_multi_command_storage()
queryset = multi_command_storage.filter( queryset = multi_command_storage.filter(
date_from=date_from, date_to=date_to, date_from=date_from, date_to=date_to, input=q.get("input"),
user=q.get("user"), asset=q.get("asset"), system_user=q.get("system_user"), user=q.get("user"), asset=q.get("asset"),
input=q.get("input"), session=q.get("session_id"), system_user=q.get("system_user"),
risk_level=self.get_query_risk_level(), org_id=self.get_org_id(), risk_level=self.get_query_risk_level(), org_id=self.get_org_id(),
) )
return queryset return queryset
def filter_queryset(self, queryset): def filter_queryset(self, queryset):
# 解决es存储命令时父类根据filter_fields过滤出现异常的问题返回的queryset类型list
return queryset return queryset
def get_filter_fields(self, request):
fields = self.filter_fields
fields.extend(["date_from", "date_to"])
return fields
def get_date_range(self): def get_date_range(self):
now = timezone.now() now = timezone.now()
days_ago = now - timezone.timedelta(days=self.default_days_ago) days_ago = now - timezone.timedelta(days=self.default_days_ago)

View File

@@ -44,7 +44,7 @@ class SessionViewSet(OrgBulkModelViewSet):
permission_classes = (IsOrgAdminOrAppUser, ) permission_classes = (IsOrgAdminOrAppUser, )
filter_fields = [ filter_fields = [
"user", "asset", "system_user", "remote_addr", "user", "asset", "system_user", "remote_addr",
"protocol", "terminal", "is_finished", 'login_from', "protocol", "terminal", "is_finished",
] ]
date_range_filter_fields = [ date_range_filter_fields = [
('date_start', ('date_from', 'date_to')) ('date_start', ('date_from', 'date_to'))

View File

@@ -16,7 +16,7 @@ class CommandBase(object):
@abc.abstractmethod @abc.abstractmethod
def filter(self, date_from=None, date_to=None, def filter(self, date_from=None, date_to=None,
user=None, asset=None, system_user=None, user=None, asset=None, system_user=None,
input=None, session=None, risk_level=None, org_id=None): input=None, session=None):
pass pass
@abc.abstractmethod @abc.abstractmethod

View File

@@ -1,18 +0,0 @@
# Generated by Django 2.2.10 on 2020-07-15 09:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('terminal', '0023_command_risk_level'),
]
operations = [
migrations.AlterField(
model_name='session',
name='login_from',
field=models.CharField(choices=[('ST', 'SSH Terminal'), ('WT', 'Web Terminal')], default='ST', max_length=2, verbose_name='Login from'),
),
]

View File

@@ -188,7 +188,7 @@ class Session(OrgModelMixin):
asset_id = models.CharField(blank=True, default='', max_length=36, db_index=True) asset_id = models.CharField(blank=True, default='', max_length=36, db_index=True)
system_user = models.CharField(max_length=128, verbose_name=_("System user"), db_index=True) system_user = models.CharField(max_length=128, verbose_name=_("System user"), db_index=True)
system_user_id = models.CharField(blank=True, default='', max_length=36, db_index=True) system_user_id = models.CharField(blank=True, default='', max_length=36, db_index=True)
login_from = models.CharField(max_length=2, choices=LOGIN_FROM_CHOICES, default="ST", verbose_name=_("Login from")) login_from = models.CharField(max_length=2, choices=LOGIN_FROM_CHOICES, default="ST")
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)
is_success = models.BooleanField(default=True, db_index=True) is_success = models.BooleanField(default=True, db_index=True)
is_finished = models.BooleanField(default=False, db_index=True) is_finished = models.BooleanField(default=False, db_index=True)

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from .ticket import * from .ticket import *
from .request_asset_perm import *

View File

@@ -1,137 +0,0 @@
from collections import namedtuple
from django.db.transaction import atomic
from django.db.models import F
from django.utils.translation import ugettext_lazy as _
from rest_framework.decorators import action
from rest_framework.response import Response
from users.models.user import User
from common.const.http import POST, GET
from common.drf.api import JMSModelViewSet
from common.permissions import IsValidUser
from common.utils.django import get_object_or_none
from common.drf.serializers import EmptySerializer
from perms.models.asset_permission import AssetPermission, Asset
from assets.models.user import SystemUser
from ..exceptions import (
ConfirmedAssetsChanged, ConfirmedSystemUserChanged,
TicketClosed, TicketActionYet, NotHaveConfirmedAssets,
NotHaveConfirmedSystemUser
)
from .. import serializers
from ..models import Ticket
from ..permissions import IsAssignee
class RequestAssetPermTicketViewSet(JMSModelViewSet):
queryset = Ticket.objects.filter(type=Ticket.TYPE_REQUEST_ASSET_PERM)
serializer_classes = {
'default': serializers.RequestAssetPermTicketSerializer,
'approve': EmptySerializer,
'reject': EmptySerializer,
'assignees': serializers.OrgAssigneeSerializer,
}
permission_classes = (IsValidUser,)
filter_fields = ['status', 'title', 'action', 'user_display']
search_fields = ['user_display', 'title']
def _check_can_set_action(self, instance, action):
if instance.status == instance.STATUS_CLOSED:
raise TicketClosed(detail=_('Ticket closed'))
if instance.action == action:
action_display = dict(instance.ACTION_CHOICES).get(action)
raise TicketActionYet(detail=_('Ticket has %s') % action_display)
@action(detail=False, methods=[GET], permission_classes=[IsValidUser])
def assignees(self, request, *args, **kwargs):
org_mapper = {}
UserTuple = namedtuple('UserTuple', ('id', 'name', 'username'))
user = request.user
superusers = User.objects.filter(role=User.ROLE_ADMIN)
admins_with_org = User.objects.filter(related_admin_orgs__users=user).annotate(
org_id=F('related_admin_orgs__id'), org_name=F('related_admin_orgs__name')
)
for user in admins_with_org:
org_id = user.org_id
if org_id not in org_mapper:
org_mapper[org_id] = {
'org_name': user.org_name,
'org_admins': set() # 去重
}
org_mapper[org_id]['org_admins'].add(UserTuple(user.id, user.name, user.username))
result = [
{
'org_name': _('Superuser'),
'org_admins': set(UserTuple(user.id, user.name, user.username)
for user in superusers)
}
]
for org in org_mapper.values():
result.append(org)
serializer_class = self.get_serializer_class()
serilizer = serializer_class(instance=result, many=True)
return Response(data=serilizer.data)
@action(detail=True, methods=[POST], permission_classes=[IsAssignee, IsValidUser])
def reject(self, request, *args, **kwargs):
instance = self.get_object()
action = instance.ACTION_REJECT
self._check_can_set_action(instance, action)
instance.perform_action(action, request.user)
return Response()
@action(detail=True, methods=[POST], permission_classes=[IsAssignee, IsValidUser])
def approve(self, request, *args, **kwargs):
instance = self.get_object()
action = instance.ACTION_APPROVE
self._check_can_set_action(instance, action)
meta = instance.meta
confirmed_assets = meta.get('confirmed_assets', [])
assets = list(Asset.objects.filter(id__in=confirmed_assets))
if not assets:
raise NotHaveConfirmedAssets(detail=_('Confirm assets first'))
if len(assets) != len(confirmed_assets):
raise ConfirmedAssetsChanged(detail=_('Confirmed assets changed'))
confirmed_system_user = meta.get('confirmed_system_user')
if not confirmed_system_user:
raise NotHaveConfirmedSystemUser(detail=_('Confirm system-user first'))
system_user = get_object_or_none(SystemUser, id=confirmed_system_user)
if system_user is None:
raise ConfirmedSystemUserChanged(detail=_('Confirmed system-user changed'))
self._create_asset_permission(instance, assets, system_user)
return Response({'detail': _('Succeed')})
def _create_asset_permission(self, instance: Ticket, assets, system_user):
meta = instance.meta
request = self.request
ap_kwargs = {
'name': meta.get('name', ''),
'created_by': self.request.user.username,
'comment': _('{} request assets, approved by {}').format(instance.user_display,
instance.assignee_display)
}
date_start = meta.get('date_start')
date_expired = meta.get('date_expired')
if date_start:
ap_kwargs['date_start'] = date_start
if date_expired:
ap_kwargs['date_expired'] = date_expired
with atomic():
instance.perform_action(instance.ACTION_APPROVE, request.user)
ap = AssetPermission.objects.create(**ap_kwargs)
ap.system_users.add(system_user)
ap.assets.add(*assets)
return ap

View File

@@ -1,25 +0,0 @@
from common.exceptions import JMSException
class NotHaveConfirmedAssets(JMSException):
pass
class ConfirmedAssetsChanged(JMSException):
pass
class NotHaveConfirmedSystemUser(JMSException):
pass
class ConfirmedSystemUserChanged(JMSException):
pass
class TicketClosed(JMSException):
pass
class TicketActionYet(JMSException):
pass

View File

@@ -20,11 +20,9 @@ class Ticket(CommonModelMixin):
) )
TYPE_GENERAL = 'general' TYPE_GENERAL = 'general'
TYPE_LOGIN_CONFIRM = 'login_confirm' TYPE_LOGIN_CONFIRM = 'login_confirm'
TYPE_REQUEST_ASSET_PERM = 'request_asset'
TYPE_CHOICES = ( TYPE_CHOICES = (
(TYPE_GENERAL, _("General")), (TYPE_GENERAL, _("General")),
(TYPE_LOGIN_CONFIRM, _("Login confirm")), (TYPE_LOGIN_CONFIRM, _("Login confirm"))
(TYPE_REQUEST_ASSET_PERM, _('Request asset permission'))
) )
ACTION_APPROVE = 'approve' ACTION_APPROVE = 'approve'
ACTION_REJECT = 'reject' ACTION_REJECT = 'reject'

View File

@@ -4,6 +4,3 @@
from rest_framework.permissions import BasePermission from rest_framework.permissions import BasePermission
class IsAssignee(BasePermission):
def has_object_permission(self, request, view, obj):
return obj.is_assignee(request.user)

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from .ticket import * from .ticket import *
from .request_asset_perm import *

View File

@@ -1,141 +0,0 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from django.urls import reverse
from django.db.models import Q
from users.models.user import User
from ..models import Ticket
class RequestAssetPermTicketSerializer(serializers.ModelSerializer):
ips = serializers.ListField(child=serializers.IPAddressField(), source='meta.ips',
default=list, label=_('IP group'))
hostname = serializers.CharField(max_length=256, source='meta.hostname', default=None,
allow_blank=True, label=_('Hostname'))
system_user = serializers.CharField(max_length=256, source='meta.system_user', default='',
allow_blank=True, label=_('System user'))
date_start = serializers.DateTimeField(source='meta.date_start', allow_null=True,
required=False, label=_('Date start'))
date_expired = serializers.DateTimeField(source='meta.date_expired', allow_null=True,
required=False, label=_('Date expired'))
confirmed_assets = serializers.ListField(child=serializers.UUIDField(),
source='meta.confirmed_assets',
default=list, required=False,
label=_('Confirmed assets'))
confirmed_system_user = serializers.ListField(child=serializers.UUIDField(),
source='meta.confirmed_system_user',
default=list, required=False,
label=_('Confirmed system user'))
assets_waitlist_url = serializers.SerializerMethodField()
system_user_waitlist_url = serializers.SerializerMethodField()
class Meta:
model = Ticket
mini_fields = ['id', 'title']
small_fields = [
'status', 'action', 'date_created', 'date_updated', 'system_user_waitlist_url',
'type', 'type_display', 'action_display', 'ips', 'confirmed_assets',
'date_start', 'date_expired', 'confirmed_system_user', 'hostname',
'assets_waitlist_url', 'system_user'
]
m2m_fields = [
'user', 'user_display', 'assignees', 'assignees_display',
'assignee', 'assignee_display'
]
fields = mini_fields + small_fields + m2m_fields
read_only_fields = [
'user_display', 'assignees_display', 'type', 'user', 'status',
'date_created', 'date_updated', 'action', 'id', 'assignee',
'assignee_display',
]
extra_kwargs = {
'status': {'label': _('Status')},
'action': {'label': _('Action')},
'user_display': {'label': _('User')}
}
def validate_assignees(self, assignees):
user = self.context['request'].user
count = User.objects.filter(Q(related_admin_orgs__users=user) | Q(role=User.ROLE_ADMIN)).filter(
id__in=[assignee.id for assignee in assignees]).distinct().count()
if count != len(assignees):
raise serializers.ValidationError(_('Must be organization admin or superuser'))
return assignees
def get_system_user_waitlist_url(self, instance: Ticket):
if not self._is_assignee(instance):
return None
meta = instance.meta
url = reverse('api-assets:system-user-list')
query = meta.get('system_user', '')
return '{}?search={}'.format(url, query)
def get_assets_waitlist_url(self, instance: Ticket):
if not self._is_assignee(instance):
return None
asset_api = reverse('api-assets:asset-list')
query = ''
meta = instance.meta
ips = meta.get('ips', [])
hostname = meta.get('hostname')
if ips:
query = '?ips=%s' % ','.join(ips)
elif hostname:
query = '?search=%s' % hostname
return asset_api + query
def create(self, validated_data):
validated_data['type'] = self.Meta.model.TYPE_REQUEST_ASSET_PERM
validated_data['user'] = self.context['request'].user
self._pop_confirmed_fields()
return super().create(validated_data)
def save(self, **kwargs):
meta = self.validated_data.get('meta', {})
date_start = meta.get('date_start')
if date_start:
meta['date_start'] = date_start.strftime('%Y-%m-%d %H:%M:%S%z')
date_expired = meta.get('date_expired')
if date_expired:
meta['date_expired'] = date_expired.strftime('%Y-%m-%d %H:%M:%S%z')
return super().save(**kwargs)
def update(self, instance, validated_data):
new_meta = validated_data['meta']
if not self._is_assignee(instance):
self._pop_confirmed_fields()
old_meta = instance.meta
meta = {}
meta.update(old_meta)
meta.update(new_meta)
validated_data['meta'] = meta
return super().update(instance, validated_data)
def _pop_confirmed_fields(self):
meta = self.validated_data['meta']
meta.pop('confirmed_assets', None)
meta.pop('confirmed_system_user', None)
def _is_assignee(self, obj: Ticket):
user = self.context['request'].user
return obj.is_assignee(user)
class AssigneeSerializer(serializers.Serializer):
id = serializers.UUIDField()
name = serializers.CharField()
username = serializers.CharField()
class OrgAssigneeSerializer(serializers.Serializer):
org_name = serializers.CharField()
org_admins = AssigneeSerializer(many=True)

View File

@@ -7,7 +7,6 @@ from .. import api
app_name = 'tickets' app_name = 'tickets'
router = BulkRouter() router = BulkRouter()
# router.register('tickets/request-asset-perm', api.RequestAssetPermTicketViewSet, 'ticket-request-asset-perm')
router.register('tickets', api.TicketViewSet, 'ticket') router.register('tickets', api.TicketViewSet, 'ticket')
router.register('tickets/(?P<ticket_id>[0-9a-zA-Z\-]{36})/comments', api.TicketCommentViewSet, 'ticket-comment') router.register('tickets/(?P<ticket_id>[0-9a-zA-Z\-]{36})/comments', api.TicketCommentViewSet, 'ticket-comment')

View File

@@ -3,7 +3,6 @@ import uuid
from rest_framework import generics from rest_framework import generics
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from django.conf import settings
from common.permissions import ( from common.permissions import (
IsCurrentUserOrReadOnly IsCurrentUserOrReadOnly
@@ -65,9 +64,8 @@ class UserProfileApi(generics.RetrieveUpdateAPIView):
return self.request.user return self.request.user
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
if not settings.SESSION_EXPIRE_AT_BROWSER_CLOSE: age = request.session.get_expiry_age()
age = request.session.get_expiry_age() request.session.set_expiry(age)
request.session.set_expiry(age)
return super().retrieve(request, *args, **kwargs) return super().retrieve(request, *args, **kwargs)

View File

@@ -18,7 +18,6 @@ from ..serializers import UserSerializer, UserRetrieveSerializer
from .mixins import UserQuerysetMixin from .mixins import UserQuerysetMixin
from ..models import User from ..models import User
from ..signals import post_user_create from ..signals import post_user_create
from ..filters import OrgRoleUserFilterBackend
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -36,7 +35,6 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet):
'default': UserSerializer, 'default': UserSerializer,
'retrieve': UserRetrieveSerializer 'retrieve': UserRetrieveSerializer
} }
extra_filter_backends = [OrgRoleUserFilterBackend]
def get_queryset(self): def get_queryset(self):
return super().get_queryset().prefetch_related('groups') return super().get_queryset().prefetch_related('groups')

View File

@@ -1,32 +0,0 @@
from rest_framework.compat import coreapi, coreschema
from rest_framework import filters
from users.models.user import User
from orgs.utils import current_org
class OrgRoleUserFilterBackend(filters.BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
org_role = request.query_params.get('org_role')
if not org_role:
return queryset
if org_role == 'admins':
return queryset & (current_org.get_org_admins() | User.objects.filter(role=User.ROLE_ADMIN))
elif org_role == 'auditors':
return queryset & current_org.get_org_auditors()
elif org_role == 'users':
return queryset & current_org.get_org_users()
elif org_role == 'members':
return queryset & current_org.get_org_members()
def get_schema_fields(self, view):
return [
coreapi.Field(
name='org_role', location='query', required=False, type='string',
schema=coreschema.String(
title='Organization role users',
description='Organization role users can be {admins|auditors|users|members}'
)
)
]

View File

@@ -42,7 +42,14 @@ class UserGroupSerializer(BulkOrgResourceModelSerializer):
def set_fields_queryset(self): def set_fields_queryset(self):
users_field = self.fields.get('users') users_field = self.fields.get('users')
if users_field: if users_field:
users_field.child_relation.queryset = utils.get_current_org_members() users_field.child_relation.queryset = utils.get_current_org_members(exclude=('Auditor',))
def validate_users(self, users):
for user in users:
if user.is_super_auditor:
msg = _('Auditors cannot be join in the user group')
raise serializers.ValidationError(msg)
return users
@classmethod @classmethod
def setup_eager_loading(cls, queryset): def setup_eager_loading(cls, queryset):

View File

@@ -87,11 +87,7 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer):
if not role: if not role:
return return
choices = role._choices choices = role._choices
choices.pop(User.ROLE_APP, None) choices.pop('App', None)
request = self.context.get('request')
if request and hasattr(request, 'user') and not request.user.is_superuser:
choices.pop(User.ROLE_ADMIN, None)
choices.pop(User.ROLE_AUDITOR, None)
role._choices = choices role._choices = choices
def validate_role(self, value): def validate_role(self, value):
@@ -324,9 +320,3 @@ class UserUpdatePublicKeySerializer(serializers.ModelSerializer):
new_public_key = self.validated_data.get('public_key') new_public_key = self.validated_data.get('public_key')
instance.set_public_key(new_public_key) instance.set_public_key(new_public_key)
return instance return instance
class MiniUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'name', 'username']

2
jms
View File

@@ -217,7 +217,7 @@ def get_start_celery_ansible_kwargs():
def get_start_celery_default_kwargs(): def get_start_celery_default_kwargs():
print("\n- Start Celery as Distributed Task Queue: Celery") print("\n- Start Celery as Distributed Task Queue: Celery")
return get_start_worker_kwargs('celery', 4) return get_start_worker_kwargs('celery', 2)
def get_start_worker_kwargs(queue, num): def get_start_worker_kwargs(queue, num):

View File

@@ -61,7 +61,7 @@ pytz==2018.3
PyYAML==5.1 PyYAML==5.1
redis==3.2.0 redis==3.2.0
requests==2.22.0 requests==2.22.0
jms-storage==0.0.31 jms-storage==0.0.29
s3transfer==0.3.3 s3transfer==0.3.3
simplejson==3.13.2 simplejson==3.13.2
six==1.11.0 six==1.11.0

View File

@@ -5,19 +5,15 @@ utils_dir=$(pwd)
project_dir=$(dirname "$utils_dir") project_dir=$(dirname "$utils_dir")
release_dir=${project_dir}/release release_dir=${project_dir}/release
# 安装依赖包
command -v git || yum -y install git
# 打包 # 打包
cd "${project_dir}" || exit 3 cd "${project_dir}" || exit 3
rm -rf "${release_dir:?}"/* rm -rf "${release_dir:?}/*"
to_dir="${release_dir}/jumpserver" to_dir="${release_dir}/jumpserver"
mkdir -p "${to_dir}" mkdir -p "${to_dir}"
git archive --format tar HEAD | tar x -C "${to_dir}"
if [[ -d '.git' ]];then
command -v git || yum -y install git
git archive --format tar HEAD | tar x -C "${to_dir}"
else
cp -R . /tmp/jumpserver
mv /tmp/jumpserver/* "${to_dir}"
fi
if [[ $(uname) == 'Darwin' ]];then if [[ $(uname) == 'Darwin' ]];then
alias sedi="sed -i ''" alias sedi="sed -i ''"