Compare commits

..

63 Commits

Author SHA1 Message Date
BaiJiangJie
070af8c491 fix(radius): 修复radius认证失败问题 (#4342) (#4343)
* fix(radius): 修复radius认证失败问题,添加get_django_user方法参数(django-radius==1.4.0 中添加了额外参数)

* fix(radius): 修复radius认证失败问题,重写authenticate方法(django-radius 不接受public_key参数)
2020-07-16 18:08:44 +08:00
BaiJiangJie
08fdc57543 Merge pull request #4338 from jumpserver/dev
merge(master): Merge from dev to master
2020-07-16 10:50:26 +08:00
Bai
bb60d2a1d9 fix(users): 组织管理员创建用户时,角色只能选择: 用户 2020-07-15 20:14:14 +08:00
xinwen
0014bd0cb9 fix(audits): 操作日志中的动作搜索条件,删除文件字段改成删除 (#4334) 2020-07-15 20:13:21 +08:00
xinwen
9488c8bd97 fix(cmd_filter): 命令过滤器唯一应该为 name + org_id (#4325) 2020-07-15 20:04:46 +08:00
Bai
1f30d459ae fix(command): 修复命令记录没有根据sesion进行过滤的问题 2020-07-15 17:30:55 +08:00
Bai
4e933fc1ca feat(session + db): 会话搜索添加登录来源选项 2020-07-15 17:25:43 +08:00
Bai
c0f3a1f64a fix(all): 修复创建资源时,created_by字段长度限制导致创建失败的问题 2020-07-15 16:27:40 +08:00
Bai
0f70f5eccf fix(orgs): 删除组织失败时返回对应错误信息 2020-07-15 16:25:34 +08:00
Bai
eef942c155 fix(gather_asset_users): 修复收集资产用户日志中用户名显示不完整的问题 2020-07-15 16:16:11 +08:00
xinwen
061592fa6b fix(terminal): 移除CommandQueryMixin.get_filter_fields 2020-07-14 19:35:18 +08:00
Bai
c7a02586c1 chore(jms): 修改celery队列数量: 2 -> 4 2020-07-14 19:34:17 +08:00
Bai
ddcd4ebbfc fix(asset_user): 修改创建AuthBook对象锁机制,使用select_for_update替换redis_lock3 2020-07-14 19:06:06 +08:00
Bai
9550ea62fb fix(asset_user): 修改创建AuthBook对象锁机制,使用select_for_update替换redis_lock2 2020-07-14 19:06:06 +08:00
Bai
abcb589658 fix(asset_user): 修改创建AuthBook对象锁机制,使用select_for_update替换redis_lock 2020-07-14 19:06:06 +08:00
Bai
1bb366ad94 fix(authbook): 修改创建AuthBook对象锁机制,解决并发操作堵塞问题 2020-07-14 19:06:06 +08:00
xinwen
a5df7738f6 fix(audits): 日志审计模块 Serializer 添加 org_id 字段 2020-07-14 18:05:21 +08:00
xinwen
da858c8998 fix(tickets): 隐藏申请资产工单URL (#4307) 2020-07-13 17:49:00 +08:00
BaiJiangJie
724a8f6324 fix(assets): 修复用户name字段长度与资产created_by字段长度不一致导致创建资产失败的问题 (#4302)
* fix(assets): 修复用户name字段长度与资产created_by字段长度不一致导致创建资产失败的问题

* fix(assets): 修复用户name字段长度与资产created_by字段长度不一致导致创建资产失败的问题(修改迁移文件名称 0050_auto_20200702_1602.py -> 0051_auto_20200713_1143.py)
2020-07-13 12:00:44 +08:00
ibuler
437df9a533 fix(assets): node asset 关系发生变化是,关联系统用户引起的问题 2020-07-10 17:31:51 +08:00
ibuler
f2c70d0bba ci(fix): 修改构建脚本 2020-07-09 17:44:54 +08:00
ibuler
ea913a5b6e ci(build): 修改构建逻辑 2020-07-09 17:44:54 +08:00
ibuler
c0cd8878dc ci(fix): 修改构建脚本 2020-07-09 17:41:27 +08:00
ibuler
15e995ade6 ci(build): 修改构建逻辑 2020-07-09 17:41:27 +08:00
BaiJiangJie
cadf42f3fa Merge pull request #4280 from jumpserver/dev
merge: Merge to master from branch dev
2020-07-09 15:02:47 +08:00
BaiJiangJie
f588093cd3 Merge pull request #4282 from jumpserver/dev_master
merge: Merge to master from branch dev
2020-07-09 14:51:03 +08:00
Bai
7c12f8f462 merge: Merge to dev from branch master 2020-07-09 14:27:08 +08:00
xinwen
6f5a92c21f [Update] assets/gathered_user 添加过滤字段 2020-07-09 14:08:46 +08:00
xinwen
17a76994dc [Update] 系统用户添加过滤字段 2020-07-09 14:08:46 +08:00
jym503558564
39d793bc47 fix:修改 ftp 日志按开始日期排序 2020-07-09 14:08:46 +08:00
xinwen
c3eafbee8c [Fix] X-Pack/云管中心 i18n 2020-07-09 14:08:46 +08:00
ibuler
10f99be100 添加example api 2020-07-09 14:08:46 +08:00
xinwen
8eb6cfa9c9 fix(ticket): 修改工单获取系统用户的字段 (#4274)
fix(ticket): 申请资产工单修改bug
2020-07-09 14:06:06 +08:00
xinwen
f430c9e435 Merge pull request #4270 from jumpserver/request-asset-ticket-dev
feat(ticket): 添加申请资产工单
2020-07-08 15:42:04 +08:00
BaiJiangJie
10c428a432 Merge pull request #4269 from jumpserver/dev_user_group
fix(user_group): 用户组中添加用户,取消审计员的限制
2020-07-08 15:23:45 +08:00
Bai
a30c603bdc fix(user_group): 用户组中添加用户,取消审计员的限制 2020-07-08 15:17:07 +08:00
xinwen
39a75074af [Feature] 添加申请资产工单 2020-07-08 15:09:44 +08:00
BaiJiangJie
452ed2baf1 Merge pull request #4268 from jumpserver/dev_adminuser
fix(assets): 修复测试管理用户/系统用户资产可连接性问题
2020-07-08 14:47:09 +08:00
Bai
8c7240193a fix(system_user): 修复系统用户测试可连接性失败问题(所有资产)(不应该执行校验系统用户是否可以推送的逻辑) 2020-07-08 11:30:37 +08:00
Bai
b622aca9af fix(admin_user): 修复管理用户单独测试某台资产可连接性失败的情况(private_key_file) 2020-07-08 10:48:25 +08:00
BaiJiangJie
ebf1a9d5e2 Merge pull request #4255 from jumpserver/dev_asset
feat(assets): 资产序列类修改字段名 _name 为 _display
2020-07-07 14:36:27 +08:00
Bai
69f49f7776 feat(i18n): 修改翻译 2020-07-07 14:13:20 +08:00
Bai
fcd684e2db feat(assets): 资产序列类修改字段名 _name 为 _display 2020-07-07 11:09:43 +08:00
BaiJiangJie
afcb6bd77c Merge pull request #4242 from jumpserver/dev_cloud
feat(node + domain + domain_migrate): NodeModel添加get_or_create_child()方法,修改网域唯一字段 org_id+name
2020-07-06 15:18:13 +08:00
Bai
1c264399bb feat(domain + migrate): 修改网域唯一字段为:org_id + name 2020-07-02 18:51:16 +08:00
Bai
872e2546e9 feat(node): NodeModel添加方法get_or_create_child() 2020-07-02 18:49:17 +08:00
BaiJiangJie
8f347eee4d Merge pull request #4216 from jumpserver/dev_org
feat(Cloud): 组织管理ViewSet添加搜索字段
2020-07-01 14:57:50 +08:00
BaiJiangJie
fa886b90c2 Merge pull request #4211 from jumpserver/dev_command_execute
feat(Command execute): 批量命令执行配置添加默认值True
2020-07-01 14:57:37 +08:00
Bai
caf312c5be feat(Cloud): 组织管理ViewSet添加搜索字段 2020-07-01 14:38:03 +08:00
Bai
ac6168a06c feat(Command execute): 批量命令执行配置添加默认值True 2020-07-01 11:19:57 +08:00
BaiJiangJie
eba9f2325a Merge pull request #4204 from jumpserver/dev_login_password_encrypt
feat(Login password ecrypt): 登录密码加密传输
2020-06-30 18:57:51 +08:00
Bai
b46e772d09 feat(login password ecrypt): 登录密码加密传输 4 2020-06-30 18:35:01 +08:00
Bai
183df82a75 feat(login password ecrypt): 登录密码加密传输 3 2020-06-30 18:14:53 +08:00
Bai
98c91d0f18 feat(login password ecrypt): 登录密码加密传输(添加翻译) 2020-06-30 17:37:16 +08:00
Bai
e17d875206 feat(login password ecrypt): 登录密码加密传输2 2020-06-30 17:23:56 +08:00
Bai
4b1e84ed8a Merge branch 'dev' into dev_login_password_encrypt 2020-06-30 17:13:08 +08:00
Bai
71ee33e3be feat(login password ecrypt): 登录密码加密传输 2020-06-30 17:12:38 +08:00
xinwen
5dd24f5cf9 Merge pull request #4188 from jumpserver/limit-upload-csv
[Update] 限制上传CSV文件的大小
2020-06-28 19:13:53 +08:00
xinwen
2b6e818943 [Update] 限制上传CSV文件的大小 2020-06-28 19:02:20 +08:00
BaiJiangJie
fdcda83c93 Merge pull request #4142 from jumpserver/sftp-log-i18n
Sftp log i18n
2020-06-24 17:30:57 +08:00
xinwen
6e3369c944 [Update] sftp log页面操作翻译 2020-06-24 17:22:53 +08:00
BaiJiangJie
d7e432a851 Merge pull request #4139 from jumpserver/dev_session
[Update] UserProfileAPI 判断是否设置session过期时间,解决前端关闭浏览器session未失效的问题
2020-06-24 16:14:36 +08:00
Bai
c0a153d13a [Update] UserProfileAPI 判断是否设置session过期时间,解决前端关闭浏览器session未失效的问题 2020-06-24 10:52:05 +08:00
60 changed files with 1276 additions and 388 deletions

View File

@@ -1,19 +1,28 @@
FROM registry.fit2cloud.com/public/python:v3 FROM registry.fit2cloud.com/public/python:v3 as stage-build
MAINTAINER Jumpserver Team <ibuler@qq.com> MAINTAINER Jumpserver Team <ibuler@qq.com>
ARG VERSION
ENV VERSION=$VERSION
WORKDIR /opt/jumpserver WORKDIR /opt/jumpserver
RUN useradd jumpserver ADD . .
RUN cd utils && bash -ixeu build.sh
COPY ./requirements /tmp/requirements
FROM registry.fit2cloud.com/public/python:v3
WORKDIR /opt/jumpserver
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
RUN useradd jumpserver
RUN yum -y install epel-release && \ 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 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 && \ COPY . .
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt || pip install -r requirements.txt RUN yum -y install $(cat requirements/rpm_requirements.txt)
RUN pip install --upgrade pip setuptools && pip install wheel && \
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements/requirements.txt || pip install -r requirements/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 from ..filters import AssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend
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" "is_active", 'ip'
) )
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] extra_filter_backends = [AssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend]
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 -*-
# #
import coreapi from rest_framework.compat import coreapi, coreschema
from rest_framework import filters from rest_framework import filters
from django.db.models import Q from django.db.models import Q
@@ -117,3 +117,23 @@ 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

@@ -0,0 +1,18 @@
# 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

@@ -0,0 +1,22 @@
# 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

@@ -0,0 +1,22 @@
# 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=32, null=True, blank=True, verbose_name=_('Created by')) created_by = models.CharField(max_length=128, 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,10 +244,6 @@ 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,7 +3,6 @@
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
@@ -59,19 +58,17 @@ class AuthBook(BaseUser):
""" """
username = kwargs['username'] username = kwargs['username']
asset = kwargs['asset'] asset = kwargs['asset']
key_lock = 'KEY_LOCK_CREATE_AUTH_BOOK_{}_{}'.format(username, asset.id) with transaction.atomic():
with cache.lock(key_lock): # 使用select_for_update限制并发创建相同的username、asset条目
with transaction.atomic(): instances = cls.objects.select_for_update().filter(username=username, asset=asset)
cls.objects.filter( instances.filter(is_latest=True).update(is_latest=False)
username=username, asset=asset, is_latest=True max_version = cls.get_max_version(username, asset)
).update(is_latest=False) kwargs.update({
max_version = cls.get_max_version(username, asset) 'version': max_version + 1,
kwargs.update({ 'is_latest': True
'version': max_version + 1, })
'is_latest': True obj = cls.objects.create(**kwargs)
}) 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, unique=True, verbose_name=_("Name")) name = models.CharField(max_length=64, 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,6 +29,7 @@ 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,13 +17,14 @@ __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, unique=True, verbose_name=_('Name')) name = models.CharField(max_length=128, 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,6 +199,20 @@ 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,6 +67,9 @@ 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')
""" """
资产的数据结构 资产的数据结构
""" """
@@ -82,7 +85,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', 'platform' 'admin_user', 'admin_user_display', 'domain', 'domain_display', 'platform'
] ]
fk_only_fields = { fk_only_fields = {
'platform': ['name'] 'platform': ['name']

View File

@@ -185,7 +185,9 @@ 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:
system_users_assets[system_user].update(set(assets)) assets_has_set = system_user.assets.all().filter(id__in=assets).values_list('id', flat=True)
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 private_key=asset_user.private_key_file
) )
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 -F $i -1 | " "grep -v 'shudown' | awk -F: '{ print $1 }');for i in $users;do last -w -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,7 +31,10 @@ 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

