mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-15 16:42:34 +00:00
Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
526babb724 | ||
|
|
a3c1d1e730 | ||
|
|
8905613e50 | ||
|
|
d6d85dff40 | ||
|
|
28b92a0d80 | ||
|
|
e4d2b5e98d | ||
|
|
1e1cd2e1aa | ||
|
|
5065490a25 | ||
|
|
070af8c491 | ||
|
|
08fdc57543 | ||
|
|
bb60d2a1d9 | ||
|
|
0014bd0cb9 | ||
|
|
9488c8bd97 | ||
|
|
1f30d459ae | ||
|
|
4e933fc1ca | ||
|
|
c0f3a1f64a | ||
|
|
0f70f5eccf | ||
|
|
eef942c155 | ||
|
|
061592fa6b | ||
|
|
c7a02586c1 | ||
|
|
ddcd4ebbfc | ||
|
|
9550ea62fb | ||
|
|
abcb589658 | ||
|
|
1bb366ad94 | ||
|
|
a5df7738f6 | ||
|
|
da858c8998 | ||
|
|
724a8f6324 | ||
|
|
437df9a533 | ||
|
|
f2c70d0bba | ||
|
|
ea913a5b6e | ||
|
|
c0cd8878dc | ||
|
|
15e995ade6 | ||
|
|
cadf42f3fa | ||
|
|
f588093cd3 | ||
|
|
7c12f8f462 | ||
|
|
6f5a92c21f | ||
|
|
17a76994dc | ||
|
|
39d793bc47 | ||
|
|
c3eafbee8c | ||
|
|
10f99be100 | ||
|
|
8eb6cfa9c9 | ||
|
|
f430c9e435 | ||
|
|
10c428a432 | ||
|
|
a30c603bdc | ||
|
|
39a75074af | ||
|
|
452ed2baf1 | ||
|
|
8c7240193a | ||
|
|
b622aca9af | ||
|
|
ebf1a9d5e2 | ||
|
|
69f49f7776 | ||
|
|
fcd684e2db | ||
|
|
afcb6bd77c | ||
|
|
1c264399bb | ||
|
|
872e2546e9 | ||
|
|
8f347eee4d | ||
|
|
fa886b90c2 | ||
|
|
caf312c5be | ||
|
|
ac6168a06c | ||
|
|
eba9f2325a | ||
|
|
b46e772d09 | ||
|
|
183df82a75 | ||
|
|
98c91d0f18 | ||
|
|
e17d875206 | ||
|
|
4b1e84ed8a | ||
|
|
71ee33e3be | ||
|
|
5dd24f5cf9 | ||
|
|
2b6e818943 | ||
|
|
fdcda83c93 | ||
|
|
6e3369c944 | ||
|
|
d7e432a851 | ||
|
|
c0a153d13a |
12
.github/workflows/jms-generic-action-handler.yml
vendored
Normal file
12
.github/workflows/jms-generic-action-handler.yml
vendored
Normal 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 }}
|
||||
32
Dockerfile
32
Dockerfile
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
18
apps/assets/migrations/0050_auto_20200711_1740.py
Normal file
18
apps/assets/migrations/0050_auto_20200711_1740.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
22
apps/assets/migrations/0051_auto_20200713_1143.py
Normal file
22
apps/assets/migrations/0051_auto_20200713_1143.py
Normal 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')},
|
||||
),
|
||||
]
|
||||
22
apps/assets/migrations/0052_auto_20200715_1535.py
Normal file
22
apps/assets/migrations/0052_auto_20200715_1535.py
Normal 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')},
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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):
|
||||
|
||||
18
apps/audits/migrations/0009_auto_20200624_1654.py
Normal file
18
apps/audits/migrations/0009_auto_20200624_1654.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@@ -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'))
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
0
apps/common/db/__init__.py
Normal file
0
apps/common/db/__init__.py
Normal file
28
apps/common/db/aggregates.py
Normal file
28
apps/common/db/aggregates.py
Normal 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
11
apps/common/drf/api.py
Normal 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
|
||||
@@ -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)
|
||||
|
||||
5
apps/common/drf/serializers.py
Normal file
5
apps/common/drf/serializers.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
|
||||
class EmptySerializer(Serializer):
|
||||
pass
|
||||
@@ -1,3 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
|
||||
class JMSException(APIException):
|
||||
pass
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
73
apps/static/js/plugins/jsencrypt/jsencrypt.min.js
vendored
Normal file
73
apps/static/js/plugins/jsencrypt/jsencrypt.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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)
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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
|
||||
|
||||
18
apps/terminal/migrations/0024_auto_20200715_1713.py
Normal file
18
apps/terminal/migrations/0024_auto_20200715_1713.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from .ticket import *
|
||||
from .request_asset_perm import *
|
||||
|
||||
137
apps/tickets/api/request_asset_perm.py
Normal file
137
apps/tickets/api/request_asset_perm.py
Normal 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
|
||||
25
apps/tickets/exceptions.py
Normal file
25
apps/tickets/exceptions.py
Normal 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
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from .ticket import *
|
||||
from .request_asset_perm import *
|
||||
|
||||
141
apps/tickets/serializers/request_asset_perm.py
Normal file
141
apps/tickets/serializers/request_asset_perm.py
Normal 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)
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
32
apps/users/filters.py
Normal 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}'
|
||||
)
|
||||
)
|
||||
]
|
||||
@@ -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):
|
||||
|
||||
@@ -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
2
jms
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
26
utils/example_api.py
Normal 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()
|
||||
Reference in New Issue
Block a user