Compare commits

...

71 Commits

Author SHA1 Message Date
ibuler
526babb724 fix(auth): 修复radius decode error的问题 2020-08-03 10:33:02 +08:00
ibuler
a3c1d1e730 ci: 修改docker构建的问题 2020-07-29 19:49:51 +08:00
ibuler
8905613e50 ci(dockerfile): 修改dockerfile构建 2020-07-28 19:28:49 +08:00
Bai
d6d85dff40 fix(perms): 修复perms api UserPermissionMixin 中 kwargs 参数传递(未发现引出其他问题) 2020-07-23 11:04:11 +08:00
github-actions
28b92a0d80 fix(cas): 修复cas校验不同的问题 2020-07-22 17:22:54 +08:00
ibuler
e4d2b5e98d fix(es): 修复es7数据结构引起的命令无法查询的问题
- 更新了 jms-storage版本依赖
2020-07-22 17:14:59 +08:00
github-actions
1e1cd2e1aa ci(github): 添加通用action 2020-07-21 14:33:08 +08:00
Bai
5065490a25 fix(authentication): 修复用户认证Radius认证中参数传递错误问题 *kwargs -> **kwargs 2020-07-21 10:31:57 +08:00
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
68 changed files with 1340 additions and 396 deletions

View File

@@ -0,0 +1,12 @@
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,19 +1,33 @@
FROM registry.fit2cloud.com/public/python:v3
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
ARG PIP_MIRROR=https://pypi.douban.com/simple
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
COPY ./requirements ./requirements
RUN useradd 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
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
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
VOLUME /opt/jumpserver/data
VOLUME /opt/jumpserver/logs

View File

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

View File

@@ -18,5 +18,5 @@ class GatheredUserViewSet(OrgModelViewSet):
permission_classes = [IsOrgAdmin]
extra_filter_backends = [AssetRelatedByNodeFilterBackend]
filter_fields = ['asset', 'username', 'present', 'asset__ip', 'asset__hostname']
filter_fields = ['asset', 'username', 'present', 'asset__ip', 'asset__hostname', 'asset_id']
search_fields = ['username', 'asset__ip', 'asset__hostname']

View File

@@ -28,7 +28,7 @@ class SystemUserViewSet(OrgBulkModelViewSet):
System user api set, for add,delete,update,list,retrieve resource
"""
model = SystemUser
filter_fields = ("name", "username")
filter_fields = ("name", "username", "protocol")
search_fields = filter_fields
serializer_class = serializers.SystemUserSerializer
serializer_classes = {

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
import coreapi
from rest_framework.compat import coreapi, coreschema
from rest_framework import filters
from django.db.models import Q
@@ -117,3 +117,23 @@ class AssetRelatedByNodeFilterBackend(AssetByNodeFilterBackend):
def perform_query(pattern, queryset):
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'))
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'))
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):
return self.platform.base
@lazyproperty
def admin_user_display(self):
return self.admin_user.name
@lazyproperty
def admin_user_username(self):
"""求可连接性时直接用用户名去取避免再查一次admin user

View File

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

View File

@@ -18,7 +18,7 @@ __all__ = [
class CommandFilter(OrgModelMixin):
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'))
comment = models.TextField(blank=True, default='', verbose_name=_("Comment"))
date_created = models.DateTimeField(auto_now_add=True)
@@ -29,6 +29,7 @@ class CommandFilter(OrgModelMixin):
return self.name
class Meta:
unique_together = [('org_id', 'name')]
verbose_name = _("Command filter")

View File

@@ -17,13 +17,14 @@ __all__ = ['Domain', 'Gateway']
class Domain(OrgModelMixin):
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'))
date_created = models.DateTimeField(auto_now_add=True, null=True,
verbose_name=_('Date created'))
class Meta:
verbose_name = _("Domain")
unique_together = [('org_id', 'name')]
def __str__(self):
return self.name

View File

@@ -199,6 +199,20 @@ class FamilyMixin:
)
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):
mark = self.child_mark
self.child_mark += 1

View File