@@ -0,0 +1,18 @@
# 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,12 +14,30 @@ __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")) operate = models.CharField(max_length=16, verbose_name=_("Operate"), choices=OPERATE_CHOICES)
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,12 +12,13 @@ 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', 'id', 'user', 'remote_addr', 'asset', 'system_user', 'org_id',
'operate', 'filename', 'is_success', 'date_start' 'operate', 'filename', 'is_success', 'date_start', 'operate_display'
) )
@@ -39,7 +40,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' 'remote_addr', 'datetime', 'org_id'
) )
@@ -65,7 +66,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' 'date_start', 'result', 'is_success', 'org_id'
] ]
fields = fields_small + ['hosts', 'run_as_display', 'user_display'] fields = fields_small + ['hosts', 'run_as_display', 'user_display']
extra_kwargs = { extra_kwargs = {

View File

@@ -11,7 +11,7 @@ User = get_user_model()
class CreateUserMixin: class CreateUserMixin:
def get_django_user(self, username, password=None): def get_django_user(self, username, password=None, *args, **kwargs):
if isinstance(username, bytes): if isinstance(username, bytes):
username = username.decode() username = username.decode()
try: try:
@@ -27,6 +27,12 @@ class CreateUserMixin:
user.save() user.save()
return user return user
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,6 +10,7 @@ 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'
@@ -19,6 +20,7 @@ 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=128, strip=False max_length=1024, 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 class="m-t" role="form" method="post" action=""> <form id="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" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required=""> <input type="password" class="form-control" id="password" 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">{% trans 'Login' %}</button> <button type="submit" class="btn btn-primary block full-width m-b" onclick="doLogin();return false;">{% 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,4 +64,20 @@
{% 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" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required=""> <input type="password" class="form-control" id="password" 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">{% trans 'Login' %}</button> <button type="submit" class="btn btn-transparent" onclick="doLogin();return false;">{% 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,4 +127,21 @@
</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 +1,15 @@
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,9 +1,47 @@
# -*- 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)
@@ -11,6 +49,16 @@ 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 from .. import forms, mixins, errors, utils
__all__ = [ __all__ = [
@@ -108,9 +108,13 @@ 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

View File

@@ -0,0 +1,28 @@
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
)

11
apps/common/drf/api.py Normal file
View File

@@ -0,0 +1,11 @@
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,18 +6,27 @@ 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 from rest_framework.exceptions import ParseError, APIException
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'
@@ -46,23 +55,31 @@ class JMSCSVParser(BaseParser):
return fields_map return fields_map
@staticmethod @staticmethod
def _process_row(row): def _replace_chinese_quot(str_):
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.find("[") != -1 and col.find("]") != -1: if isinstance(col, str) and col.startswith('[') and col.endswith(']'):
# 替换中文格式引号 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.find("{") != -1 and col.find("}") != -1: if isinstance(col, str) and col.startswith("{") and col.endswith("}"):
# 替换中文格式引号 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
@@ -82,11 +99,19 @@ 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:
serializer = parser_context["view"].get_serializer() view = parser_context['view']
meta = view.request.META
serializer = view.get_serializer()
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)

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ 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
@@ -15,8 +16,8 @@ from common.drf.filters import IDSpmFilter, CustomFilter, IDInFilter
from ..utils import lazyproperty from ..utils import lazyproperty
__all__ = [ __all__ = [
"JSONResponseMixin", "CommonApiMixin", 'JSONResponseMixin', 'CommonApiMixin', 'AsyncApiMixin', 'RelationMixin',
'AsyncApiMixin', 'RelationMixin' 'SerializerMixin2', 'QuerySetMixin', 'ExtraFilterFieldsMixin'
] ]
@@ -54,9 +55,10 @@ 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(self.filter_backends) + \ backends = list(chain(
list(self.default_added_filters) + \ self.filter_backends,
list(self.extra_filter_backends) self.default_added_filters,
self.extra_filter_backends))
return backends return backends
def filter_queryset(self, queryset): def filter_queryset(self, queryset):
@@ -233,3 +235,32 @@ 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,6 +226,7 @@ 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,

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@
# #
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
@@ -22,6 +23,8 @@ 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,)
@@ -51,10 +54,12 @@ 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:
return Response(status=status.HTTP_400_BAD_REQUEST) msg = _('Organization contains undeleted resources')
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):
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) msg = _('The current organization cannot be deleted')
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

@@ -43,4 +43,7 @@ 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:
instance.created_by = current_request.user.name user_name = 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,21 +55,17 @@ 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, input=q.get("input"), date_from=date_from, date_to=date_to,
user=q.get("user"), asset=q.get("asset"), user=q.get("user"), asset=q.get("asset"), system_user=q.get("system_user"),
system_user=q.get("system_user"), input=q.get("input"), session=q.get("session_id"),
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", "protocol", "terminal", "is_finished", 'login_from',
] ]
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): input=None, session=None, risk_level=None, org_id=None):
pass pass
@abc.abstractmethod @abc.abstractmethod