@@ -67,6 +67,9 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
slug_field='name', queryset=Platform.objects.all(), label=_("Platform")
)
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',
]
fields_fk = [
'admin_user', 'admin_user_display', 'domain', 'platform'
'admin_user', 'admin_user_display', 'domain', 'domain_display', 'platform'
]
fk_only_fields = {
'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)
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():
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(
task_name=task_name, asset=asset_user.asset,
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:
logger.warn("Failed run adhoc {}, {}".format(task_name, e))

View File

@@ -64,7 +64,7 @@ GATHER_ASSET_USERS_TASKS = [
"action": {
"module": "shell",
"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"
}
}

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
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:
return {}
platform_hosts_map = {}

View File

@@ -27,6 +27,7 @@ class FTPLogViewSet(CreateModelMixin,
]
filter_fields = ['user', 'asset', 'system_user', 'filename']
search_fields = filter_fields
ordering = ['-date_start']
class UserLoginLogViewSet(ListModelMixin, CommonGenericViewSet):

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):
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)
user = models.CharField(max_length=128, verbose_name=_('User'))
remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True)
asset = models.CharField(max_length=1024, verbose_name=_("Asset"))
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"))
is_success = models.BooleanField(default=True, verbose_name=_("Success"))
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):
operate_display = serializers.ReadOnlyField(source='get_operate_display')
class Meta:
model = models.FTPLog
fields = (
'id', 'user', 'remote_addr', 'asset', 'system_user',
'operate', 'filename', 'is_success', 'date_start'
'id', 'user', 'remote_addr', 'asset', 'system_user', 'org_id',
'operate', 'filename', 'is_success', 'date_start', 'operate_display'
)
@@ -39,7 +40,7 @@ class OperateLogSerializer(serializers.ModelSerializer):
model = models.OperateLog
fields = (
'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_small = fields_mini + [
'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']
extra_kwargs = {

View File

@@ -1,17 +1,17 @@
# -*- coding: utf-8 -*-
#
import traceback
from django.contrib.auth import get_user_model
from radiusauth.backends import RADIUSBackend, RADIUSRealmBackend
from django.conf import settings
from pyrad.packet import AccessRequest
User = get_user_model()
class CreateUserMixin:
def get_django_user(self, username, password=None):
def get_django_user(self, username, password=None, *args, **kwargs):
if isinstance(username, bytes):
username = username.decode()
try:
@@ -27,6 +27,23 @@ class CreateUserMixin:
user.save()
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):
pass

View File

@@ -10,6 +10,7 @@ from users.utils import (
)
reason_password_failed = 'password_failed'
reason_password_decrypt_failed = 'password_decrypt_failed'
reason_mfa_failed = 'mfa_failed'
reason_mfa_unset = 'mfa_unset'
reason_user_not_exist = 'user_not_exist'
@@ -19,6 +20,7 @@ reason_user_inactive = 'user_inactive'
reason_choices = {
reason_password_failed: _('Username/password check failed'),
reason_password_decrypt_failed: _('Password decrypt failed'),
reason_mfa_failed: _('MFA failed'),
reason_mfa_unset: _('MFA unset'),
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)
password = forms.CharField(
label=_('Password'), widget=forms.PasswordInput,
max_length=128, strip=False
max_length=1024, strip=False
)
def confirm_login_allowed(self, user):

View File

@@ -7,7 +7,7 @@
{% endblock %}
{% block content %}
<form class="m-t" role="form" method="post" action="">
<form id="form" class="m-t" role="form" method="post" action="">
{% csrf_token %}
{% if form.non_field_errors %}
<div style="line-height: 17px;">
@@ -26,7 +26,7 @@
{% endif %}
</div>
<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 %}
<div class="help-block field-error">
<p class="red-fonts">{{ form.errors.password.as_text }}</p>
@@ -36,7 +36,7 @@
<div>
{{ form.captcha }}
</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 %}
<p class="text-muted font-bold" style="color: red">
@@ -64,4 +64,20 @@
{% endif %}
</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 %}

View File

@@ -98,7 +98,7 @@
{% endif %}
</div>
<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 %}
<div class="help-block field-error">
<p class="red-fonts">{{ form.errors.password.as_text }}</p>
@@ -109,7 +109,7 @@
{{ form.captcha }}
</div>
<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 style="text-align: center">
<a href="{% url 'authentication:forgot-password' %}">
@@ -127,4 +127,21 @@
</div>
</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>

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 -*-
#
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 common.utils import get_logger
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):
password = kwargs.pop('password', None)
@@ -11,6 +49,16 @@ def check_user_valid(**kwargs):
username = kwargs.pop('username', None)
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,
password=password, public_key=public_key)
if not user:

View File

@@ -22,7 +22,7 @@ from common.utils import get_request_ip, get_object_or_none
from users.utils import (
redirect_user_first_login_or_index
)
from .. import forms, mixins, errors
from .. import forms, mixins, errors, utils
__all__ = [
@@ -108,9 +108,13 @@ class UserLoginView(mixins.AuthMixin, FormView):
return self.form_class
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 = {
'demo_mode': os.environ.get("DEMO_MODE"),
'AUTH_OPENID': settings.AUTH_OPENID,
'rsa_public_key': rsa_public_key.replace('\n', '\\n')
}
kwargs.update(context)
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 unicodecsv
from django.utils.translation import ugettext as _
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
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):
"""
Parses CSV file to serializer data
"""
CSV_UPLOAD_MAX_SIZE = 1024 * 1024 * 10
media_type = 'text/csv'
@@ -46,23 +55,31 @@ class JMSCSVParser(BaseParser):
return fields_map
@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数据前的行处理
"""
_row = []
for col in row:
# 列表转换
if isinstance(col, str) and col.find("[") != -1 and col.find("]") != -1:
# 替换中文格式引号
col = col.replace("", '"').replace("", '"').\
replace("", '"').replace('', '"').replace("'", '"')
if isinstance(col, str) and col.startswith('[') and col.endswith(']'):
col = cls._replace_chinese_quot(col)
col = json.loads(col)
# 字典转换
if isinstance(col, str) and col.find("{") != -1 and col.find("}") != -1:
# 替换中文格式引号
col = col.replace("", '"').replace("", '"'). \
replace("", '"').replace('', '"').replace("'", '"')
if isinstance(col, str) and col.startswith("{") and col.endswith("}"):
col = cls._replace_chinese_quot(col)
col = json.loads(col)
_row.append(col)
return _row
@@ -82,11 +99,19 @@ class JMSCSVParser(BaseParser):
def parse(self, stream, media_type=None, parser_context=None):
parser_context = parser_context or {}
try:
serializer = parser_context["view"].get_serializer()
view = parser_context['view']
meta = view.request.META
serializer = view.get_serializer()
except Exception as e:
logger.debug(e, exc_info=True)
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:
stream_data = stream.read()
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 -*-
#
from rest_framework.exceptions import APIException
class JMSException(APIException):
pass

View File

@@ -4,6 +4,7 @@ import time
from hashlib import md5
from threading import Thread
from collections import defaultdict
from itertools import chain
from django.db.models.signals import m2m_changed
from django.core.cache import cache
@@ -15,8 +16,8 @@ from common.drf.filters import IDSpmFilter, CustomFilter, IDInFilter
from ..utils import lazyproperty
__all__ = [
"JSONResponseMixin", "CommonApiMixin",
'AsyncApiMixin', 'RelationMixin'
'JSONResponseMixin', 'CommonApiMixin', 'AsyncApiMixin', 'RelationMixin',
'SerializerMixin2', 'QuerySetMixin', 'ExtraFilterFieldsMixin'
]
@@ -54,9 +55,10 @@ class ExtraFilterFieldsMixin:
def get_filter_backends(self):
if self.filter_backends != self.__class__.filter_backends:
return self.filter_backends
backends = list(self.filter_backends) + \
list(self.default_added_filters) + \
list(self.extra_filter_backends)
backends = list(chain(
self.filter_backends,
self.default_added_filters,
self.extra_filter_backends))
return backends
def filter_queryset(self, queryset):
@@ -233,3 +235,32 @@ class RelationMixin:
def perform_create(self, serializer):
instance = serializer.save()
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': {},
'SECURITY_MFA_AUTH': False,
'SECURITY_COMMAND_EXECUTION': True,
'SECURITY_SERVICE_ACCOUNT_REGISTRATION': True,
'SECURITY_VIEW_AUTH_NEED_MFA': True,
'SECURITY_LOGIN_LIMIT_COUNT': 7,

View File

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

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.utils.translation import ugettext as _
from rest_framework import status, generics
from rest_framework.views import Response
from rest_framework_bulk import BulkModelViewSet
@@ -22,6 +23,8 @@ logger = get_logger(__file__)
class OrgViewSet(BulkModelViewSet):
filter_fields = ('name',)
search_fields = ('name', 'comment')
queryset = Organization.objects.all()
serializer_class = OrgSerializer
permission_classes = (IsSuperUserOrAppUser,)
@@ -51,10 +54,12 @@ class OrgViewSet(BulkModelViewSet):
for model in models:
data = self.get_data_from_model(model)
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:
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()
return Response({'msg': True}, status=status.HTTP_200_OK)

View File

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

View File

@@ -43,4 +43,7 @@ def on_create_set_created_by(sender, instance=None, **kwargs):
return
if hasattr(instance, 'created_by') and not instance.created_by:
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
multi_command_storage = get_multi_command_storage()
queryset = multi_command_storage.filter(
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"),
date_from=date_from, date_to=date_to,
user=q.get("user"), asset=q.get("asset"), 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(),
)
return queryset
def filter_queryset(self, queryset):
# 解决es存储命令时父类根据filter_fields过滤出现异常的问题返回的queryset类型list
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):
now = timezone.now()
days_ago = now - timezone.timedelta(days=self.default_days_ago)

View File

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

View File

@@ -16,7 +16,7 @@ class CommandBase(object):
@abc.abstractmethod
def filter(self, date_from=None, date_to=None,
user=None, asset=None, system_user=None,
input=None, session=None):
input=None, session=None, risk_level=None, org_id=None):
pass
@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)
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)
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)
is_success = models.BooleanField(default=True, db_index=True)
is_finished = models.BooleanField(default=False, db_index=True)

View File

@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
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_LOGIN_CONFIRM = 'login_confirm'
TYPE_REQUEST_ASSET_PERM = 'request_asset'
TYPE_CHOICES = (
(TYPE_GENERAL, _("General")),
(TYPE_LOGIN_CONFIRM, _("Login confirm"))
(TYPE_LOGIN_CONFIRM, _("Login confirm")),
(TYPE_REQUEST_ASSET_PERM, _('Request asset permission'))
)
ACTION_APPROVE = 'approve'
ACTION_REJECT = 'reject'

View File

@@ -4,3 +4,6 @@
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 -*-
#
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'
router = BulkRouter()
# router.register('tickets/request-asset-perm', api.RequestAssetPermTicketViewSet, 'ticket-request-asset-perm')
router.register('tickets', api.TicketViewSet, 'ticket')
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.permissions import IsAuthenticated
from django.conf import settings
from common.permissions import (
IsCurrentUserOrReadOnly
@@ -64,8 +65,9 @@ class UserProfileApi(generics.RetrieveUpdateAPIView):
return self.request.user
def retrieve(self, request, *args, **kwargs):
age = request.session.get_expiry_age()
request.session.set_expiry(age)
if not settings.SESSION_EXPIRE_AT_BROWSER_CLOSE:
age = request.session.get_expiry_age()
request.session.set_expiry(age)
return super().retrieve(request, *args, **kwargs)

View File

@@ -18,6 +18,7 @@ from ..serializers import UserSerializer, UserRetrieveSerializer
from .mixins import UserQuerysetMixin
from ..models import User
from ..signals import post_user_create
from ..filters import OrgRoleUserFilterBackend
logger = get_logger(__name__)
@@ -35,6 +36,7 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet):
'default': UserSerializer,
'retrieve': UserRetrieveSerializer
}
extra_filter_backends = [OrgRoleUserFilterBackend]
def get_queryset(self):
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):
users_field = self.fields.get('users')
if users_field:
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
users_field.child_relation.queryset = utils.get_current_org_members()
@classmethod
def setup_eager_loading(cls, queryset):

View File

@@ -87,7 +87,11 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer):
if not role:
return
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
def validate_role(self, value):
@@ -320,3 +324,9 @@ class UserUpdatePublicKeySerializer(serializers.ModelSerializer):
new_public_key = self.validated_data.get('public_key')
instance.set_public_key(new_public_key)
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():
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):

View File

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

View File

@@ -5,15 +5,19 @@ utils_dir=$(pwd)
project_dir=$(dirname "$utils_dir")
release_dir=${project_dir}/release
# 安装依赖包
command -v git || yum -y install git
# 打包
cd "${project_dir}" || exit 3
rm -rf "${release_dir:?}/*"
rm -rf "${release_dir:?}"/*
to_dir="${release_dir}/jumpserver"
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
alias sedi="sed -i ''"

26
utils/example_api.py Normal file
View File

@@ -0,0 +1,26 @@
#!/usr/bin/env python
import requests
# 私有token页面上目前不允许创建只能后台生成见 https://docs.jumpserver.org/zh/master/dev/rest_api/
private_token = '10659d70a223235b8f76d45a3023eca1147488d7'
def do_request(url, data=None, method='get', params=None, org_id=''):
authorization = 'Token {}'.format(private_token)
headers = {'Authorization': authorization, 'Content-Type': 'application/json'}
if org_id:
headers['X-JMS-ORG'] = org_id
resp = requests.request(method=method, url=url, data=data, params=params, headers=headers)
return resp
def get_assets_list():
url = 'http://localhost:8080/api/v1/assets/assets/?limit=10'
resp = do_request(url)
print(resp.status_code)
print(resp.json())
print(resp)
if __name__ == '__main__':
get_assets_list()