View File

@@ -0,0 +1,18 @@
# 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") login_from = models.CharField(max_length=2, choices=LOGIN_FROM_CHOICES, default="ST", verbose_name=_("Login from"))
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,3 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from .ticket import * from .ticket import *
from .request_asset_perm import *

View File

@@ -0,0 +1,137 @@
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

@@ -0,0 +1,25 @@
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,9 +20,11 @@ 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,3 +4,6 @@
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,3 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from .ticket import * from .ticket import *
from .request_asset_perm import *

View File

@@ -0,0 +1,141 @@
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,6 +7,7 @@ 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,6 +3,7 @@ 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
@@ -64,8 +65,9 @@ class UserProfileApi(generics.RetrieveUpdateAPIView):
return self.request.user return self.request.user
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
age = request.session.get_expiry_age() if not settings.SESSION_EXPIRE_AT_BROWSER_CLOSE:
request.session.set_expiry(age) age = request.session.get_expiry_age()
request.session.set_expiry(age)
return super().retrieve(request, *args, **kwargs) return super().retrieve(request, *args, **kwargs)

View File

@@ -18,6 +18,7 @@ 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__)
@@ -35,6 +36,7 @@ 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')

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

@@ -0,0 +1,32 @@
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,14 +42,7 @@ 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(exclude=('Auditor',)) users_field.child_relation.queryset = utils.get_current_org_members()
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,7 +87,11 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer):
if not role: if not role:
return return
choices = role._choices choices = role._choices
choices.pop('App', None) choices.pop(User.ROLE_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):
@@ -320,3 +324,9 @@ 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', 2) return get_start_worker_kwargs('celery', 4)
def get_start_worker_kwargs(queue, num): def get_start_worker_kwargs(queue, num):

View File

@@ -5,15 +5,19 @@ 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 ''"